/*
* ====================
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of the Common Development
* and Distribution License("CDDL") (the "License"). You may not use this file
* except in compliance with the License.
*
* You can obtain a copy of the License at
* http://opensource.org/licenses/cddl1.php
* See the License for the specific language governing permissions and limitations
* under the License.
*
* When distributing the Covered Code, include this CDDL Header Notice in each file
* and include the License file at http://opensource.org/licenses/cddl1.php.
* If applicable, add the following below this CDDL Header, with the fields
* enclosed by brackets [] replaced by your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
* ====================
*/
package org.identityconnectors.framework.common.objects.filter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Base class to make it easier to implement Search. A search filter may contain
* operators (such as 'contains' or 'in') or may contain logical operators (such
* as 'AND', 'OR' or 'NOT') that a connector cannot implement using the native
* API of the target system or application. A connector developer should
* subclass <code>AbstractFilterTranslator</code> in order to declare which
* filter operations the connector does support. This allows the
* <code>FilterTranslator</code> instance to analyze a specified search filter
* and reduce the filter to its most efficient form. The default (and
* worst-case) behavior is to return a null expression, which means that the
* connector should return "everything" (that is, should return all values for
* every requested attribute) and rely on the common code in the framework to
* perform filtering. This "fallback" behavior is good (in that it ensures
* consistency of search behavior across connector implementations) but it is
* obviously better for performance and scalability if each connector performs
* as much filtering as the native API of the target can support.
* <p>
* A subclass should override each of the following methods where possible:
* <ol>
* <li>{@link #createAndExpression}</li>
* <li>{@link #createOrExpression}</li>
* <li>{@link #createContainsExpression(ContainsFilter, boolean)}</li>
* <li>{@link #createEndsWithExpression(EndsWithFilter, boolean)}</li>
* <li>{@link #createEqualsExpression(EqualsFilter, boolean)}</li>
* <li>{@link #createGreaterThanExpression(GreaterThanFilter, boolean)}</li>
* <li>
* {@link #createGreaterThanOrEqualExpression(GreaterThanOrEqualFilter, boolean)}
* </li>
* <li>{@link #createStartsWithExpression(StartsWithFilter, boolean)}</li>
* <li>
* {@link #createContainsAllValuesExpression(ContainsAllValuesFilter, boolean)}</li>
* </ol>
* <p>
* Translation can then be performed using {@link #translate(Filter)}.
* <p>
*
* @param <T>
* The result type of the translator. Commonly this will be a string,
* but there are cases where you might need to return a more complex
* data structure. For example if you are building a SQL query, you
* will need not *just* the base WHERE clause but a list of tables
* that need to be joined together.
*/
abstract public class AbstractFilterTranslator<T> implements FilterTranslator<T> {
/**
* Main method to be called to translate a filter
*
* @param filter
* The filter to translate.
* @return The list of queries to be performed. The list <code>size()</code>
* may be one of the following:
* <ol>
* <li>0 - This signifies <b>fetch everything</b>. This may occur if
* your filter was null or one of your <code>create*</code> methods
* returned null.</li>
* <li>1 - List contains a single query that will return the results
* from the filter. Note that the results may be a <b>superset</b>
* of those specified by the filter in the case that one of your
* <code>create*</code> methods returned null. That is OK from a
* behavior standpoint since <code>ConnectorFacade</code> performs a
* second level of filtering. However it is undesirable from a
* performance standpoint.</li>
* <li>>1 - List contains multiple queries that must be performed in
* order to meet the filter that was passed in. Note that this only
* occurs if your {@link #createOrExpression} method can return
* null. If this happens, it is the responsibility of the connector
* implementor to perform each query and combine the results. In
* order to eliminate duplicates, the connector implementation must
* keep an in-memory <code>HashSet</code> of those UID that have
* been visited thus far. This will not scale well if your result
* sets are large. Therefore it is <b>recommended</b> that if at all
* possible you implement {@link #createOrExpression}</li>
* </ol>
*/
public final List<T> translate(Filter filter) {
if (filter == null) {
return new ArrayList<T>();
}
// this must come first
filter = eliminateExternallyChainedFilters(filter);
filter = normalizeNot(filter);
filter = simplifyAndDistribute(filter);
// might have simplified it to the everything filter
if (filter == null) {
return new ArrayList<T>();
}
List<T> result = translateInternal(filter);
// now "optimize" - we can eliminate exact matches at least
Set<T> set = new HashSet<T>();
List<T> optimized = new ArrayList<T>(result.size());
for (T obj : result) {
if (set.add(obj)) {
optimized.add(obj);
}
}
return optimized;
}
private Filter eliminateExternallyChainedFilters(Filter filter) {
while (filter instanceof ExternallyChainedFilter) {
filter = ((ExternallyChainedFilter) filter).getFilter();
}
return filter;
}
/**
* Pushes Not's so that they are just before the leaves of the tree
*/
private Filter normalizeNot(Filter filter) {
if (filter instanceof AndFilter) {
AndFilter af = (AndFilter) filter;
return new AndFilter(normalizeNot(af.getLeft()), normalizeNot(af.getRight()));
} else if (filter instanceof OrFilter) {
OrFilter of = (OrFilter) filter;
return new OrFilter(normalizeNot(of.getLeft()), normalizeNot(of.getRight()));
} else if (filter instanceof NotFilter) {
NotFilter nf = (NotFilter) filter;
return negate(normalizeNot(nf.getFilter()));
} else {
return filter;
}
}
/**
* Given a filter, create a filter representing its negative. This is used
* by normalizeNot.
*/
private Filter negate(Filter filter) {
if (filter instanceof AndFilter) {
AndFilter af = (AndFilter) filter;
return new OrFilter(negate(af.getLeft()), negate(af.getRight()));
} else if (filter instanceof OrFilter) {
OrFilter of = (OrFilter) filter;
return new AndFilter(negate(of.getLeft()), negate(of.getRight()));
} else if (filter instanceof NotFilter) {
NotFilter nf = (NotFilter) filter;
return nf.getFilter();
} else {
return new NotFilter(filter);
}
}
/**
* Simultaneously prunes those portions of the filter than cannot be
* implemented and distributes Ands over Ors where needed if the resource
* does not implement Or.
*
* @param filter
* Nots must already be normalized
* @return a simplified filter or null to represent the "everything" filter.
*/
private Filter simplifyAndDistribute(Filter filter) {
if (filter instanceof AndFilter) {
AndFilter af = (AndFilter) filter;
Filter simplifiedLeft = simplifyAndDistribute(af.getLeft());
Filter simplifiedRight = simplifyAndDistribute(af.getRight());
if (simplifiedLeft == null) {
// left is "everything" - just return the right
return simplifiedRight;
} else if (simplifiedRight == null) {
// right is "everything" - just return the left
return simplifiedLeft;
} else {
// simulate translation of the left and right
// to see where we end up
List<T> leftExprs = translateInternal(simplifiedLeft);
List<T> rightExprs = translateInternal(simplifiedRight);
if (leftExprs.isEmpty()) {
// This can happen only when one of the create* methods
// is inconsistent from one invocation to the next
// (simplifiedLeft should have been null
// in the previous 'if' above).
throw new IllegalStateException("Translation method is inconsistent: "
+ leftExprs);
}
if (rightExprs.isEmpty()) {
// This can happen only when one of the create* methods
// is inconsistent from one invocation to the next
// (simplifiedRight should have been null
// in the previous 'if' above).
throw new IllegalStateException("Translation method is inconsistent: "
+ rightExprs);
}
// Simulate ANDing each pair(left,right).
// If all of them return null (i.e., "everything"),
// then the request cannot be filtered.
boolean anyAndsPossible = false;
for (T leftExpr : leftExprs) {
for (T rightExpr : rightExprs) {
T test = createAndExpression(leftExpr, rightExpr);
if (test != null) {
anyAndsPossible = true;
break;
}
}
if (anyAndsPossible) {
break;
}
}
// If no AND filtering is possible,
// return whichever of left or right
// contains the fewest expressions.
if (!anyAndsPossible) {
if (leftExprs.size() <= rightExprs.size()) {
return simplifiedLeft;
} else {
return simplifiedRight;
}
}
// Since AND filtering is possible for at least
// one expression, let's distribute.
if (leftExprs.size() > 1) {
// The left can contain more than one expression
// only if the left-hand side is an unimplemented OR.
// Distribute our AND to the left.
OrFilter left = (OrFilter) simplifiedLeft;
OrFilter newFilter =
new OrFilter(new AndFilter(left.getLeft(), simplifiedRight),
new AndFilter(left.getRight(), simplifiedRight));
return simplifyAndDistribute(newFilter);
} else if (rightExprs.size() > 1) {
// The right can contain more than one expression
// only if the right-hand side is an unimplemented OR.
// Distribute our AND to the right.
OrFilter right = (OrFilter) simplifiedRight;
OrFilter newFilter =
new OrFilter(new AndFilter(simplifiedLeft, right.getLeft()),
new AndFilter(simplifiedLeft, right.getRight()));
return simplifyAndDistribute(newFilter);
} else {
// Each side contains exactly one expression
// and the translator does implement AND
// (anyAndsPossible must be true
// for them to have hit this branch).
assert anyAndsPossible;
return new AndFilter(simplifiedLeft, simplifiedRight);
}
}
} else if (filter instanceof OrFilter) {
OrFilter of = (OrFilter) filter;
Filter simplifiedLeft = simplifyAndDistribute(of.getLeft());
Filter simplifiedRight = simplifyAndDistribute(of.getRight());
// If either left or right reduces to "everything",
// then simplify the OR to "everything".
if (simplifiedLeft == null || simplifiedRight == null) {
return null;
}
// otherwise
return new OrFilter(simplifiedLeft, simplifiedRight);
} else {
// Otherwise, it's a NOT(LEAF) or a LEAF.
// Simulate creating it.
T expr = createLeafExpression(filter);
if (expr == null) {
// If the expression cannot be implemented,
// return the "everything" filter.
return null;
} else {
// Otherwise, return the filter.
return filter;
}
}
}
/**
* Translates the filter into a list of expressions. The filter must have
* already been transformed using normalizeNot followed by a
* simplifyAndDistribute.
*
* @param filter
* A filter (normalized, simplified, and distibuted)
* @return A list of expressions or empty list for everything.
*/
private List<T> translateInternal(Filter filter) {
if (filter instanceof AndFilter) {
T result = translateAnd((AndFilter) filter);
List<T> rv = new ArrayList<T>();
if (result != null) {
rv.add(result);
}
return rv;
} else if (filter instanceof OrFilter) {
return translateOr((OrFilter) filter);
} else {
// otherwise it's either a leaf or a NOT (leaf)
T expr = createLeafExpression(filter);
List<T> exprs = new ArrayList<T>();
if (expr != null) {
exprs.add(expr);
}
return exprs;
}
}
private T translateAnd(AndFilter filter) {
List<T> leftExprs = translateInternal(filter.getLeft());
List<T> rightExprs = translateInternal(filter.getRight());
if (leftExprs.size() != 1) {
// this can happen only if one of the create* methods
// is inconsistent from one invocation to the next
// (at this point we've already been simplified and
// distributed).
throw new IllegalStateException("Translation method is inconsistent: " + leftExprs);
}
if (rightExprs.size() != 1) {
// this can happen only if one of the create* methods
// is inconsistent from one invocation to the next
// (at this point we've already been simplified and
// distributed).
throw new IllegalStateException("Translation method is inconsistent: " + rightExprs);
}
T rv = createAndExpression(leftExprs.get(0), rightExprs.get(0));
if (rv == null) {
// This could happen only if we're inconsistent
// (since the simplify logic already should have removed
// any expression that cannot be filtered).
throw new IllegalStateException("createAndExpression is inconsistent");
}
return rv;
}
private List<T> translateOr(OrFilter filter) {
List<T> leftExprs = translateInternal(filter.getLeft());
List<T> rightExprs = translateInternal(filter.getRight());
if (leftExprs.isEmpty()) {
// This can happen only if one of the create* methods
// is inconsistent from one invocation to the next.
throw new IllegalStateException("Translation method is inconsistent");
}
if (rightExprs.isEmpty()) {
// This can happen only if one of the create* methods
// methods is inconsistent from on invocation to the next.
throw new IllegalStateException("Translation method is inconsistent");
}
if (leftExprs.size() == 1 && rightExprs.size() == 1) {
// If each side contains exactly one expression,
// try to create a combined expression.
T val = createOrExpression(leftExprs.get(0), rightExprs.get(0));
if (val != null) {
List<T> rv = new ArrayList<T>();
rv.add(val);
return rv;
}
// Otherwise, fall through
}
// Return a list of queries from the left and from the right
List<T> rv = new ArrayList<T>(leftExprs.size() + rightExprs.size());
rv.addAll(leftExprs);
rv.addAll(rightExprs);
return rv;
}
/**
* Creates an expression for a LEAF or a NOT(leaf)
*
* @param filter
* Must be either a leaf or a NOT(leaf)
* @return The expression
*/
private T createLeafExpression(Filter filter) {
Filter leafFilter;
boolean not;
if (filter instanceof NotFilter) {
NotFilter nf = (NotFilter) filter;
leafFilter = nf.getFilter();
not = true;
} else {
leafFilter = filter;
not = false;
}
T expr = createLeafExpression(leafFilter, not);
return expr;
}
/**
* Creates a Leaf expression
*
* @param filter
* Must be a leaf expression
* @param not
* Is ! to be applied to the leaf expression
* @return The expression or null (for everything)
*/
private T createLeafExpression(Filter filter, boolean not) {
if (filter instanceof ContainsFilter) {
return createContainsExpression((ContainsFilter) filter, not);
} else if (filter instanceof EndsWithFilter) {
return createEndsWithExpression((EndsWithFilter) filter, not);
} else if (filter instanceof EqualsFilter) {
return createEqualsExpression((EqualsFilter) filter, not);
} else if (filter instanceof GreaterThanFilter) {
return createGreaterThanExpression((GreaterThanFilter) filter, not);
} else if (filter instanceof GreaterThanOrEqualFilter) {
return createGreaterThanOrEqualExpression((GreaterThanOrEqualFilter) filter, not);
} else if (filter instanceof LessThanFilter) {
return createLessThanExpression((LessThanFilter) filter, not);
} else if (filter instanceof LessThanOrEqualFilter) {
return createLessThanOrEqualExpression((LessThanOrEqualFilter) filter, not);
} else if (filter instanceof StartsWithFilter) {
return createStartsWithExpression((StartsWithFilter) filter, not);
} else if (filter instanceof ContainsAllValuesFilter) {
return createContainsAllValuesExpression((ContainsAllValuesFilter) filter, not);
} else {
// unrecognized expression - nothing we can do
return null;
}
}
/**
* Should be overridden by subclasses to create an AND expression if the
* native resource supports AND.
*
* @param leftExpression
* The left expression. Will never be null.
* @param rightExpression
* The right expression. Will never be null.
* @return The AND expression. A return value of null means a native AND
* query cannot be created for the given expressions. In this case,
* the resulting query will consist of the leftExpression only.
*/
protected T createAndExpression(T leftExpression, T rightExpression) {
return null;
}
/**
* Should be overridden by subclasses to create an OR expression if the
* native resource supports OR.
*
* @param leftExpression
* The left expression. Will never be null.
* @param rightExpression
* The right expression. Will never be null.
* @return The OR expression. A return value of null means a native OR query
* cannot be created for the given expressions. In this case,
* {@link #translate} may return multiple queries, each of which
* must be run and results combined.
*/
protected T createOrExpression(T leftExpression, T rightExpression) {
return null;
}
/**
* Should be overridden by subclasses to create a CONTAINS expression if the
* native resource supports CONTAINS.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT CONTAINS
* @return The CONTAINS expression. A return value of null means a native
* CONTAINS query cannot be created for the given filter. In this
* case, {@link #translate} may return an empty query set, meaning
* fetch <b>everything</b>. The filter will be re-applied in memory
* to the resulting object stream. This does not scale well, so if
* possible, you should implement this method.
*/
protected T createContainsExpression(ContainsFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a ENDS-WITH expression if
* the native resource supports ENDS-WITH.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT ENDS-WITH
* @return The ENDS-WITH expression. A return value of null means a native
* ENDS-WITH query cannot be created for the given filter. In this
* case, {@link #translate} may return an empty query set, meaning
* fetch <b>everything</b>. The filter will be re-applied in memory
* to the resulting object stream. This does not scale well, so if
* possible, you should implement this method.
*/
protected T createEndsWithExpression(EndsWithFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a EQUALS expression if the
* native resource supports EQUALS.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT EQUALS
* @return The EQUALS expression. A return value of null means a native
* EQUALS query cannot be created for the given filter. In this
* case, {@link #translate} may return an empty query set, meaning
* fetch <b>everything</b>. The filter will be re-applied in memory
* to the resulting object stream. This does not scale well, so if
* possible, you should implement this method.
*/
protected T createEqualsExpression(EqualsFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a GREATER-THAN expression if
* the native resource supports GREATER-THAN.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT GREATER-THAN
* @return The GREATER-THAN expression. A return value of null means a
* native GREATER-THAN query cannot be created for the given filter.
* In this case, {@link #translate} may return an empty query set,
* meaning fetch <b>everything</b>. The filter will be re-applied in
* memory to the resulting object stream. This does not scale well,
* so if possible, you should implement this method.
*/
protected T createGreaterThanExpression(GreaterThanFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a GREATER-THAN-EQUAL
* expression if the native resource supports GREATER-THAN-EQUAL.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT GREATER-THAN-EQUAL
* @return The GREATER-THAN-EQUAL expression. A return value of null means a
* native GREATER-THAN-EQUAL query cannot be created for the given
* filter. In this case, {@link #translate} may return an empty
* query set, meaning fetch <b>everything</b>. The filter will be
* re-applied in memory to the resulting object stream. This does
* not scale well, so if possible, you should implement this method.
*/
protected T createGreaterThanOrEqualExpression(GreaterThanOrEqualFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a LESS-THAN expression if
* the native resource supports LESS-THAN.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT LESS-THAN
* @return The LESS-THAN expression. A return value of null means a native
* LESS-THAN query cannot be created for the given filter. In this
* case, {@link #translate} may return an empty query set, meaning
* fetch <b>everything</b>. The filter will be re-applied in memory
* to the resulting object stream. This does not scale well, so if
* possible, you should implement this method.
*/
protected T createLessThanExpression(LessThanFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a LESS-THAN-EQUAL expression
* if the native resource supports LESS-THAN-EQUAL.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT LESS-THAN-EQUAL
* @return The LESS-THAN-EQUAL expression. A return value of null means a
* native LESS-THAN-EQUAL query cannot be created for the given
* filter. In this case, {@link #translate} may return an empty
* query set, meaning fetch <b>everything</b>. The filter will be
* re-applied in memory to the resulting object stream. This does
* not scale well, so if possible, you should implement this method.
*/
protected T createLessThanOrEqualExpression(LessThanOrEqualFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a STARTS-WITH expression if
* the native resource supports STARTS-WITH.
*
* @param filter
* The contains filter. Will never be null.
* @param not
* True if this should be a NOT STARTS-WITH
* @return The STARTS-WITH expression. A return value of null means a native
* STARTS-WITH query cannot be created for the given filter. In this
* case, {@link #translate} may return an empty query set, meaning
* fetch <b>everything</b>. The filter will be re-applied in memory
* to the resulting object stream. This does not scale well, so if
* possible, you should implement this method.
*/
protected T createStartsWithExpression(StartsWithFilter filter, boolean not) {
return null;
}
/**
* Should be overridden by subclasses to create a CONTAINS-ALL-VALUES
* expression if the native resource supports a contains all values.
*
* @param filter
* The contains all filter. Will never be null.
* @param not
* True if this should be a NOT CONTAINS-ALL-VALUES.
* @return The CONTAINS-ALL-VALUES expression. A return value of null means
* a native CONTAINS-ALL-VALUES query cannot be created for the
* given filter. In this case, {@link #translate} may return an
* empty query set, meaning fetch <b>everything</b>. The filter will
* be re-applied in memory to the resulting object stream. This does
* not scale well, so if possible, you should implement this method.
*/
protected T createContainsAllValuesExpression(ContainsAllValuesFilter filter, boolean not) {
return null;
}
}