diff options
| author | Brian S. O'Neill <bronee@gmail.com> | 2006-08-30 01:56:35 +0000 | 
|---|---|---|
| committer | Brian S. O'Neill <bronee@gmail.com> | 2006-08-30 01:56:35 +0000 | 
| commit | 1205ea08cc9e53de40b71447a79daf378cb0a205 (patch) | |
| tree | b86d9fa0efa8a1276104354af513ef533db52277 /src/main/java/com/amazon/carbonado | |
| parent | ab635e23ad5fb9cd9edb61665a3654ee3e4b372a (diff) | |
Add filter implementation
Diffstat (limited to 'src/main/java/com/amazon/carbonado')
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           = "=" | "!=" | "<" | ">=" | ">" | "<="
 + * 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;
  | 
