/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.filter;
import java.awt.Color;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.filter.visitor.DuplicatingFilterVisitor;
import org.opengis.filter.And;
import org.opengis.filter.ExcludeFilter;
import org.opengis.filter.Filter;
import org.opengis.filter.FilterFactory2;
import org.opengis.filter.Id;
import org.opengis.filter.IncludeFilter;
import org.opengis.filter.Not;
import org.opengis.filter.Or;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsEqualTo;
import org.opengis.filter.PropertyIsGreaterThan;
import org.opengis.filter.PropertyIsGreaterThanOrEqualTo;
import org.opengis.filter.PropertyIsLessThan;
import org.opengis.filter.PropertyIsLessThanOrEqualTo;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNotEqualTo;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Beyond;
import org.opengis.filter.spatial.Contains;
import org.opengis.filter.spatial.Crosses;
import org.opengis.filter.spatial.DWithin;
import org.opengis.filter.spatial.Disjoint;
import org.opengis.filter.spatial.Equals;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Overlaps;
import org.opengis.filter.spatial.Touches;
import org.opengis.filter.spatial.Within;
/**
* Utility class for working with Filters & Expression.
* <p>
* To get the full benifit you will need to create an instanceof
* this Object (supports your own custom FilterFactory!). Additional
* methods to help create expressions are available.
* </p>
* <p>
* Example use:
* <pre><code>
* Filters filters = new Filters( factory );
* filters.duplicate( origional );
* </code></pre>
* The above example creates a copy of the provided Filter,
* the factory provided will be used when creating the duplicated
* content.
* </p>
* <h3>Expression</h3>
* <p>
* Expressions form an interesting little semi scripting languge,
* intended for queries. A interesting Feature of Filter as a language
* is that it is not strongly typed. This utility class many helper
* methods that ease the transition from Strongly typed Java to the more
* relaxed setting of Expression where most everything can be a string.
* </p>
* <pre><code>
* double sum = Filters.number( Object ) + Filters.number( Object );
* </code></pre>
* The above example will support the conversion of many things into a format
* suitable for addition - the complete list is something like:
* <ul>
* <li>Any instance of Number
* <li>"1234" - aka Integer
* <li>"#FFF" - aka Integer
* <li>"123.0" - aka Double
* </ul>
* A few things (like Geometry and "ABC") will not be considered addative.
* </p>
* In general the scope of these functions should be similar to that
* allowed by the XML Atomic Types, aka those that can be seperated by
* whitespace to form a list.
* </p>
* <p>
* We do our best to be forgiving, any Java class which takes a String as
* a constructor can be tried, and toString() assumed to be the inverse. This
* lets many things (like URL and Date) function without modification.
* </p>
*
* @author Jody Garnett, Refractions Research
* @since GeoTools 2.2.M3
* @source $URL$
*/
public class Filters {
/** <code>NOTFOUND</code> indicates int value was unavailable */
public static final int NOTFOUND = -1;
/**
* Set to true to start throwing exceptions when org.geotools.filter.Filter is used.
*/
private static final boolean STRICT = false;
org.opengis.filter.FilterFactory2 ff;
public Filters(){
this( CommonFactoryFinder.getFilterFactory2(null) );
}
public Filters( org.opengis.filter.FilterFactory2 factory ){
ff = factory;
}
public void setFilterFactory( org.opengis.filter.FilterFactory2 factory ){
ff = factory;
}
/**
* Safe version of FilterFactory *and* that is willing to combine
* filter1 and filter2 correctly in the even either of them is already
* an And filter.
*
* @param ff
* @param filter1
* @param filter2
* @return And
*/
public static Filter and( org.opengis.filter.FilterFactory ff, Filter filter1, Filter filter2 ){
ArrayList<Filter> list = new ArrayList<Filter>(2);
if( filter1 == null ){
// ignore
}
else if( filter1 instanceof And){
And some = (And) filter1;
list.addAll( some.getChildren() );
}
else {
list.add( filter1 );
}
if( filter2 == null ){
// ignore
}
else if( filter2 instanceof And){
And more = (And) filter2;
list.addAll( more.getChildren() );
}
else {
list.add( filter2 );
}
if( list.size() == 0 ){
return Filter.EXCLUDE;
}
else if( list.size() == 1 ){
return list.get(0);
}
else {
return ff.and( list );
}
}
/**
* Safe version of FilterFactory *or* that is willing to combine
* filter1 and filter2 correctly in the even either of them is already
* an Or filter.
*
* @param ff
* @param filter1
* @param filter2
* @return
*/
public static Filter or( org.opengis.filter.FilterFactory ff, Filter filter1, Filter filter2 ){
ArrayList<Filter> list = new ArrayList<Filter>();
if( filter1 == null ){
// ignore
}
else if( filter1 instanceof Or){
Or some = (Or) filter1;
list.addAll( some.getChildren() );
}
else {
list.add( filter1 );
}
if( filter2 == null){
// ignore
}
else if( filter2 instanceof Or){
Or more = (Or) filter2;
list.addAll( more.getChildren() );
}
else {
list.add( filter2 );
}
if( list.size() == 0 ){
return Filter.EXCLUDE;
}
else if( list.size() == 1 ){
return list.get(0);
}
else {
return ff.or( list );
}
}
/**
* Safely visit the provided filter.
* <p>
* This method handles the case of:
* <ul>
* <li>Filter.INCLUDES: will call FilterVisitor2 method if available
* <li>Filter.EXCLUDES: will call FilterVisitor2 method if available
* <li>org.geotools.filter.Filter: will visit
* </ul>
* Please note that when called with a strict *org.opengis.filter.Filter* this
* method will fail with a ClassCastException
*
* @param filter
* @param visitor
* @deprecated Please update your code to a org.opengis.filter.FilterVisitor
*/
public static void accept( org.opengis.filter.Filter filter, FilterVisitor visitor ){
if( filter == Filter.EXCLUDE ){
if( visitor instanceof FilterVisitor2 ){
((FilterVisitor2)visitor).visit( (ExcludeFilter) Filter.EXCLUDE );
}
return;
}
else if( filter == Filter.INCLUDE ){
if( visitor instanceof FilterVisitor2 ){
((FilterVisitor2)visitor).visit( (IncludeFilter) Filter.INCLUDE);
}
return;
}
if( filter instanceof org.geotools.filter.Filter ){
((org.geotools.filter.Filter) filter).accept( visitor );
}
else {
if( STRICT ){
// don't even try ..
throw new ClassCastException("Please update your code to a org.opengis.filter.FilterVisitor");
}
// Copy the provided filter into the old org.geotools.filter.Filter api
FilterFactory2 ff = CommonFactoryFinder.getFilterFactory2(null);
DuplicatingFilterVisitor xerox = new DuplicatingFilterVisitor( ff );
org.geotools.filter.Filter copy = (org.geotools.filter.Filter) filter.accept( xerox, ff );
// Visit the resulting copy
copy.accept(visitor);
}
}
/**
* Deep copy the filter.
* <p>
* Filter objects are mutable, when copying a rich
* data structure (like SLD) you will need to duplicate
* the Filters referenced therein.
* </p>
*/
public Filter duplicate( Filter filter ){
DuplicatingFilterVisitor xerox = new DuplicatingFilterVisitor( ff );
Filter copy = (Filter) filter.accept( xerox, ff );
return copy;
}
/**
* Utility method used to transition to geoapi filter.
* <p>
* This utility method is designed to help people port their
* code quickly, an instanceof check is much preferred.
* </p>
* Example:<pre><code>
* BEFORE: filter.getFilterType() == FilterType.GEOMETRY_CONTAINS
* QUICK: Filters.getFilterType( filter ) == FilterType.GEOMETRY_CONTAINS
* AFTER: filter instanceof Contains
* </code></pre>
* @param filter
* @deprecated please use instanceof checks
*/
public static short getFilterType( org.opengis.filter.Filter filter ){
if( filter == org.opengis.filter.Filter.EXCLUDE ) return FilterType.ALL;
if( filter == org.opengis.filter.Filter.INCLUDE ) return FilterType.NONE;
if( filter instanceof org.geotools.filter.Filter){
return ((org.geotools.filter.Filter)filter).getFilterType();
}
if( filter instanceof PropertyIsBetween ) return FilterType.BETWEEN;
if( filter instanceof PropertyIsEqualTo ) return FilterType.COMPARE_EQUALS;
if( filter instanceof PropertyIsGreaterThan ) return FilterType.COMPARE_GREATER_THAN;
if( filter instanceof PropertyIsGreaterThanOrEqualTo ) return FilterType.COMPARE_GREATER_THAN_EQUAL;
if( filter instanceof PropertyIsLessThan) return FilterType.COMPARE_LESS_THAN;
if( filter instanceof PropertyIsLessThanOrEqualTo ) return FilterType.COMPARE_LESS_THAN_EQUAL;
if( filter instanceof PropertyIsNotEqualTo ) return FilterType.COMPARE_NOT_EQUALS;
if( filter instanceof Id ) return FilterType.FID;
if( filter instanceof BBOX ) return FilterType.GEOMETRY_BBOX;
if( filter instanceof Beyond) return FilterType.GEOMETRY_BEYOND;
if( filter instanceof Contains ) return FilterType.GEOMETRY_CONTAINS;
if( filter instanceof Crosses ) return FilterType.GEOMETRY_CROSSES;
if( filter instanceof Disjoint ) return FilterType.GEOMETRY_DISJOINT;
if( filter instanceof DWithin) return FilterType.GEOMETRY_DWITHIN;
if( filter instanceof Equals) return FilterType.GEOMETRY_EQUALS;
if( filter instanceof Intersects) return FilterType.GEOMETRY_INTERSECTS;
if( filter instanceof Overlaps) return FilterType.GEOMETRY_OVERLAPS;
if( filter instanceof Touches) return FilterType.GEOMETRY_TOUCHES;
if( filter instanceof Within) return FilterType.GEOMETRY_WITHIN;
if( filter instanceof PropertyIsLike) return FilterType.LIKE;
if( filter instanceof And) return FilterType.LOGIC_AND;
if( filter instanceof Not) return FilterType.LOGIC_NOT;
if( filter instanceof Or ) return FilterType.LOGIC_OR;
if( filter instanceof PropertyIsNull) return FilterType.NULL;
if( filter instanceof Filter){
return 0;
}
return 0;
}
/**
* Obtain the provided Expression as an integer.
* <p>
* This method is quickly used to safely check Literal expressions.
*
* @param expr
* @return int value of first Number, or NOTFOUND
*/
public static int asInt( Expression expr ) {
if( expr == null ) return NOTFOUND;
try {
Integer number = expr.evaluate( null, Integer.class );
if( number == null ){
return NOTFOUND;
}
return number;
}
catch( NullPointerException npe ){
return NOTFOUND; // well that was not unexpected
}
/*
Number number = (Number) asType(expr, Number.class);
if (number != null) {
return number.intValue();
}
//look for a string
String string = (String) asType(expr,String.class);
if (string != null) {
//try parsing into a integer
try {
return Integer.parseInt(string);
}
catch(NumberFormatException e) {}
}
//no dice
return NOTFOUND;
*/
}
/**
* Obtain the provided Expression as a String.
* <p>
* This method only reliably works when the Expression is a Literal.
*
* @param expr
*
* @return Expression as a String, or null
*/
public static String asString(Expression expr) {
if( expr == null ) return null;
try {
return expr.evaluate( null, String.class );
}
catch( NullPointerException npe){
// must be a more complicated expression than a literal
return null;
}
}
/**
* Obtain the provided Expression as a double.
* @param expr
* @return int value of first Number, or Double.NaN
*/
public static double asDouble(Expression expr) {
if( expr == null ) {
return Double.NaN;
}
try {
Double number = expr.evaluate(null, Double.class );
if( number == null ) {
return Double.NaN;
}
return number.doubleValue();
}
catch( NullPointerException npe){
// must be a more complicated expression than a literal
return Double.NaN;
}
}
/**
* Navigate through the expression searching for something that can be a TYPE.
* <p>
* This will work even with dynamic expression that would normally require a
* feature. It works especially well when the Expression is a Literal
* literal (which is usually the case).
* </p>
*
* <p>
* If you have a specific Feature, please do this:
* <pre><code>
* Object value = expr.getValue( feature );
* return value instanceof Color ? (Color) value : null;
* </code></pre>
* </p>
*
* @param expr This only really works for down casting literals to a value
* @param Target type
*
* @return expr smunched into indicated type
* @deprecated This is not a good idea; use expr.evaulate( null, TYPE )
*/
public static <T> T asType(Expression expr, Class<T> TYPE) {
if (expr == null) {
return null;
}
if( STRICT ){
return expr.evaluate(null, TYPE );
}
else if (expr instanceof Literal) {
Literal literal = (Literal) expr;
return (T) literal.evaluate(null, TYPE );
}
else if (expr instanceof Function) {
Function function = (Function) expr;
List params = function.getParameters();
if ( params != null && params.size() != 0 ) {
for (int i = 0; i < params.size(); i++) {
Expression e = (Expression) params.get(i);
T value = asType(e, TYPE);
if (value != null) {
return value;
}
}
}
}
else {
try { // this is a bad idea, not expected to work much
T value = expr.evaluate(null, TYPE );
if (TYPE.isInstance(value)) {
return value;
}
} catch (NullPointerException expected) {
return null; // well that was not unexpected
} catch (Throwable ignore) { // I did say that was a bad idea
}
}
return null; // really need a Feature to acomplish this one
}
/**
* Treat provided value as a Number, used for math opperations.
* <p>
* This function allows for the non stongly typed Math Opperations
* favoured by the Expression standard.
* </p>
* <p>
* Able to hanle:
* <ul>
* <li>null - to NaN
* <li>Number
* <li>String - valid Integer and Double encodings
* </ul>
*
* </p>
* @param value
* @return double or Double.NaN;
* @throws IllegalArgumentException For non numerical among us -- like Geometry
*/
public static double number(Object value) {
if( value == null ) return Double.NaN;
if( value instanceof Number ){
Number number = (Number) value;
return number.doubleValue();
}
if( value instanceof String ){
String text = (String) value;
try {
Number number = (Number) gets( text, Number.class );
return number.doubleValue();
} catch (Throwable e) {
throw new IllegalArgumentException("Unable to decode '"+text+"' as a number" );
}
}
if( value instanceof Expression ){
throw new IllegalArgumentException("Cannot deal with un evaulated Expression");
}
throw new IllegalArgumentException("Unable to evaulate "+value.getClass()+" in a numeric context");
}
/**
* Used to upcovnert a "Text Value" into the provided TYPE.
* <p>
* Used to tread softly on the Java typing system, because
* Filter/Expression is not strongly typed. Values in in
* Expression land are often not the the real Java Objects
* we wish they were - it is reall a small, lax, query
* language and Java objects need a but of help getting
* through.
* <p>
* </p>
* A couple notes:
* <ul>
* <li>Usual trick of reflection for a Constructors that
* supports a String parameter is used as a last ditch effort.
* </li>
* <li>will do its best to turn Object into the indicated Class
* <li>will be used for ordering literals against attribute values
* are calculated at runtime (like Date.)
* </ul>
* Remember Strong typing is for whimps who know what they are
* doing ahead of time. Real programmers let their program
* learn at runtime... :-)
* </p>
*
* @param text
* @param TYPE
* @throws open set of Throwable reflection for TYPE( String )
*/
public static Object gets( String text, Class TYPE ) throws Throwable {
if( text == null ) return null;
if( TYPE == String.class ) return text;
if( TYPE == Integer.class ) {
return Integer.decode( text );
}
if( TYPE == Double.class ){
return Double.valueOf( text );
}
if( TYPE == Number.class ){
try {
return Double.valueOf( text );
}
catch( NumberFormatException ignore ){
}
return Integer.decode( text );
}
if( TYPE == Color.class ){
return new Color( Integer.decode( text ).intValue() );
}
try {
Constructor create = TYPE.getConstructor( new Class[]{String.class});
return create.newInstance( new Object[]{ text } );
} catch (SecurityException e) {
// hates you
} catch (NoSuchMethodException e) {
// nope
} catch (IllegalArgumentException e) {
// should not occur
} catch (InstantiationException e) {
// should not occur, perhaps the class was abstract?
// eg. Number.class is a bad idea
} catch (IllegalAccessException e) {
// hates you
} catch (InvocationTargetException e) {
// should of worked but we got a real problem,
// an actual problem
throw e.getCause();
}
return null;
}
public static String puts( double number ){
if( Math.rint(number) == number ){
return Integer.toString( (int) number );
}
return Double.toString( number );
}
/**
* Inverse of eval, used to softly type supported
* types into Text for use as literals.
*/
public static String puts( Object obj ){
if( obj == null ) return null;
if( obj instanceof String) return (String) obj;
if( obj instanceof Color ){
Color color = (Color) obj;
return puts( color );
}
if( obj instanceof Number ){
Number number = (Number) obj;
return puts( number.doubleValue() );
}
return obj.toString();
}
public static String puts( Color color ){
String redCode = Integer.toHexString(color.getRed());
String greenCode = Integer.toHexString(color.getGreen());
String blueCode = Integer.toHexString(color.getBlue());
if (redCode.length() == 1) redCode = "0" + redCode;
if (greenCode.length() == 1) greenCode = "0" + greenCode;
if (blueCode.length() == 1) blueCode = "0" + blueCode;
return "#" + redCode + greenCode + blueCode;
}
}