summaryrefslogtreecommitdiff
path: root/src/main/java
diff options
context:
space:
mode:
authorBrian S. O'Neill <bronee@gmail.com>2006-08-30 01:56:35 +0000
committerBrian S. O'Neill <bronee@gmail.com>2006-08-30 01:56:35 +0000
commit1205ea08cc9e53de40b71447a79daf378cb0a205 (patch)
treeb86d9fa0efa8a1276104354af513ef533db52277 /src/main/java
parentab635e23ad5fb9cd9edb61665a3654ee3e4b372a (diff)
Add filter implementation
Diffstat (limited to 'src/main/java')
-rw-r--r--src/main/java/com/amazon/carbonado/filter/AndFilter.java128
-rw-r--r--src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java116
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Binder.java93
-rw-r--r--src/main/java/com/amazon/carbonado/filter/ClosedFilter.java127
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Distributer.java78
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Filter.java505
-rw-r--r--src/main/java/com/amazon/carbonado/filter/FilterParser.java316
-rw-r--r--src/main/java/com/amazon/carbonado/filter/FilterValues.java623
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Group.java134
-rw-r--r--src/main/java/com/amazon/carbonado/filter/OpenFilter.java127
-rw-r--r--src/main/java/com/amazon/carbonado/filter/OrFilter.java128
-rw-r--r--src/main/java/com/amazon/carbonado/filter/PropertyFilter.java472
-rw-r--r--src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java186
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Reducer.java103
-rw-r--r--src/main/java/com/amazon/carbonado/filter/RelOp.java78
-rw-r--r--src/main/java/com/amazon/carbonado/filter/Visitor.java55
-rw-r--r--src/main/java/com/amazon/carbonado/filter/package-info.java24
17 files changed, 3293 insertions, 0 deletions
diff --git a/src/main/java/com/amazon/carbonado/filter/AndFilter.java b/src/main/java/com/amazon/carbonado/filter/AndFilter.java
new file mode 100644
index 0000000..92c3937
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/AndFilter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Filter tree node that performs a logical 'and' test.
+ *
+ * @author Brian S O'Neill
+ */
+public class AndFilter<S extends Storable> extends BinaryOpFilter<S> {
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if either filter is null
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> AndFilter<S> getCanonical(Filter<S> left, Filter<S> right) {
+ return (AndFilter<S>) cCanonical.put(new AndFilter<S>(left, right));
+ }
+
+ /**
+ * @throws IllegalArgumentException if either filter is null
+ */
+ private AndFilter(Filter<S> left, Filter<S> right) {
+ super(left, right);
+ }
+
+ public Filter<S> not() {
+ return mLeft.not().or(mRight.not());
+ }
+
+ public <R, P> R accept(Visitor<S, R, P> visitor, P param) {
+ return visitor.visit(this, param);
+ }
+
+ Filter<S> buildDisjunctiveNormalForm() {
+ Filter<S> left = mLeft.reduce().dnf();
+ Filter<S> right = mRight.reduce().dnf();
+ if (left instanceof OrFilter) {
+ return left.accept(new Distributer<S>(true, true), right).reduce().dnf();
+ }
+ if (right instanceof OrFilter) {
+ return right.accept(new Distributer<S>(false, true), left).reduce().dnf();
+ }
+ return left.and(right).reduce();
+ }
+
+ Filter<S> buildConjunctiveNormalForm() {
+ return mLeft.cnf().and(mRight.cnf()).reduce();
+ }
+
+ boolean checkIsDisjunctiveNormalForm() {
+ return (!(mLeft instanceof OrFilter))
+ && (!(mRight instanceof OrFilter))
+ && mLeft.isDisjunctiveNormalForm()
+ && mRight.isDisjunctiveNormalForm();
+ }
+
+ boolean checkIsConjunctiveNormalForm() {
+ return mLeft.isConjunctiveNormalForm() && mRight.isConjunctiveNormalForm();
+ }
+
+ @Override
+ public int hashCode() {
+ return mLeft.hashCode() * 31 + mRight.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof AndFilter) {
+ AndFilter<?> other = (AndFilter<?>) obj;
+ return getStorableType() == other.getStorableType()
+ && mLeft.equals(other.mLeft) && mRight.equals(other.mRight);
+ }
+ return false;
+ }
+
+ public void appendTo(Appendable app, FilterValues<S> values) throws IOException {
+ if (mLeft instanceof OrFilter) {
+ app.append('(');
+ mLeft.appendTo(app, values);
+ app.append(')');
+ } else {
+ mLeft.appendTo(app, values);
+ }
+ app.append(" & ");
+ if (mRight instanceof OrFilter) {
+ app.append('(');
+ mRight.appendTo(app, values);
+ app.append(')');
+ } else {
+ mRight.appendTo(app, values);
+ }
+ }
+
+ void dumpTree(Appendable app, int indentLevel) throws IOException {
+ mRight.dumpTree(app, indentLevel + 1);
+ for (int i=0; i<indentLevel; i++) {
+ app.append(" ");
+ }
+ app.append("and");
+ app.append('\n');
+ mLeft.dumpTree(app, indentLevel + 1);
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java b/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java
new file mode 100644
index 0000000..c95f493
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/BinaryOpFilter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Base class for filter tree nodes that have a left and right child.
+ *
+ * @author Brian S O'Neill
+ */
+public abstract class BinaryOpFilter<S extends Storable> extends Filter<S> {
+ // Bits used in mState.
+ private static final byte REDUCED = 0x1 << 0; // tree is reduced
+ private static final byte DNF_KNOWN = 0x1 << 1; // disjunctive normal form state is known
+ private static final byte DNF = 0x1 << 2; // is disjunctive normal form if set
+ private static final byte CNF_KNOWN = 0x1 << 3; // conjunctive normal form state is known
+ private static final byte CNF = 0x1 << 4; // is conjunctive normal form if set
+ private static final byte BOUND = 0x1 << 5; // properties are bound when set
+
+ final Filter<S> mLeft;
+ final Filter<S> mRight;
+
+ byte mState;
+
+ BinaryOpFilter(Filter<S> left, Filter<S> right) {
+ super(left == null ? null : left.getStorableType());
+ if (right == null) {
+ throw new IllegalArgumentException();
+ }
+ if (left.getStorableType() != right.getStorableType()) {
+ throw new IllegalArgumentException("Type mismatch");
+ }
+ mLeft = left;
+ mRight = right;
+ }
+
+ public Filter<S> getLeftFilter() {
+ return mLeft;
+ }
+
+ public Filter<S> getRightFilter() {
+ return mRight;
+ }
+
+ public Filter<S> bind() {
+ if (isBound()) {
+ return this;
+ }
+ return accept(new Binder<S>(), null);
+ }
+
+ public boolean isBound() {
+ return (mState & BOUND) != 0;
+ }
+
+ void markBound() {
+ mState |= BOUND;
+ }
+
+ final synchronized boolean isDisjunctiveNormalForm() {
+ if ((mState & DNF_KNOWN) != 0) { // if dnf state is known...
+ return (mState & DNF) != 0; // return true if dnf
+ }
+ if (checkIsDisjunctiveNormalForm()) {
+ mState |= (DNF_KNOWN | DNF); // dnf state is now known, and is dnf
+ return true;
+ } else {
+ mState |= DNF_KNOWN; // dnf state is now known, and is not dnf
+ mState &= ~DNF;
+ return false;
+ }
+ }
+
+ abstract boolean checkIsDisjunctiveNormalForm();
+
+ final synchronized boolean isConjunctiveNormalForm() {
+ if ((mState & CNF_KNOWN) != 0) { // if cnf state is known...
+ return (mState & CNF) != 0; // return true if cnf
+ }
+ if (checkIsConjunctiveNormalForm()) {
+ mState |= (CNF_KNOWN | CNF); // cnf state is now known, and is cnf
+ return true;
+ } else {
+ mState |= CNF_KNOWN; // cnf state is now known, and is not cnf
+ mState &= ~CNF;
+ return false;
+ }
+ }
+
+ abstract boolean checkIsConjunctiveNormalForm();
+
+ synchronized boolean isReduced() {
+ return (mState & REDUCED) != 0;
+ }
+
+ synchronized void markReduced() {
+ mState |= REDUCED;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Binder.java b/src/main/java/com/amazon/carbonado/filter/Binder.java
new file mode 100644
index 0000000..7a854f1
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Binder.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Used by bind operation - assigns bind IDs to property filters. This allows
+ * operations like dnf to not lose track of variable assignments even after
+ * more nodes are added to the filter tree.
+ *
+ * @author Brian S O'Neill
+ */
+class Binder<S extends Storable> extends Visitor<S, Filter<S>, Object> {
+ // Maps PropertyFilter with bind ID zero to PropertyFilter with highest
+ // bind ID.
+ private final Map<PropertyFilter<S>, PropertyFilter<S>> mBindMap;
+
+ Binder() {
+ mBindMap = new IdentityHashMap<PropertyFilter<S>, PropertyFilter<S>>();
+ }
+
+ @Override
+ public Filter<S> visit(OrFilter<S> filter, Object param) {
+ Filter<S> left = filter.getLeftFilter();
+ Filter<S> newLeft = left.accept(this, null);
+ Filter<S> right = filter.getRightFilter();
+ Filter<S> newRight = right.accept(this, null);
+
+ Filter<S> newFilter;
+ if (left != newLeft || right != newRight) {
+ newFilter = newLeft.or(newRight);
+ } else {
+ newFilter = filter;
+ }
+
+ newFilter.markBound();
+ return newFilter;
+ }
+
+ @Override
+ public Filter<S> visit(AndFilter<S> filter, Object param) {
+ Filter<S> left = filter.getLeftFilter();
+ Filter<S> newLeft = left.accept(this, null);
+ Filter<S> right = filter.getRightFilter();
+ Filter<S> newRight = right.accept(this, null);
+
+ Filter<S> newFilter;
+ if (left != newLeft || right != newRight) {
+ newFilter = newLeft.and(newRight);
+ } else {
+ newFilter = filter;
+ }
+
+ newFilter.markBound();
+ return newFilter;
+ }
+
+ @Override
+ public Filter<S> visit(PropertyFilter<S> filter, Object param) {
+ if (filter.getBindID() != 0) {
+ return filter;
+ }
+ filter = PropertyFilter.getCanonical(filter, 1);
+ PropertyFilter<S> highest = mBindMap.get(filter);
+ if (highest == null) {
+ highest = filter;
+ } else {
+ highest = PropertyFilter.getCanonical(filter, highest.getBindID() + 1);
+ }
+ mBindMap.put(filter, highest);
+ return highest;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java b/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java
new file mode 100644
index 0000000..4584109
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/ClosedFilter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Filter which blocks any results from passing through.
+ *
+ * @author Brian S O'Neill
+ */
+public class ClosedFilter<S extends Storable> extends Filter<S> {
+ ClosedFilter(Class<S> type) {
+ super(type);
+ }
+
+ public Filter<S> and(Filter<S> filter) {
+ return this;
+ }
+
+ public Filter<S> or(Filter<S> filter) {
+ return filter;
+ }
+
+ public Filter<S> not() {
+ return getOpenFilter(getStorableType());
+ }
+
+ @Override
+ public FilterValues<S> initialFilterValues() {
+ return null;
+ }
+
+ @Override
+ PropertyFilterList<S> getTailPropertyFilterList() {
+ return null;
+ }
+
+ public <R, P> R accept(Visitor<S, R, P> visitor, P param) {
+ return visitor.visit(this, param);
+ }
+
+ public Filter<S> bind() {
+ return this;
+ }
+
+ public boolean isBound() {
+ return true;
+ }
+
+ void markBound() {
+ }
+
+ Filter<S> buildDisjunctiveNormalForm() {
+ return this;
+ }
+
+ Filter<S> buildConjunctiveNormalForm() {
+ return this;
+ }
+
+ boolean isDisjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isConjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isReduced() {
+ return true;
+ }
+
+ void markReduced() {
+ }
+
+ @Override
+ public int hashCode() {
+ return getStorableType().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof ClosedFilter) {
+ ClosedFilter<?> other = (ClosedFilter<?>) obj;
+ return getStorableType() == other.getStorableType();
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "closed";
+ }
+
+ public void appendTo(Appendable app, FilterValues<S> values) throws IOException {
+ app.append("closed");
+ }
+
+ void dumpTree(Appendable app, int indentLevel) throws IOException {
+ for (int i=0; i<indentLevel; i++) {
+ app.append(" ");
+ }
+ app.append("closed");
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Distributer.java b/src/main/java/com/amazon/carbonado/filter/Distributer.java
new file mode 100644
index 0000000..e47e842
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Distributer.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Algebraically distributes a tree into each node of the visted tree.
+ *
+ * @author Brian S O'Neill
+ */
+class Distributer<S extends Storable> extends Visitor<S, Filter<S>, Filter<S>> {
+ private final boolean mDoRight;
+ private final boolean mDoAnd;
+
+ /**
+ * @param doRight when true, distribute to the right, otherwise,
+ * distribute to the left.
+ * @param doAnd when true, distribute 'and' operations, otherwise
+ * distribute 'or' operations.
+ */
+ Distributer(boolean doRight, boolean doAnd) {
+ mDoRight = doRight;
+ mDoAnd = doAnd;
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param distribute node to distribute into candidate node
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(OrFilter<S> filter, Filter<S> distribute) {
+ return OrFilter.getCanonical(filter.getLeftFilter().accept(this, distribute),
+ filter.getRightFilter().accept(this, distribute));
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param distribute node to distribute into candidate node
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(AndFilter<S> filter, Filter<S> distribute) {
+ return AndFilter.getCanonical(filter.getLeftFilter().accept(this, distribute),
+ filter.getRightFilter().accept(this, distribute));
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param distribute node to distribute into candidate node
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(PropertyFilter<S> filter, Filter<S> distribute) {
+ if (mDoRight) {
+ return mDoAnd ? filter.and(distribute) : filter.or(distribute);
+ } else {
+ return mDoAnd ? distribute.and(filter) : distribute.or(filter);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Filter.java b/src/main/java/com/amazon/carbonado/filter/Filter.java
new file mode 100644
index 0000000..f688d60
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Filter.java
@@ -0,0 +1,505 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+import java.util.Map;
+import org.cojen.util.SoftValuedHashMap;
+import org.cojen.util.WeakCanonicalSet;
+import org.cojen.util.WeakIdentityMap;
+
+import com.amazon.carbonado.MalformedFilterException;
+import com.amazon.carbonado.Storable;
+
+import com.amazon.carbonado.info.ChainedProperty;
+
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * An immutable tree structure representing a query result filter. Filters can
+ * be created using a builder pattern, by expression parsing, or by a
+ * combination of techniques. Filter instances are canonical, which means that
+ * equivalent instances can be compared for equality using the '==' operator.
+ *
+ * <p>Any method that accepts a filter expression parses against the following
+ * syntax:
+ *
+ * <pre>
+ * Filter = OrFilter
+ * OrFilter = AndFilter { "|" AndFilter }
+ * AndFilter = NotFilter { "&" NotFilter }
+ * NotFilter = [ "!" ] EntityFilter
+ * EntityFilter = PropertyFilter
+ * | "(" Filter ")"
+ * PropertyFilter = ChainedProperty RelOp "?"
+ * RelOp = "=" | "!=" | "&lt;" | "&gt;=" | "&gt;" | "&lt;="
+ * ChainedProperty = Identifier { "." Identifier }
+ * </pre>
+ *
+ * @author Brian S O'Neill
+ */
+public abstract class Filter<S extends Storable> implements Appender {
+
+ private static final Object OPEN_KEY = new Object();
+ private static final Object CLOSED_KEY = new Object();
+
+ // Collection of canonical filters.
+ static WeakCanonicalSet cCanonical = new WeakCanonicalSet();
+
+ // Map<(weak)Class<S>, Map<Object, (soft)Filter<S>>>
+ private static Map cCache = new WeakIdentityMap();
+
+ /**
+ * Returns a cached filter instance that operates on the given type and
+ * filter expression.
+ *
+ * @param type type of Storable that query is made against
+ * @param expression query filter expression to parse
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if type or filter expression is null
+ * @throws MalformedFilterException if filter expression is malformed
+ */
+ public static <S extends Storable> Filter<S> filterFor(Class<S> type, String expression) {
+ Map<Object, Filter<S>> filterCache = getFilterCache(type);
+ synchronized (filterCache) {
+ Filter<S> filter = filterCache.get(expression);
+ if (filter == null) {
+ filter = new FilterParser<S>(type, expression).parseRoot();
+ filterCache.put(expression, filter);
+ }
+ return filter;
+ }
+ }
+
+ /**
+ * Returns a cached filter instance that operates on the given type, which
+ * allows all results to pass through.
+ *
+ * @param type type of Storable that query is made against
+ * @return canonical Filter instance
+ * @see OpenFilter
+ */
+ public static <S extends Storable> Filter<S> getOpenFilter(Class<S> type) {
+ Map<Object, Filter<S>> filterCache = getFilterCache(type);
+ synchronized (filterCache) {
+ Filter<S> filter = filterCache.get(OPEN_KEY);
+ if (filter == null) {
+ filter = new OpenFilter<S>(type);
+ filterCache.put(OPEN_KEY, filter);
+ }
+ return filter;
+ }
+ }
+
+ /**
+ * Returns a cached filter instance that operates on the given type, which
+ * prevents any results from passing through.
+ *
+ * @param type type of Storable that query is made against
+ * @return canonical Filter instance
+ * @see ClosedFilter
+ */
+ public static <S extends Storable> Filter<S> getClosedFilter(Class<S> type) {
+ Map<Object, Filter<S>> filterCache = getFilterCache(type);
+ synchronized (filterCache) {
+ Filter<S> filter = filterCache.get(CLOSED_KEY);
+ if (filter == null) {
+ filter = new ClosedFilter<S>(type);
+ filterCache.put(CLOSED_KEY, filter);
+ }
+ return filter;
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private static <S extends Storable> Map<Object, Filter<S>> getFilterCache(Class<S> type) {
+ synchronized (cCache) {
+ Map<Object, Filter<S>> filterCache = (Map<Object, Filter<S>>) cCache.get(type);
+ if (filterCache == null) {
+ filterCache = new SoftValuedHashMap(7);
+ cCache.put(type, filterCache);
+ }
+ return filterCache;
+ }
+ }
+
+ private final Class<S> mType;
+
+ // Root FilterValues, built on demand, which is immutable.
+ private transient volatile FilterValues<S> mFilterValues;
+
+ // Tail of PropertyFilterList used by mFilterValues. mFilterValues
+ // references the head.
+ private transient volatile PropertyFilterList<S> mTailPropertyFilterList;
+
+ /**
+ * Package-private constructor to prevent subclassing outside this package.
+ */
+ Filter(Class<S> type) {
+ mType = type;
+ }
+
+ /**
+ * Returns the storable type that this filter operates on.
+ */
+ public Class<S> getStorableType() {
+ return mType;
+ }
+
+ /**
+ * Returns a FilterValues instance for assigning values to a
+ * Filter. Returns null if Filter has no parameters.
+ *
+ * <p>Note: The returned FilterValues instance may reference a different
+ * filter instance than this one. Call getFilter to retrieve it. The
+ * difference is caused by the filter property values being {@link #bind bound}.
+ */
+ public FilterValues<S> initialFilterValues() {
+ if (mFilterValues == null) {
+ Filter<S> boundFilter = bind();
+
+ if (boundFilter != this) {
+ return boundFilter.initialFilterValues();
+ }
+
+ buildFilterValues();
+ }
+
+ return mFilterValues;
+ }
+
+ /**
+ * Returns tail of linked list, and so it can only be traversed by getting
+ * previous nodes.
+ *
+ * @return tail of PropertyFilterList, or null if no parameters
+ */
+ PropertyFilterList<S> getTailPropertyFilterList() {
+ if (mTailPropertyFilterList == null) {
+ buildFilterValues();
+ }
+
+ return mTailPropertyFilterList;
+ }
+
+ private void buildFilterValues() {
+ PropertyFilterList<S> list = accept(new PropertyFilterList.Builder<S>(), null);
+
+ // List should never be null since only OpenFilter and ClosedFilter
+ // have no properties, and they override initialFilterValues and
+ // getTailPropertyFilterList.
+ assert(list != null);
+
+ // Since FilterValues instances are immutable, save this for re-use.
+ mFilterValues = FilterValues.create(this, list);
+
+ // PropertyFilterList can be saved for re-use because it too is
+ // immutable (after PropertyFilterListBuilder has run).
+ mTailPropertyFilterList = list.get(-1);
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are only
+ * accepted by this filter and the one given.
+ *
+ * @param expression query filter expression to parse
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if filter is null
+ */
+ public final Filter<S> and(String expression) {
+ return and(new FilterParser<S>(mType, expression).parseRoot());
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are only
+ * accepted by this filter and the one given.
+ *
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if filter is null
+ */
+ public Filter<S> and(Filter<S> filter) {
+ if (filter instanceof OpenFilter) {
+ return this;
+ }
+ if (filter instanceof ClosedFilter) {
+ return filter;
+ }
+ return AndFilter.getCanonical(this, filter);
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are only
+ * accepted by this filter and the one given.
+ *
+ * @param propertyName property name to match on, which may be a chained property
+ * @param operator relational operator
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if property is not found
+ */
+ public final Filter<S> and(String propertyName, RelOp operator) {
+ ChainedProperty<S> prop = new FilterParser<S>(mType, propertyName).parseChainedProperty();
+ return and(PropertyFilter.getCanonical(prop, operator, 0));
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are only
+ * accepted by this filter and the one given.
+ *
+ * @param propertyName property name to match on, which may be a chained property
+ * @param operator relational operator
+ * @param constantValue constant value to match
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if property is not found
+ */
+ public final Filter<S> and(String propertyName, RelOp operator, Object constantValue) {
+ ChainedProperty<S> prop = new FilterParser<S>(mType, propertyName).parseChainedProperty();
+ return and(PropertyFilter.getCanonical(prop, operator, 0).constant(constantValue));
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are
+ * accepted either by this filter or the one given.
+ *
+ * @param expression query filter expression to parse
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if filter is null
+ */
+ public final Filter<S> or(String expression) {
+ return or(new FilterParser<S>(mType, expression).parseRoot());
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are
+ * accepted either by this filter or the one given.
+ *
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if filter is null
+ */
+ public Filter<S> or(Filter<S> filter) {
+ if (filter instanceof OpenFilter) {
+ return filter;
+ }
+ if (filter instanceof ClosedFilter) {
+ return this;
+ }
+ return OrFilter.getCanonical(this, filter);
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are
+ * accepted either by this filter or the one given.
+ *
+ * @param propertyName property name to match on, which may be a chained property
+ * @param operator relational operator
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if property is not found
+ */
+ public final Filter<S> or(String propertyName, RelOp operator) {
+ ChainedProperty<S> prop = new FilterParser<S>(mType, propertyName).parseChainedProperty();
+ return or(PropertyFilter.getCanonical(prop, operator, 0));
+ }
+
+ /**
+ * Returns a combined filter instance that accepts records which are
+ * accepted either by this filter or the one given.
+ *
+ * @param propertyName property name to match on, which may be a chained property
+ * @param operator relational operator
+ * @param constantValue constant value to match
+ * @return canonical Filter instance
+ * @throws IllegalArgumentException if property is not found
+ */
+ public final Filter<S> or(String propertyName, RelOp operator, Object constantValue) {
+ ChainedProperty<S> prop = new FilterParser<S>(mType, propertyName).parseChainedProperty();
+ return or(PropertyFilter.getCanonical(prop, operator, 0).constant(constantValue));
+ }
+
+ /**
+ * Returns the logical negation of this filter.
+ *
+ * @return canonical Filter instance
+ */
+ public abstract Filter<S> not();
+
+ /**
+ * Returns an equivalent filter that is in disjunctive normal form. In this
+ * form, all logical 'and' operations are performed before all logical 'or'
+ * operations. This method often returns a filter with more terms than
+ * before.
+ *
+ * <p>The tree is also normalized such that all terms in a common logical
+ * operation are ordered left to right. For example, expressions of the
+ * form {@code "(a = ? & b = ?) & (c = ? & d = ?)"} are converted to
+ * {@code "(((a = ?) & (b = ?)) & c = ?) & d = ?"}.
+ *
+ * <p>Although the disjunctive normal filter may have more terms, it can be
+ * used to extract values from a FilterValues instance created from this
+ * filter. This works because the disjunctive normal filter is composed of
+ * the same set of PropertyFilter instances.
+ *
+ * @return canonical Filter instance
+ */
+ public final Filter<S> disjunctiveNormalForm() {
+ return bind().dnf();
+ }
+
+ final Filter<S> dnf() {
+ Filter<S> filter = this;
+ if (!filter.isDisjunctiveNormalForm()) {
+ filter = filter.buildDisjunctiveNormalForm();
+ }
+ return filter.reduce();
+ }
+
+ /**
+ * Returns an equivalent filter that is in conjunctive normal form. In this
+ * form, all logical 'or' operations are performed before all logical 'and'
+ * operations. This method often returns a filter with more terms than
+ * before.
+ *
+ * <p>The tree is also normalized such that all terms in a common logical
+ * operation are ordered left to right. For example, expressions of the
+ * form {@code "(a = ? | b = ?) | (c = ? | d = ?)"} are converted to
+ * {@code "(((a = ?) | (b = ?)) | c = ?) | d = ?"}.
+ *
+ * <p>Although the conjunctive normal filter may have more terms, it can be
+ * used to extract values from a FilterValues instance created from this
+ * filter. This works because the conjunctive normal filter is composed of
+ * the same set of PropertyFilter instances.
+ *
+ * @return canonical Filter instance
+ */
+ public final Filter<S> conjunctiveNormalForm() {
+ return bind().cnf();
+ }
+
+ final Filter<S> cnf() {
+ Filter<S> filter = this;
+ if (!filter.isConjunctiveNormalForm()) {
+ filter = filter.buildConjunctiveNormalForm();
+ }
+ return filter.reduce();
+ }
+
+ /**
+ * Accept the given visitor subclass to traverse the filter tree.
+ *
+ * @param visitor visitor to traverse through the tree
+ * @param param generic input parameter passed to visit methods
+ * @return generic return value passed from visit methods
+ */
+ public abstract <R, P> R accept(Visitor<S, R, P> visitor, P param);
+
+ /**
+ * Walks through each property filter, assigning a bind ID to it. This step
+ * is automatically performed for proper dnf/cnf conversion, and for
+ * building FilterValues.
+ *
+ * @return canonical Filter instance with bound property filters
+ */
+ public abstract Filter<S> bind();
+
+ /**
+ * Returns true if all property filters are known to be properly
+ * bound. This is a side effect of calling {@link #bind}, {@link
+ * #initialFilterValues}, {@link #disjunctiveNormalForm} or {@link
+ * #conjunctiveNormalForm}.
+ */
+ public abstract boolean isBound();
+
+ /**
+ * Identify this filter node as bound. Should only be called by Binder.
+ */
+ abstract void markBound();
+
+ /**
+ * Returns an equivalent filter with redundant terms eliminated. The tree
+ * is also normalized such that all terms in a common logical operation are
+ * ordered left to right. For example, expressions of the form
+ * {@code "(a = ? & b = ?) & (c = ? & d = ?)"} are converted to
+ * {@code "(((a = ?) & (b = ?)) & c = ?) & d = ?"}.
+ *
+ * @return canonical Filter instance
+ */
+ public final Filter<S> reduce() {
+ return isReduced() ? this : accept(new Reducer<S>(), null);
+ }
+
+ abstract Filter<S> buildDisjunctiveNormalForm();
+
+ abstract Filter<S> buildConjunctiveNormalForm();
+
+ abstract boolean isDisjunctiveNormalForm();
+
+ abstract boolean isConjunctiveNormalForm();
+
+ /**
+ * Returns true if filter node is in a reduced form.
+ */
+ abstract boolean isReduced();
+
+ /**
+ * Identify this filter node as reduced. Should only be called by Reducer.
+ */
+ abstract void markReduced();
+
+ @Override
+ public abstract int hashCode();
+
+ @Override
+ public abstract boolean equals(Object obj);
+
+ /**
+ * Returns the string value of this filter, which is also parsable.
+ */
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ try {
+ appendTo(buf);
+ } catch (IOException e) {
+ // Not gonna happen.
+ }
+ return buf.toString();
+ }
+
+ /**
+ * Appends the string value of this filter into the given Appendable.
+ */
+ public void appendTo(Appendable app) throws IOException {
+ appendTo(app, null);
+ }
+
+ /**
+ * Appends the string value of this filter into the given Appendable.
+ *
+ * @param values optionally supply filter values
+ */
+ public abstract void appendTo(Appendable app, FilterValues<S> values)
+ throws IOException;
+
+ /**
+ * Prints a tree representation of the filter to the given Appendable.
+ */
+ public void dumpTree(Appendable app) throws IOException {
+ dumpTree(app, 0);
+ }
+
+ abstract void dumpTree(Appendable app, int indentLevel) throws IOException;
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/FilterParser.java b/src/main/java/com/amazon/carbonado/filter/FilterParser.java
new file mode 100644
index 0000000..5c483e0
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/FilterParser.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.MalformedFilterException;
+
+import com.amazon.carbonado.info.ChainedProperty;
+import com.amazon.carbonado.info.StorableInfo;
+import com.amazon.carbonado.info.StorableIntrospector;
+import com.amazon.carbonado.info.StorableProperty;
+
+/**
+ *
+ *
+ * @author Brian S O'Neill
+ */
+class FilterParser<S extends Storable> {
+ private final Class<S> mType;
+ private final String mFilter;
+ private int mPos;
+
+ FilterParser(Class<S> type, String filter) {
+ if (type == null) {
+ throw new IllegalArgumentException();
+ }
+ if (filter == null) {
+ throw new IllegalArgumentException("Query filter must not be null");
+ }
+ mType = type;
+ mFilter = filter;
+ }
+
+ // Design note: This parser is actually a scanner, parser, and type checker
+ // all rolled into one. This is okay since the grammar is so simple.
+
+ Filter<S> parseRoot() {
+ Filter<S> filter = parseFilter();
+ int c = nextCharIgnoreWhitespace();
+ if (c >= 0) {
+ mPos--;
+ throw error("Unexpected trailing characters");
+ }
+ return filter;
+ }
+
+ private Filter<S> parseFilter() {
+ return parseOrFilter();
+ }
+
+ private Filter<S> parseOrFilter() {
+ Filter<S> filter = parseAndFilter();
+ while (true) {
+ int c = nextCharIgnoreWhitespace();
+ if (c == '|') {
+ operatorCheck();
+ filter = filter.or(parseAndFilter());
+ } else {
+ mPos--;
+ break;
+ }
+ }
+ return filter;
+ }
+
+ private Filter<S> parseAndFilter() {
+ Filter<S> filter = parseNotFilter();
+ while (true) {
+ int c = nextCharIgnoreWhitespace();
+ if (c == '&') {
+ operatorCheck();
+ filter = filter.and(parseNotFilter());
+ } else {
+ mPos--;
+ break;
+ }
+ }
+ return filter;
+ }
+
+ private Filter<S> parseNotFilter() {
+ int c = nextCharIgnoreWhitespace();
+ if (c == '!') {
+ return parseEntityFilter().not();
+ } else {
+ mPos--;
+ return parseEntityFilter();
+ }
+ }
+
+ private Filter<S> parseEntityFilter() {
+ int c = nextCharIgnoreWhitespace();
+ if (c == '(') {
+ Filter<S> test = parseFilter();
+ c = nextCharIgnoreWhitespace();
+ if (c != ')') {
+ mPos--;
+ throw error("Right paren expected");
+ }
+ return test;
+ } else {
+ mPos--;
+ return parsePropertyFilter();
+ }
+ }
+
+ private PropertyFilter<S> parsePropertyFilter() {
+ ChainedProperty<S> chained = parseChainedProperty();
+ int c = nextCharIgnoreWhitespace();
+
+ RelOp op;
+ switch (c) {
+ case '=':
+ op = RelOp.EQ;
+ operatorCheck();
+ break;
+ case '!':
+ c = nextChar();
+ if (c == '=') {
+ op = RelOp.NE;
+ } else {
+ mPos--;
+ throw error("Inequality operator must be specified as '!='");
+ }
+ operatorCheck();
+ break;
+ case '<':
+ c = nextChar();
+ if (c == '=') {
+ op = RelOp.LE;
+ } else {
+ mPos--;
+ op = RelOp.LT;
+ }
+ operatorCheck();
+ break;
+ case '>':
+ c = nextChar();
+ if (c == '=') {
+ op = RelOp.GE;
+ } else {
+ mPos--;
+ op = RelOp.GT;
+ }
+ operatorCheck();
+ break;
+ case '?': case ':':
+ mPos--;
+ throw error("Relational operator missing");
+ case -1:
+ throw error("Relational operator expected");
+ default:
+ mPos--;
+ throw error("Unexpected operator character: '" + (char)c + '\'');
+ }
+
+ c = nextCharIgnoreWhitespace();
+
+ if (c != '?') {
+ mPos--;
+ throw error("Parameter placeholder '?' required");
+ }
+
+ return PropertyFilter.getCanonical(chained, op, 0);
+ }
+
+ private void operatorCheck() {
+ int c = nextChar();
+ mPos--;
+ switch (c) {
+ case -1:
+ case '?': case ':':
+ case '(': case ')':
+ case ' ': case '\r': case '\n': case '\t': case '\0':
+ return;
+ default:
+ if (Character.isWhitespace(c)) {
+ return;
+ }
+ }
+ if (!Character.isJavaIdentifierStart(c)) {
+ throw error("Unknown operator");
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ ChainedProperty<S> parseChainedProperty() {
+ String ident = parseIdentifier();
+ StorableProperty<S> prime =
+ StorableIntrospector.examine(mType).getAllProperties().get(ident);
+ if (prime == null) {
+ mPos -= ident.length();
+ throw error("Property \"" + ident + "\" not found for type: \"" +
+ mType.getName() + '"');
+ }
+
+ if (nextCharIgnoreWhitespace() != '.') {
+ mPos--;
+ return ChainedProperty.get(prime);
+ }
+
+ List<StorableProperty<?>> chain = new ArrayList<StorableProperty<?>>(4);
+ Class<?> type = prime.isJoin() ? prime.getJoinedType() : prime.getType();
+
+ while (true) {
+ ident = parseIdentifier();
+ if (Storable.class.isAssignableFrom(type)) {
+ StorableInfo<?> info =
+ StorableIntrospector.examine((Class<? extends Storable>) type);
+ Map<String, ? extends StorableProperty<?>> props = info.getAllProperties();
+ StorableProperty<?> prop = props.get(ident);
+ if (prop == null) {
+ mPos -= ident.length();
+ throw error("Property \"" + ident + "\" not found for type: \"" +
+ type.getName() + '"');
+ }
+ chain.add(prop);
+ type = prop.isJoin() ? prop.getJoinedType() : prop.getType();
+ } else {
+ throw error("Property \"" + ident + "\" not found for type \"" +
+ type.getName() + "\" because type has no properties");
+ }
+ if (nextCharIgnoreWhitespace() != '.') {
+ mPos--;
+ break;
+ }
+ }
+
+ return ChainedProperty.get(prime, (StorableProperty<?>[]) chain.toArray(new StorableProperty[chain.size()]));
+ }
+
+ private String parseIdentifier() {
+ int start = mPos;
+ int c = nextChar();
+ if (c < 0) {
+ throw error("Identifier expected");
+ }
+ if (!Character.isJavaIdentifierStart(c)) {
+ mPos--;
+ throw error("Not a valid character for start of identifier: '" + (char)c + '\'');
+ }
+ do {
+ c = nextChar();
+ } while (Character.isJavaIdentifierPart(c));
+
+ return mFilter.substring(start, --mPos);
+ }
+
+ /**
+ * Returns -1 if no more characters.
+ */
+ private int nextChar() {
+ String filter = mFilter;
+ int pos = mPos;
+ int c = (pos >= filter.length()) ? -1 : mFilter.charAt(pos);
+ mPos = pos + 1;
+ return c;
+ }
+
+ private int nextCharIgnoreWhitespace() {
+ int c;
+ while ((c = nextChar()) >= 0) {
+ switch (c) {
+ case ' ': case '\r': case '\n': case '\t': case '\0':
+ break;
+ default:
+ if (Character.isWhitespace(c)) {
+ break;
+ }
+ return c;
+ }
+ }
+ return c;
+ }
+
+ private MalformedFilterException error(String message) {
+ return error(message, mPos);
+ }
+
+ private MalformedFilterException error(String message, int pos) {
+ if (pos <= 0 || mFilter.length() == 0) {
+ message += " (at start of filter expession)";
+ } else if (pos >= mFilter.length()) {
+ message += " (at end of filter expression)";
+ } else {
+ // Show the next 20 characters, or show 17 + ellipsis if more than 20.
+ int remaining = mFilter.length() - pos;
+ if (remaining <= 20) {
+ message = message + " (at \"" + mFilter.substring(pos) + "\")";
+ } else {
+ message = message + " (at \"" + mFilter.substring(pos, pos + 17) + "...\")";
+ }
+ }
+ return new MalformedFilterException(mFilter, message, pos);
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/FilterValues.java b/src/main/java/com/amazon/carbonado/filter/FilterValues.java
new file mode 100644
index 0000000..162194c
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/FilterValues.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.util.Appender;
+
+/**
+ * Assigns values to {@link Filter} placeholders. FilterValues instances are
+ * immutable.
+ *
+ * @author Brian S O'Neill
+ */
+public class FilterValues<S extends Storable> implements Appender {
+ private static final Object[] NO_VALUES = new Object[0];
+
+ static <S extends Storable> FilterValues<S>
+ create(Filter<S> filter, PropertyFilterList<S> propFilterList)
+ {
+ return create(filter, propFilterList, null, null);
+ }
+
+ private static <S extends Storable> FilterValues<S>
+ create(Filter<S> filter, PropertyFilterList<S> propFilterList,
+ FilterValues<S> prevValues, Object prevValue)
+ {
+ FilterValues<S> fv = new FilterValues<S>(filter, propFilterList, prevValues, prevValue);
+ PropertyFilter<S> propFilter;
+ while (propFilterList != null
+ && (propFilter = propFilterList.getPropertyFilter()).isConstant())
+ {
+ propFilterList = propFilterList.getNext();
+ fv = new FilterValues<S>(filter, propFilterList, fv, propFilter.constant());
+ }
+ return fv;
+ }
+
+ private final Filter<S> mFilter;
+ private final PropertyFilterList<S> mCurrentProperty;
+ private final FilterValues<S> mPrevValues;
+ private final Object mPrevValue;
+
+ private transient volatile Map<PropertyFilter<S>, Object> mValueMap;
+
+ private FilterValues(Filter<S> filter,
+ PropertyFilterList<S> propFilterList,
+ FilterValues<S> prevValues,
+ Object prevValue)
+ {
+ mFilter = filter;
+ mCurrentProperty = propFilterList;
+ mPrevValues = prevValues;
+ mPrevValue = prevValue;
+ }
+
+ /**
+ * Returns the Filter that this FilterValues instance applies to.
+ */
+ public Filter<S> getFilter() {
+ return mFilter;
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(int value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(int.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(long value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(long.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(float value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(float.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(double value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(double.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(boolean value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(boolean.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(char value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(char.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(byte value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(byte.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(short value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(short.class, value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameter filled in.
+ *
+ * @param value parameter value to fill in
+ * @throws IllegalStateException if no blank parameters
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> with(Object value) {
+ PropertyFilterList<S> current = currentProperty();
+ Object obj;
+ try {
+ obj = current.getPropertyFilter().adaptValue(value);
+ } catch (IllegalArgumentException e) {
+ throw mismatch(value == null ? null : value.getClass(), value);
+ }
+ return with(current, obj);
+ }
+
+ /**
+ * Returns a new FilterValues instance with the next blank parameters filled in.
+ *
+ * @param values parameter values to fill in; if null or empty, this
+ * FilterValues instance is returned
+ * @throws IllegalStateException if no blank parameters or if too many
+ * parameter values supplied
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ public FilterValues<S> withValues(Object... values) {
+ if (values == null) {
+ return this;
+ }
+ if (values.length > getBlankParameterCount()) {
+ throw new IllegalStateException("Too many values supplied");
+ }
+ FilterValues<S> filterValues = this;
+ for (Object value : values) {
+ filterValues = filterValues.with(value);
+ }
+ return filterValues;
+ }
+
+ private FilterValues<S> with(PropertyFilterList<S> current, Object value) {
+ return create(mFilter, current.getNext(), this, value);
+ }
+
+ /**
+ * Returns the amount of values yet to be assigned.
+ *
+ */
+ public int getBlankParameterCount() {
+ return mCurrentProperty == null ? 0 : (mCurrentProperty.getNextBlankRemaining() + 1);
+ }
+
+ /**
+ * Returns the value assigned to the given PropertyFilter. If null, value
+ * may be unassigned. Call getAssignedValue to have an exception thrown
+ * instead.
+ */
+ public Object getValue(PropertyFilter<S> propFilter) {
+ return getValue(propFilter, false);
+ }
+
+ /**
+ * Returns the value assigned to the given PropertyFilter, throwing an
+ * exception if not assigned. Call getValue to have null returned instead.
+ *
+ * @throws IllegalStateException if value is blank
+ */
+ public Object getAssignedValue(PropertyFilter<S> propFilter) throws IllegalStateException {
+ return getValue(propFilter, true);
+ }
+
+ private Object getValue(PropertyFilter<S> propFilter, boolean mustBeAssigned) {
+ if (propFilter.isConstant()) {
+ return propFilter.constant();
+ }
+
+ Map<PropertyFilter<S>, Object> map = mValueMap;
+
+ if (map == null) {
+ FilterValues<S> prevValues = mPrevValues;
+ if (prevValues == null) {
+ if (mustBeAssigned) {
+ throw valueNotFound(propFilter);
+ }
+ return null;
+ }
+
+ if (prevValues.mCurrentProperty.getPreviousRemaining() < 3) {
+ // Map would have few values in it, so don't bother building it.
+
+ FilterValues<S> filterValues = this;
+ do {
+ if (propFilter == prevValues.mCurrentProperty.getPropertyFilter()) {
+ return filterValues.mPrevValue;
+ }
+ filterValues = prevValues;
+ prevValues = prevValues.mPrevValues;
+ } while (prevValues != null);
+
+ if (mustBeAssigned) {
+ throw valueNotFound(propFilter);
+ }
+ return null;
+ }
+
+ map = buildValueMap();
+ }
+
+ Object value = map.get(propFilter);
+ if (value == null && mustBeAssigned && !map.containsKey(propFilter)) {
+ throw valueNotFound(propFilter);
+ }
+
+ return value;
+ }
+
+ /**
+ * Returns true if a value is assigned to the given PropertyFilter.
+ */
+ public boolean isAssigned(PropertyFilter<S> propFilter) {
+ if (propFilter.isConstant()) {
+ return true;
+ }
+
+ Map<PropertyFilter<S>, Object> map = mValueMap;
+
+ if (map == null) {
+ FilterValues<S> prevValues = mPrevValues;
+ if (prevValues == null) {
+ return false;
+ }
+
+ if (prevValues.mCurrentProperty.getPreviousRemaining() < 3) {
+ // Map would have few values in it, so don't bother building it.
+
+ do {
+ if (propFilter == prevValues.mCurrentProperty.getPropertyFilter()) {
+ return true;
+ }
+ prevValues = prevValues.mPrevValues;
+ } while (prevValues != null);
+
+ return false;
+ }
+
+ map = buildValueMap();
+ }
+
+ return map.containsKey(propFilter);
+ }
+
+ private Map<PropertyFilter<S>, Object> buildValueMap() {
+ Map<PropertyFilter<S>, Object> map =
+ new IdentityHashMap<PropertyFilter<S>, Object>();
+
+ FilterValues<S> filterValues = this;
+ FilterValues<S> prevValues = mPrevValues;
+
+ do {
+ map.put(prevValues.mCurrentProperty.getPropertyFilter(), filterValues.mPrevValue);
+ filterValues = prevValues;
+ prevValues = prevValues.mPrevValues;
+ } while (prevValues != null);
+
+ mValueMap = map;
+ return map;
+ }
+
+ /**
+ * Returns all values in this object, including those provided by filter
+ * constants. An IllegalStateException will result if any values are blank.
+ *
+ * @return new object array
+ * @throws IllegalStateException if any values are blank
+ */
+ public Object[] getValues() throws IllegalStateException {
+ return getValuesFor(mFilter);
+ }
+
+ /**
+ * Returns all supplied values in this object. Constant filter values are
+ * not included.
+ *
+ * @return new object array
+ */
+ public Object[] getSuppliedValues() {
+ FilterValues<S> prevValues = mPrevValues;
+ if (prevValues == null) {
+ return NO_VALUES;
+ }
+
+ int i = prevValues.mCurrentProperty.getBlankCount();
+ if (i == 0) {
+ return NO_VALUES;
+ }
+ Object[] values = new Object[i];
+ FilterValues filterValues = this;
+
+ while (true) {
+ if (!filterValues.mPrevValues.mCurrentProperty.getPropertyFilter().isConstant()) {
+ values[--i] = filterValues.mPrevValue;
+ if (i <= 0) {
+ break;
+ }
+ }
+ filterValues = prevValues;
+ prevValues = prevValues.mPrevValues;
+ }
+
+ return values;
+ }
+
+ /**
+ * Returns all values in this object, as required by the given Filter. The
+ * given Filter must be composed only of the same PropertyFilter instances
+ * as used to construct this object. An IllegalStateException will result
+ * otherwise.
+ *
+ * @return new object array
+ * @throws IllegalStateException if any values are blank
+ */
+ public Object[] getValuesFor(Filter<S> filter) throws IllegalStateException {
+ // Traverse filter properties in reverse, since the filter likely was
+ // used to create this FilterValues instance. If so, then no value map
+ // needs to be constructed.
+ PropertyFilterList<S> list = filter.getTailPropertyFilterList();
+ if (list == null) {
+ return NO_VALUES;
+ }
+
+ int i = list.getPreviousRemaining() + 1;
+ Object[] values = new Object[i];
+
+ FilterValues filterValues = this;
+ FilterValues<S> prevValues = mPrevValues;
+
+ for (; --i >= 0; list = list.getPrevious()) {
+ PropertyFilter<S> propFilter = list.getPropertyFilter();
+
+ Object value;
+
+ if (prevValues != null
+ && propFilter == prevValues.mCurrentProperty.getPropertyFilter()) {
+
+ value = filterValues.mPrevValue;
+
+ filterValues = prevValues;
+ prevValues = prevValues.mPrevValues;
+ } else {
+ if (i > 0 || mValueMap != null) {
+ value = getAssignedValue(propFilter);
+ } else {
+ // No need to force value map to be created since this is
+ // the last property to be processed. Do the same scan operation
+ // as performed by buildValueMap, except don't save the results.
+
+ filterValues = this;
+ prevValues = mPrevValues;
+
+ findValue: {
+ while (prevValues != null) {
+ if (propFilter == prevValues.mCurrentProperty.getPropertyFilter()) {
+ value = filterValues.mPrevValue;
+ break findValue;
+ }
+ filterValues = prevValues;
+ prevValues = prevValues.mPrevValues;
+ }
+
+ throw valueNotFound(propFilter);
+ }
+ }
+ }
+
+ values[i] = value;
+ }
+
+ return values;
+ }
+
+ private IllegalStateException valueNotFound(PropertyFilter<S> propFilter) {
+ return new IllegalStateException
+ ("Property value not found for: \"" + propFilter + "\" in filter \"" + this + '"');
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = mFilter.hashCode();
+ if (mPrevValue != null) {
+ hash += mPrevValue.hashCode();
+ }
+ if (mPrevValues != null) {
+ hash += mPrevValues.hashCode();
+ }
+ return hash;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof FilterValues) {
+ FilterValues<?> other = (FilterValues<?>) obj;
+ return mFilter.equals(other.mFilter)
+ && mCurrentProperty == other.mCurrentProperty
+ && (mPrevValues == null ? other.mPrevValues == null
+ : mPrevValues.equals(other.mPrevValues))
+ && (mPrevValue == null ? other.mPrevValue == null
+ : mPrevValue.equals(other.mPrevValue));
+ }
+ return false;
+ }
+
+ /**
+ * Returns the string value of the filter with any values substituted.
+ */
+ @Override
+ public String toString() {
+ StringBuilder buf = new StringBuilder();
+ try {
+ appendTo(buf);
+ } catch (java.io.IOException e) {
+ // Not gonna happen.
+ }
+ return buf.toString();
+ }
+
+ public void appendTo(Appendable app) throws IOException {
+ mFilter.appendTo(app, this);
+ }
+
+ private PropertyFilterList<S> currentProperty() {
+ PropertyFilterList<S> current = mCurrentProperty;
+ if (current == null) {
+ throw new IllegalStateException("No blank parameters");
+ }
+ return current;
+ }
+
+ private IllegalArgumentException mismatch(Class<?> actualType, Object actualValue) {
+ PropertyFilterList<S> current = currentProperty();
+ PropertyFilter<S> propFilter = current.getPropertyFilter();
+
+ StringBuilder b = new StringBuilder();
+
+ try {
+ propFilter.appendMismatchMessage(b, actualType, actualValue);
+ } catch (IOException e) {
+ // Not gonna happen
+ }
+
+ int subFilterCount = current.getPreviousRemaining() + current.getNextRemaining() + 1;
+
+ if (subFilterCount <= 1) {
+ b.append(" for filter \"");
+ b.append(propFilter);
+ b.append('"');
+ } else {
+ b.append(" for ");
+ appendOrdinal(b, current.getPreviousRemaining() + 1);
+ b.append(" sub filter in \"");
+ try {
+ appendTo(b);
+ } catch (IOException e) {
+ // Not gonna happen
+ }
+ b.append('"');
+ }
+
+ return new IllegalArgumentException(b.toString());
+ }
+
+ private void appendOrdinal(StringBuilder b, int value) {
+ b.append(value);
+
+ if (value != 11 && value != 12 && value != 13) {
+ value = value % 10;
+ switch (value) {
+ default:
+ break;
+ case 1:
+ b.append("st");
+ return;
+ case 2:
+ b.append("nd");
+ return;
+ case 3:
+ b.append("rd");
+ return;
+ }
+ }
+
+ b.append("th");
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Group.java b/src/main/java/com/amazon/carbonado/filter/Group.java
new file mode 100644
index 0000000..6805bf4
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Group.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Group of filter nodes that are all 'or'ed or 'and'ed together.
+ *
+ * @author Brian S O'Neill
+ */
+class Group<S extends Storable> {
+ final boolean mForAnd;
+ Set<Filter<S>> mChildren;
+
+ Group(boolean forAnd) {
+ mForAnd = forAnd;
+ }
+
+ boolean add(Filter<S> child) {
+ if (mChildren == null) {
+ mChildren = new LinkedHashSet<Filter<S>>(20);
+ }
+ return mChildren.add(child);
+ }
+
+ /**
+ * Reduce the group set by eliminating redundant children. The
+ * transformations applied are:
+ *
+ * ((a & b) | b) => b
+ * ((a | b) & b) => b
+ */
+ void reduce() {
+ Iterator<Filter<S>> it = mChildren.iterator();
+ aLoop:
+ while (it.hasNext()) {
+ Filter<S> a = it.next();
+ for (Filter<S> b : mChildren) {
+ if (a != b) {
+ if (a.accept(new Scanner<S>(!mForAnd), b)) {
+ it.remove();
+ continue aLoop;
+ }
+ }
+ }
+ }
+ }
+
+ Filter<S> merge() {
+ Filter<S> filter = null;
+ if (mChildren != null) {
+ for (Filter<S> child : mChildren) {
+ if (filter == null) {
+ filter = child;
+ } else if (mForAnd) {
+ filter = filter.and(child);
+ } else {
+ filter = filter.or(child);
+ }
+ }
+ }
+ return filter;
+ }
+
+ /**
+ * Does the work of reducing a group
+ */
+ private static class Scanner<S extends Storable> extends Visitor<S, Boolean, Filter<S>>{
+ private final boolean mForAnd;
+
+ Scanner(boolean forAnd) {
+ mForAnd = forAnd;
+ }
+
+ /**
+ * @return TRUE if overlap was found
+ */
+ @Override
+ public Boolean visit(OrFilter<S> filter, Filter<S> child) {
+ if (mForAnd) {
+ return false;
+ }
+ if (filter == child) {
+ return true;
+ }
+ return filter.getLeftFilter().accept(this, child) ||
+ filter.getRightFilter().accept(this, child);
+ }
+
+ /**
+ * @return TRUE if overlap was found
+ */
+ @Override
+ public Boolean visit(AndFilter<S> filter, Filter<S> child) {
+ if (!mForAnd) {
+ return false;
+ }
+ if (filter == child) {
+ return true;
+ }
+ return filter.getLeftFilter().accept(this, child) ||
+ filter.getRightFilter().accept(this, child);
+ }
+
+ /**
+ * @return TRUE if overlap was found
+ */
+ @Override
+ public Boolean visit(PropertyFilter<S> filter, Filter<S> child) {
+ return filter == child;
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/OpenFilter.java b/src/main/java/com/amazon/carbonado/filter/OpenFilter.java
new file mode 100644
index 0000000..407aed0
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/OpenFilter.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Filter which lets all results pass through.
+ *
+ * @author Brian S O'Neill
+ */
+public class OpenFilter<S extends Storable> extends Filter<S> {
+ OpenFilter(Class<S> type) {
+ super(type);
+ }
+
+ public Filter<S> and(Filter<S> filter) {
+ return filter;
+ }
+
+ public Filter<S> or(Filter<S> filter) {
+ return this;
+ }
+
+ public Filter<S> not() {
+ return getClosedFilter(getStorableType());
+ }
+
+ @Override
+ public FilterValues<S> initialFilterValues() {
+ return null;
+ }
+
+ @Override
+ PropertyFilterList<S> getTailPropertyFilterList() {
+ return null;
+ }
+
+ public <R, P> R accept(Visitor<S, R, P> visitor, P param) {
+ return visitor.visit(this, param);
+ }
+
+ public Filter<S> bind() {
+ return this;
+ }
+
+ public boolean isBound() {
+ return true;
+ }
+
+ void markBound() {
+ }
+
+ Filter<S> buildDisjunctiveNormalForm() {
+ return this;
+ }
+
+ Filter<S> buildConjunctiveNormalForm() {
+ return this;
+ }
+
+ boolean isDisjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isConjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isReduced() {
+ return true;
+ }
+
+ void markReduced() {
+ }
+
+ @Override
+ public int hashCode() {
+ return getStorableType().hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof OpenFilter) {
+ OpenFilter<?> other = (OpenFilter<?>) obj;
+ return getStorableType() == other.getStorableType();
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "open";
+ }
+
+ public void appendTo(Appendable app, FilterValues<S> values) throws IOException {
+ app.append("open");
+ }
+
+ void dumpTree(Appendable app, int indentLevel) throws IOException {
+ for (int i=0; i<indentLevel; i++) {
+ app.append(" ");
+ }
+ app.append("open");
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/OrFilter.java b/src/main/java/com/amazon/carbonado/filter/OrFilter.java
new file mode 100644
index 0000000..027ea21
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/OrFilter.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Filter tree node that performs a logical 'or' test.
+ *
+ * @author Brian S O'Neill
+ */
+public class OrFilter<S extends Storable> extends BinaryOpFilter<S> {
+ /**
+ * Returns a canonical instance.
+ *
+ * @throws IllegalArgumentException if either filter is null
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> OrFilter<S> getCanonical(Filter<S> left, Filter<S> right) {
+ return (OrFilter<S>) cCanonical.put(new OrFilter<S>(left, right));
+ }
+
+ /**
+ * @throws IllegalArgumentException if either filter is null
+ */
+ private OrFilter(Filter<S> left, Filter<S> right) {
+ super(left, right);
+ }
+
+ public Filter<S> not() {
+ return mLeft.not().and(mRight.not());
+ }
+
+ public <R, P> R accept(Visitor<S, R, P> visitor, P param) {
+ return visitor.visit(this, param);
+ }
+
+ Filter<S> buildDisjunctiveNormalForm() {
+ return mLeft.dnf().or(mRight.dnf()).reduce();
+ }
+
+ Filter<S> buildConjunctiveNormalForm() {
+ Filter<S> left = mLeft.reduce().cnf();
+ Filter<S> right = mRight.reduce().cnf();
+ if (left instanceof AndFilter) {
+ return left.accept(new Distributer<S>(true, false), right).reduce().cnf();
+ }
+ if (right instanceof AndFilter) {
+ return right.accept(new Distributer<S>(false, false), left).reduce().cnf();
+ }
+ return left.or(right).reduce();
+ }
+
+ boolean checkIsDisjunctiveNormalForm() {
+ return mLeft.isDisjunctiveNormalForm() && mRight.isDisjunctiveNormalForm();
+ }
+
+ boolean checkIsConjunctiveNormalForm() {
+ return (!(mLeft instanceof AndFilter))
+ && (!(mRight instanceof AndFilter))
+ && mLeft.isConjunctiveNormalForm()
+ && mRight.isConjunctiveNormalForm();
+ }
+
+ @Override
+ public int hashCode() {
+ return mLeft.hashCode() * 31 + mRight.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof OrFilter) {
+ OrFilter<?> other = (OrFilter<?>) obj;
+ return getStorableType() == other.getStorableType()
+ && mLeft.equals(other.mLeft) && mRight.equals(other.mRight);
+ }
+ return false;
+ }
+
+ public void appendTo(Appendable app, FilterValues<S> values) throws IOException {
+ if (mLeft instanceof AndFilter) {
+ app.append('(');
+ mLeft.appendTo(app, values);
+ app.append(')');
+ } else {
+ mLeft.appendTo(app, values);
+ }
+ app.append(" | ");
+ if (mRight instanceof AndFilter) {
+ app.append('(');
+ mRight.appendTo(app, values);
+ app.append(')');
+ } else {
+ mRight.appendTo(app, values);
+ }
+ }
+
+ void dumpTree(Appendable app, int indentLevel) throws IOException {
+ mRight.dumpTree(app, indentLevel + 1);
+ for (int i=0; i<indentLevel; i++) {
+ app.append(" ");
+ }
+ app.append("or");
+ app.append('\n');
+ mLeft.dumpTree(app, indentLevel + 1);
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
new file mode 100644
index 0000000..8fe03d6
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilter.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import java.io.IOException;
+
+import org.cojen.classfile.TypeDesc;
+
+import com.amazon.carbonado.Storable;
+import com.amazon.carbonado.info.ChainedProperty;
+
+/**
+ * Filter tree node that performs a relational test against a specific property
+ * value.
+ *
+ * @author Brian S O'Neill
+ */
+public class PropertyFilter<S extends Storable> extends Filter<S> {
+ // Indicates property has been bound to a constant value.
+ private static int BOUND_CONSTANT = -1;
+
+ /**
+ * Returns a canonical instance, creating a new one if there isn't one
+ * already in the cache.
+ *
+ * @throws IllegalArgumentException if property or operator is null
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> PropertyFilter<S> getCanonical(ChainedProperty<S> property,
+ RelOp op,
+ int bindID)
+ {
+ return (PropertyFilter<S>) cCanonical
+ .put(new PropertyFilter<S>(property, op, bindID, null));
+ }
+
+ /**
+ * Returns a canonical instance, creating a new one if there isn't one
+ * already in the cache.
+ *
+ * @throws IllegalArgumentException if property or operator is null
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> PropertyFilter<S> getCanonical(ChainedProperty<S> property,
+ RelOp op,
+ Object constant)
+ {
+ return (PropertyFilter<S>) cCanonical
+ .put(new PropertyFilter<S>(property, op, BOUND_CONSTANT, constant));
+ }
+
+ /**
+ * Returns a canonical instance, creating a new one if there isn't one
+ * already in the cache.
+ */
+ @SuppressWarnings("unchecked")
+ static <S extends Storable> PropertyFilter<S> getCanonical(PropertyFilter<S> filter,
+ int bindID)
+ {
+ if (filter.mBindID == bindID) {
+ return filter;
+ }
+ return (PropertyFilter<S>) cCanonical
+ .put(new PropertyFilter<S>(filter.getChainedProperty(),
+ filter.getOperator(), bindID, filter.mConstant));
+ }
+
+ private final ChainedProperty<S> mProperty;
+ private final RelOp mOp;
+
+ private final int mBindID;
+
+ private final Object mConstant;
+
+ private transient volatile Class<?> mBoxedType;
+
+ /**
+ * @throws IllegalArgumentException if property or operator is null
+ */
+ private PropertyFilter(ChainedProperty<S> property, RelOp op, int bindID, Object constant) {
+ super(property == null ? null : property.getPrimeProperty().getEnclosingType());
+ if (op == null) {
+ throw new IllegalArgumentException();
+ }
+ mProperty = property;
+ mOp = op;
+ mBindID = bindID;
+ mConstant = constant;
+ }
+
+ @Override
+ public Filter<S> not() {
+ if (mBindID == BOUND_CONSTANT) {
+ return getCanonical(mProperty, mOp.reverse(), mConstant);
+ } else {
+ return getCanonical(mProperty, mOp.reverse(), mBindID);
+ }
+ }
+
+ public <R, P> R accept(Visitor<S, R, P> visitor, P param) {
+ return visitor.visit(this, param);
+ }
+
+ public ChainedProperty<S> getChainedProperty() {
+ return mProperty;
+ }
+
+ /**
+ * Returns the type of the ChainedProperty.
+ */
+ public Class<?> getType() {
+ return mProperty.getType();
+ }
+
+ /**
+ * Returns the type of the ChainedProperty property, boxed into an object
+ * if primitive.
+ */
+ public Class<?> getBoxedType() {
+ if (mBoxedType == null) {
+ mBoxedType = TypeDesc.forClass(getType()).toObjectType().toClass();
+ }
+ return mBoxedType;
+ }
+
+ public RelOp getOperator() {
+ return mOp;
+ }
+
+ /**
+ * Bind ID is used to distinguish this PropertyFilter instance from another
+ * against the same property. For example, the filter "a = ? | a = ?"
+ * references the property 'a' twice. Each '?' parameter is bound to a
+ * different value, and so the bind ID for each property filter is
+ * different. "a = ?[1] | a = ?[2]".
+ *
+ * @return assigned bind ID, or 0 if unbound
+ */
+ public int getBindID() {
+ return mBindID;
+ }
+
+ public Filter<S> bind() {
+ return mBindID == 0 ? getCanonical(this, 1) : this;
+ }
+
+ public boolean isBound() {
+ return mBindID != 0;
+ }
+
+ /**
+ * Returns another PropertyFilter instance which is bound to the given constant value.
+ *
+ * @throws IllegalArgumentException if value is not compatible with property type
+ */
+ public PropertyFilter<S> constant(Object value) {
+ if (mBindID == BOUND_CONSTANT) {
+ if (mConstant == null && value == null || mConstant.equals(value)) {
+ return this;
+ }
+ }
+ return getCanonical(mProperty, mOp, adaptValue(value));
+ }
+
+ /**
+ * Returns the constant value of this PropertyFilter, which is valid only
+ * if isConstant returns true.
+ */
+ public Object constant() {
+ return mConstant;
+ }
+
+ /**
+ * Returns true if this PropertyFilter has a constant value.
+ */
+ public boolean isConstant() {
+ return mBindID == BOUND_CONSTANT;
+ }
+
+ void markBound() {
+ }
+
+ Filter<S> buildDisjunctiveNormalForm() {
+ return this;
+ }
+
+ Filter<S> buildConjunctiveNormalForm() {
+ return this;
+ }
+
+ boolean isDisjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isConjunctiveNormalForm() {
+ return true;
+ }
+
+ boolean isReduced() {
+ return true;
+ }
+
+ void markReduced() {
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(int value) {
+ Class<?> type = getBoxedType();
+ if (type == Integer.class) {
+ return Integer.valueOf(value);
+ } else if (type == Long.class) {
+ return Long.valueOf(value);
+ } else if (type == Double.class) {
+ return Double.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Integer.valueOf(value);
+ }
+ throw mismatch(int.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(long value) {
+ Class<?> type = getBoxedType();
+ if (type == Long.class) {
+ return Long.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Long.valueOf(value);
+ }
+ throw mismatch(long.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(float value) {
+ Class<?> type = getBoxedType();
+ if (type == Float.class) {
+ return Float.valueOf(value);
+ } else if (type == Double.class) {
+ return Double.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Float.valueOf(value);
+ }
+ throw mismatch(float.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(double value) {
+ Class<?> type = getBoxedType();
+ if (type == Double.class) {
+ return Double.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Double.valueOf(value);
+ }
+ throw mismatch(float.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(boolean value) {
+ Class<?> type = getBoxedType();
+ if (type == Boolean.class || type == Object.class) {
+ return Boolean.valueOf(value);
+ }
+ throw mismatch(boolean.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(char value) {
+ Class<?> type = getBoxedType();
+ if (type == Character.class || type == Object.class) {
+ return Character.valueOf(value);
+ } else if (type == String.class || type == CharSequence.class) {
+ return String.valueOf(value);
+ }
+ throw mismatch(char.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(byte value) {
+ Class<?> type = getBoxedType();
+ if (type == Byte.class) {
+ return Byte.valueOf(value);
+ } else if (type == Short.class) {
+ return Short.valueOf(value);
+ } else if (type == Integer.class) {
+ return Integer.valueOf(value);
+ } else if (type == Long.class) {
+ return Long.valueOf(value);
+ } else if (type == Double.class) {
+ return Double.valueOf(value);
+ } else if (type == Float.class) {
+ return Float.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Byte.valueOf(value);
+ }
+ throw mismatch(byte.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(short value) {
+ Class<?> type = getBoxedType();
+ if (type == Short.class) {
+ return Short.valueOf(value);
+ } else if (type == Integer.class) {
+ return Integer.valueOf(value);
+ } else if (type == Long.class) {
+ return Long.valueOf(value);
+ } else if (type == Double.class) {
+ return Double.valueOf(value);
+ } else if (type == Float.class) {
+ return Float.valueOf(value);
+ } else if (type == Number.class || type == Object.class) {
+ return Short.valueOf(value);
+ }
+ throw mismatch(short.class, value);
+ }
+
+ /**
+ * @throws IllegalArgumentException if type doesn't match
+ */
+ Object adaptValue(Object value) {
+ if (getBoxedType().isInstance(value)) {
+ return value;
+ }
+
+ Class<?> type = getType();
+
+ if (value == null) {
+ if (!type.isPrimitive()) {
+ return value;
+ }
+ } else if (type.isPrimitive()) {
+ TypeDesc actualPrim = TypeDesc.forClass(value.getClass()).toPrimitiveType();
+ if (actualPrim != null) {
+ if (type == actualPrim.toClass()) {
+ return value;
+ }
+ // Unbox and rebox.
+ switch (actualPrim.getTypeCode()) {
+ case TypeDesc.BYTE_CODE:
+ return adaptValue(((Number) value).byteValue());
+ case TypeDesc.SHORT_CODE:
+ return adaptValue(((Number) value).shortValue());
+ case TypeDesc.INT_CODE:
+ return adaptValue(((Number) value).intValue());
+ case TypeDesc.LONG_CODE:
+ return adaptValue(((Number) value).longValue());
+ case TypeDesc.FLOAT_CODE:
+ return adaptValue(((Number) value).floatValue());
+ case TypeDesc.DOUBLE_CODE:
+ return adaptValue(((Number) value).doubleValue());
+ case TypeDesc.BOOLEAN_CODE:
+ return adaptValue(((Boolean) value).booleanValue());
+ case TypeDesc.CHAR_CODE:
+ return adaptValue(((Character) value).charValue());
+ }
+ }
+ }
+
+ throw mismatch(value == null ? null : value.getClass(), value);
+ }
+
+ @Override
+ public int hashCode() {
+ return mProperty.hashCode() * 31 + mOp.hashCode() + mBindID;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj instanceof PropertyFilter) {
+ PropertyFilter<?> other = (PropertyFilter<?>) obj;
+ return getStorableType() == other.getStorableType()
+ && mProperty.equals(other.mProperty) && mOp.equals(other.mOp)
+ && mBindID == other.mBindID
+ && (mConstant == null
+ ? (other.mConstant == null)
+ : mConstant.equals(other.mConstant));
+ }
+ return false;
+ }
+
+ public void appendTo(Appendable app, FilterValues<S> values) throws IOException {
+ mProperty.appendTo(app);
+ app.append(' ');
+ app.append(mOp.toString());
+ app.append(' ');
+ if (values != null) {
+ Object value = values.getValue(this);
+ if (value != null || values.isAssigned(this)) {
+ app.append(String.valueOf(value));
+ return;
+ }
+ }
+ if (mBindID == BOUND_CONSTANT) {
+ app.append(String.valueOf(mConstant));
+ } else {
+ app.append('?');
+ /* Uncomment for testing
+ if (mBindID != 0) {
+ app.append('[').append(String.valueOf(mBindID)).append(']');
+ }
+ */
+ }
+ }
+
+ void appendMismatchMessage(Appendable a, Class<?> actualType, Object actualValue)
+ throws IOException
+ {
+ if (actualType == null || actualValue == null) {
+ a.append("Actual value is null, which cannot be assigned to type \"");
+ } else {
+ a.append("Actual value \"");
+ a.append(String.valueOf(actualValue));
+ a.append("\", of type \"");
+ a.append(TypeDesc.forClass(actualType).getFullName());
+ a.append("\", is incompatible with expected type of \"");
+ }
+ a.append(TypeDesc.forClass(getType()).getFullName());
+ a.append('"');
+ }
+
+ private IllegalArgumentException mismatch(Class<?> actualType, Object actualValue) {
+ StringBuilder b = new StringBuilder();
+ try {
+ appendMismatchMessage(b, actualType, actualValue);
+ } catch (IOException e) {
+ // Not gonna happen
+ }
+ return new IllegalArgumentException(b.toString());
+ }
+
+ void dumpTree(Appendable app, int indentLevel) throws IOException {
+ for (int i=0; i<indentLevel; i++) {
+ app.append(" ");
+ }
+ appendTo(app);
+ app.append('\n');
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java b/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java
new file mode 100644
index 0000000..447edef
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/PropertyFilterList.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Double linked list of leaf PropertyFilters in a Filter. The list order
+ * matches the left-to-right order of the PropertyFilters.
+ *
+ * @author Brian S O'Neill
+ */
+class PropertyFilterList<S extends Storable> {
+ private final PropertyFilter<S> mPropFilter;
+ private final PropertyFilterList<S> mNext;
+ private final int mNextRemaining;
+ private final int mNextBlankRemaining;
+
+ private PropertyFilterList<S> mPrev;
+ private int mPrevRemaining = -1;
+ private int mBlankCount = -1;
+
+ /**
+ * @param next pass null for tail of list
+ */
+ PropertyFilterList(PropertyFilter<S> propFilter, PropertyFilterList<S> next) {
+ mPropFilter = propFilter;
+ mNext = next;
+ mNextRemaining = next == null ? 0 : next.mNextRemaining + 1;
+ mNextBlankRemaining = next == null ? 0
+ : (next.mNextBlankRemaining + (next.mPropFilter.isConstant() ? 0 : 1));
+ }
+
+ public PropertyFilter<S> getPropertyFilter() {
+ return mPropFilter;
+ }
+
+ public Class<?> getNaturalPropertyType() {
+ return mPropFilter.getType();
+ }
+
+ /**
+ * Returns the property filter type, always represented as an object
+ * type. Primitive types are represented by a wrapper class.
+ */
+ public Class<?> getObjectPropertyType() {
+ return mPropFilter.getBoxedType();
+ }
+
+ /**
+ * Returns null if the next remaining is zero.
+ */
+ public PropertyFilterList<S> getNext() {
+ return mNext;
+ }
+
+ /**
+ * Returns the amount of next remaining list nodes.
+ */
+ public int getNextRemaining() {
+ return mNextRemaining;
+ }
+
+ /**
+ * Returns the amount of next remaining non-constant list nodes.
+ */
+ public int getNextBlankRemaining() {
+ return mNextBlankRemaining;
+ }
+
+ /**
+ * Returns null if no previous node.
+ */
+ public PropertyFilterList<S> getPrevious() {
+ return mPrev;
+ }
+
+ /**
+ * Returns the amount of previous remaining list nodes.
+ */
+ public int getPreviousRemaining() {
+ int remaining = mPrevRemaining;
+ if (remaining < 0) {
+ mPrevRemaining = remaining =
+ ((mPrev == null) ? 0 : (mPrev.getPreviousRemaining() + 1));
+ }
+ return remaining;
+ }
+
+ /**
+ * Returns the amount of non-constant list nodes up to this node.
+ */
+ public int getBlankCount() {
+ int count = mBlankCount;
+ if (count < 0) {
+ mBlankCount = count = (mPropFilter.isConstant() ? 0 : 1)
+ + ((mPrev == null) ? 0 : mPrev.getBlankCount());
+ }
+ return count;
+ }
+
+ /**
+ * @param index if zero, returns this, if one, returns next, etc. If
+ * negative, gets from last. -1 returns last, -2 returns next to last, etc.
+ * @return null if index too high or too low
+ */
+ public PropertyFilterList<S> get(int index) {
+ if (index <= 0) {
+ if (index == 0) {
+ return this;
+ }
+ if ((index = mNextRemaining + index + 1) <= 0) {
+ if (index == 0) {
+ return this;
+ }
+ return null;
+ }
+ }
+ if (mNext == null) {
+ return null;
+ }
+ // Tail recursion.
+ return mNext.get(index - 1);
+ }
+
+ /**
+ * Searches list for given PropertyFilter.
+ */
+ public boolean contains(PropertyFilter<S> propFilter) {
+ if (mPropFilter == propFilter) {
+ return true;
+ }
+ if (mNext == null) {
+ return false;
+ }
+ // Tail recursion.
+ return mNext.contains(propFilter);
+ }
+
+ /**
+ * Prepend a node to the head of the list.
+ */
+ PropertyFilterList<S> prepend(PropertyFilter<S> propFilter) {
+ PropertyFilterList<S> head = new PropertyFilterList<S>(propFilter, this);
+ mPrev = head;
+ return head;
+ }
+
+ static class Builder<S extends Storable>
+ extends Visitor<S, PropertyFilterList<S>, PropertyFilterList<S>>
+ {
+ public PropertyFilterList<S> visit(OrFilter<S> filter, PropertyFilterList<S> list) {
+ // Traverse right-to-left since list must be built in this order.
+ list = filter.getRightFilter().accept(this, list);
+ list = filter.getLeftFilter().accept(this, list);
+ return list;
+ }
+
+ public PropertyFilterList<S> visit(AndFilter<S> filter, PropertyFilterList<S> list) {
+ // Traverse right-to-left since list must be built in this order.
+ list = filter.getRightFilter().accept(this, list);
+ list = filter.getLeftFilter().accept(this, list);
+ return list;
+ }
+
+ public PropertyFilterList<S> visit(PropertyFilter<S> filter, PropertyFilterList<S> list) {
+ return list == null ? new PropertyFilterList<S>(filter, null) : list.prepend(filter);
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Reducer.java b/src/main/java/com/amazon/carbonado/filter/Reducer.java
new file mode 100644
index 0000000..6e2acc1
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Reducer.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Reduces a tree by eliminating redundant 'and' or 'or' terms. As a desired
+ * side-effect, the tree is also unbalanced to the left.
+ *
+ * @author Brian S O'Neill
+ */
+class Reducer<S extends Storable> extends Visitor<S, Filter<S>, Group<S>> {
+ Reducer() {
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param group gathered children
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(OrFilter<S> filter, Group<S> group) {
+ final Group<S> parentGroup = group;
+
+ boolean groupRoot;
+ if (groupRoot = (group == null || group.mForAnd)) {
+ group = new Group<S>(false);
+ }
+
+ filter.getLeftFilter().accept(this, group);
+ filter.getRightFilter().accept(this, group);
+
+ if (groupRoot) {
+ group.reduce();
+ Filter<S> result = group.merge();
+ result.markReduced();
+ if (parentGroup != null) {
+ parentGroup.add(result);
+ }
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param group gathered children
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(AndFilter<S> filter, Group<S> group) {
+ final Group<S> parentGroup = group;
+
+ boolean groupRoot;
+ if (groupRoot = (group == null || !group.mForAnd)) {
+ group = new Group<S>(true);
+ }
+
+ filter.getLeftFilter().accept(this, group);
+ filter.getRightFilter().accept(this, group);
+
+ if (groupRoot) {
+ group.reduce();
+ Filter<S> result = group.merge();
+ result.markReduced();
+ if (parentGroup != null) {
+ parentGroup.add(result);
+ }
+ return result;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param filter candidate node to potentially replace
+ * @param group gathered children
+ * @return original candidate or replacement
+ */
+ @Override
+ public Filter<S> visit(PropertyFilter<S> filter, Group<S> group) {
+ group.add(filter);
+ return null;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/RelOp.java b/src/main/java/com/amazon/carbonado/filter/RelOp.java
new file mode 100644
index 0000000..f74d21e
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/RelOp.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+/**
+ * Relational operator enumeration.
+ *
+ * @author Brian S O'Neill
+ */
+public enum RelOp {
+ /** Equals */
+ EQ,
+ /** Not Equals */
+ NE,
+ /** Less Than */
+ LT,
+ /** Greator than or Equal */
+ GE,
+ /** Greator Than */
+ GT,
+ /** Less than or Equal */
+ LE;
+
+ /**
+ * Returns one of "=", "!=", "<", ">=", ">", or "<=".
+ */
+ public String toString() {
+ switch (this) {
+ case EQ:
+ return "=";
+ case NE:
+ return "!=";
+ case LT:
+ return "<";
+ case GE:
+ return ">=";
+ case GT:
+ return ">";
+ case LE:
+ return "<=";
+ default:
+ return super.toString();
+ }
+ }
+
+ public RelOp reverse() {
+ switch (this) {
+ case EQ: default:
+ return NE;
+ case NE:
+ return EQ;
+ case LT:
+ return GE;
+ case GE:
+ return LT;
+ case GT:
+ return LE;
+ case LE:
+ return GT;
+ }
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/Visitor.java b/src/main/java/com/amazon/carbonado/filter/Visitor.java
new file mode 100644
index 0000000..77ca07f
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/Visitor.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.amazon.carbonado.filter;
+
+import com.amazon.carbonado.Storable;
+
+/**
+ * Traverses a filter tree in its canonical order. By overriding a visit
+ * method, individual nodes can be captured and processed based on their
+ * type. Call super.visit inside the overridden visit method to ensure that the
+ * node's children are properly traversed.
+ *
+ * @author Brian S O'Neill
+ */
+public abstract class Visitor<S extends Storable, R, P> {
+ public R visit(OrFilter<S> filter, P param) {
+ filter.getLeftFilter().accept(this, param);
+ filter.getRightFilter().accept(this, param);
+ return null;
+ }
+
+ public R visit(AndFilter<S> filter, P param) {
+ filter.getLeftFilter().accept(this, param);
+ filter.getRightFilter().accept(this, param);
+ return null;
+ }
+
+ public R visit(PropertyFilter<S> filter, P param) {
+ return null;
+ }
+
+ public R visit(OpenFilter<S> filter, P param) {
+ return null;
+ }
+
+ public R visit(ClosedFilter<S> filter, P param) {
+ return null;
+ }
+}
diff --git a/src/main/java/com/amazon/carbonado/filter/package-info.java b/src/main/java/com/amazon/carbonado/filter/package-info.java
new file mode 100644
index 0000000..b52a049
--- /dev/null
+++ b/src/main/java/com/amazon/carbonado/filter/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2006 Amazon Technologies, Inc. or its affiliates.
+ * Amazon, Amazon.com and Carbonado are trademarks or registered trademarks
+ * of Amazon Technologies, Inc. or its affiliates. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Contains classes for representing query filters.
+ *
+ * @see com.amazon.carbonado.filter.Filter
+ */
+package com.amazon.carbonado.filter;