/* (c) 2014 - 2016 Open Source Geospatial Foundation - all rights reserved
* (c) 2001 - 2013 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.catalog;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.ParametersAreNonnullByDefault;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.util.Converters;
import org.opengis.filter.And;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory;
import org.opengis.filter.MultiValuedFilter.MatchAction;
import org.opengis.filter.Or;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import com.google.common.collect.Lists;
/**
* Static factory method utility to build well known types of {@link Filter} instances.
* <p>
* Although {@code Catalog} client code is allowed to use any (well behaving) {@code Filter}, the
* factory methods in this utility class construct predicate instances of well-known types, in order
* to aid catalog backend implementations in transforming the predicates to their native query
* languages.
* <p>
* The factory methods in this utility also allow for a more compact code by using static imports,
* so that, for example:
*
* <pre>
* <code>
* FilterFactory ff = CommonFactoryFinder.getFilterFactory();
* Filter f1 = ff.equals(ff.propertyName('name'), ff.literal('roads'));
* Filter f2 = ff.equals(ff.propertyName('name'), ff.literal('streams'));
* Filter f3 = ff.equals(ff.propertyName('enabled'), ff.literal(Boolean.TRUE));
* Filter filter = ff.and(ff.or(f1, f2), f3);
* </code>
* </pre>
*
* becomes:
*
* <pre>
* <code>
* Filter filter = and(
* or(equal('name', 'roads'),equal('name', 'streams')),
* equal('enabled', Boolean.TRUE));
* </code>
* </pre>
*/
@ParametersAreNonnullByDefault
public class Predicates {
public static final FilterFactory factory = CommonFactoryFinder.getFilterFactory();
public static final PropertyName ANY_TEXT = factory.property("AnyText");
private Predicates() {
//
}
/**
* @return the "no-filter" predicate.
*/
public static Filter acceptAll() {
return Filter.INCLUDE;
}
/**
* @return the "filter-all" predicate.
*/
public static Filter acceptNone() {
return Filter.EXCLUDE;
}
/**
* Returns a predicate that checks a CatalogInfo object's property for
* {@link Object#equals(Object) equality} with the provided property value.
* <p>
* The <tt>property</tt> parameter may be specified as a "path" of the form "prop1.prop2". If
* any of the resulting properties along the path result in null this method will return null.
* <p>
* Indexed access to nested list and array properties is supported through the syntax
* {@code "prop1[M].prop2.prop3[N]"}, where {@code prop1} and {@code prop3} are list or array
* properties, {@code M} is the index of the {@code prop2} element to retrieve from
* {@code prop1}, and {@code N} is the index of array or list property {@code prop3} to
* retrieve. Indexed access to {{java.util.Set}} properties is <b>not</b> supported.
* <p>
* Evaluation of nested properties for <b>any</b> member of a collection property (including
* Array, List, and Set properties) is supported through the syntax {@code "colProp.name}, which
* will evaluate to the first {@code name} property of the first {@code colProp} property that
* matches the expected value. For example, {@code Filter filter = equal("styles.id", "id1")}
* creates a predicate that evaluates to {@code true} if any style in the set of styles of a
* layer has the given id.
* <p>
* <p>
* If the evaluated object property value and the argument value are not of the same type, the
* returned {@code Predicate} will use the {@link Converters} framework to try to match the two
* property types before performing an {@code Object.equals} check.
* <p>
* Examples:
* <ul>
* <li>Simple: {@code equal("id", "myId");}
* <li>Nested: {@code equal("resource.metadata.someKey", Boolean.TRUE);}
* <li>Any Collection (for List, Array, and Set properties):
* {@code equal("styles.name", "point");}: any style in the styles property whose name is
* "point"
* <li>Indexed (for List and Array properties):
* {@code equal("resource.attributes[1]", myAttribute);}
* <li>Combined: {@code equal("resource.attributes[1].minOccurs", Integer.valueOf(1));}
* </ul>
*
* @param property the qualified property name of the predicate's input object to evaluate
* @param expected the value to check the input object's property against
* @see PropertyIsEqualTo
*
*/
public static Filter equal(final String property, final Object expected) {
return equal(property, expected, MatchAction.ANY);
}
public static Filter equal(final String property, final Object expected,
final MatchAction matchAction) {
final boolean matchCase = true;
return factory.equal(factory.property(property), factory.literal(expected), matchCase,
matchAction);
}
/**
* @return a predicate that evaluates whether the given String {@code property} contains the
* required character string, in a <b>case insensitive</b> manner.
*/
public static Filter contains(final String property, final String subsequence) {
PropertyName propertyName = factory.property(property);
return contains(propertyName, subsequence);
}
public static Filter contains(final PropertyName propertyName, final String subsequence) {
String pattern = "*" + fixSpecials(subsequence) + "*";
String wildcard = "*";
String singleChar = "?";
String escape = "\\";
boolean matchCase = false;
return factory.like(propertyName, pattern, wildcard, singleChar, escape, matchCase);
}
/**
* convienience method to escape any character that is special to the regex system.
*
* @param inString the string to fix
*
* @return the fixed string
*/
private static String fixSpecials(final String inString) {
StringBuffer tmp = new StringBuffer("");
for (int i = 0; i < inString.length(); i++) {
char chr = inString.charAt(i);
if (isSpecial(chr)) {
tmp.append("\\" + chr);
} else {
tmp.append(chr);
}
}
return tmp.toString();
}
/**
* convienience method to determine if a character is special to the regex system.
*
* @param chr the character to test
*
* @return is the character a special character.
*/
private static boolean isSpecial(final char chr) {
return ((chr == '.') || (chr == '?') || (chr == '*') || (chr == '^') || (chr == '$')
|| (chr == '+') || (chr == '[') || (chr == ']') || (chr == '(') || (chr == ')')
|| (chr == '|') || (chr == '\\') || (chr == '&') || (chr == '}') || (chr == '{'));
}
public static Filter fullTextSearch(final String subsequence) {
return contains(ANY_TEXT, subsequence);
}
/**
* Returns a predicate that evaluates to {@code true} if each of its components evaluates to
* {@code true}.
* <p>
* The components are evaluated in order, and evaluation will be "short-circuited" as soon as a
* false predicate is found.
*
*/
public static Filter and(Filter op1, Filter op2) {
List<Filter> children = new ArrayList<Filter>();
if (op1 instanceof And) {
children.addAll(((And) op1).getChildren());
} else {
children.add(op1);
}
if (op2 instanceof And) {
children.addAll(((And) op2).getChildren());
} else {
children.add(op2);
}
return factory.and(children);
}
/**
* Returns a predicate that evaluates to {@code true} if each of its components evaluates to
* {@code true}.
* <p>
* The components are evaluated in order, and evaluation will be "short-circuited" as soon as a
* false predicate is found.
*
*/
public static Filter and(Filter... operands) {
List<Filter> anded = Lists.newArrayList(operands);
return factory.and(anded);
}
/**
* Returns a predicate that evaluates to {@code true} if each of its components evaluates to
* {@code true}.
* <p>
* The components are evaluated in order, and evaluation will be "short-circuited" as soon as a
* false predicate is found.
*
*/
public static Filter and(List<Filter> operands) {
if(operands.size() == 0) {
return Filter.INCLUDE;
} else if (operands.size() == 1) {
return operands.get(0);
} else {
return factory.and(operands);
}
}
/**
* Returns a predicate that evaluates to {@code true} if either of its components evaluates to
* {@code true}.
* <p>
* The components are evaluated in order, and evaluation will be "short-circuited" as soon as a
* true predicate is found.
*/
public static Filter or(Filter op1, Filter op2) {
List<Filter> children = new ArrayList<Filter>();
if (op1 instanceof Or) {
children.addAll(((Or) op1).getChildren());
} else {
children.add(op1);
}
if (op2 instanceof Or) {
children.addAll(((Or) op2).getChildren());
} else {
children.add(op2);
}
return factory.or(children);
}
public static Filter or(Filter... operands) {
List<Filter> ored = Lists.newArrayList(operands);
return factory.or(ored);
}
public static Filter or(List<Filter> operands) {
if(operands.size() == 0) {
return Filter.EXCLUDE;
} else if (operands.size() == 1) {
return operands.get(0);
} else {
return factory.or(operands);
}
}
public static Filter isNull(final String propertyName) {
return factory.isNull(factory.property(propertyName));
}
public static SortBy asc(final String propertyName) {
return sortBy(propertyName, true);
}
public static SortBy desc(final String propertyName) {
return sortBy(propertyName, false);
}
public static SortBy sortBy(final String propertyName, final boolean ascending) {
return factory.sort(propertyName, ascending ? SortOrder.ASCENDING : SortOrder.DESCENDING);
}
public static Filter isInstanceOf(Class clazz){
return factory.equals(factory.function("isInstanceOf", factory.literal(clazz)), factory.literal(true));
}
/**
* Returns a predicate that checks a CatalogInfo object's property for inequality with the
* provided property value.
* <p>
* The <tt>property</tt> parameter may be specified as a "path" of the form "prop1.prop2". If
* any of the resulting properties along the path result in null this method will return null.
* <p>
* Indexed access to nested list and array properties is supported through the syntax
* {@code "prop1[M].prop2.prop3[N]"}, where {@code prop1} and {@code prop3} are list or array
* properties, {@code M} is the index of the {@code prop2} element to retrieve from
* {@code prop1}, and {@code N} is the index of array or list property {@code prop3} to
* retrieve. Indexed access to {{java.util.Set}} properties is <b>not</b> supported.
* <p>
* Evaluation of nested properties for <b>any</b> member of a collection property is at the
* moment not supported
* <p>
*
* @param property the qualified property name of the predicate's input object to evaluate
* @param expected the value to check the input object's property against
* @see PropertyIsEqualTo
*
*/
public static Filter notEqual(final String property, final Object expected) {
return factory.notEqual(factory.property(property), factory.literal(expected));
}
}