/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2014-2016, 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.data.solr; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.URLDecoder; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Logger; import org.geotools.factory.CommonFactoryFinder; import org.geotools.filter.FilterCapabilities; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.GeometryDescriptor; import org.opengis.filter.And; import org.opengis.filter.BinaryComparisonOperator; import org.opengis.filter.BinaryLogicOperator; import org.opengis.filter.ExcludeFilter; import org.opengis.filter.Filter; import org.opengis.filter.FilterFactory; import org.opengis.filter.FilterVisitor; 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.PropertyIsNil; import org.opengis.filter.PropertyIsNotEqualTo; import org.opengis.filter.PropertyIsNull; import org.opengis.filter.expression.Expression; import org.opengis.filter.expression.Literal; import org.opengis.filter.expression.PropertyName; import org.opengis.filter.identity.Identifier; import org.opengis.filter.spatial.BBOX; import org.opengis.filter.spatial.Beyond; import org.opengis.filter.spatial.BinarySpatialOperator; 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; import org.opengis.filter.temporal.After; import org.opengis.filter.temporal.AnyInteracts; import org.opengis.filter.temporal.Before; import org.opengis.filter.temporal.Begins; import org.opengis.filter.temporal.BegunBy; import org.opengis.filter.temporal.BinaryTemporalOperator; import org.opengis.filter.temporal.During; import org.opengis.filter.temporal.EndedBy; import org.opengis.filter.temporal.Ends; import org.opengis.filter.temporal.Meets; import org.opengis.filter.temporal.MetBy; import org.opengis.filter.temporal.OverlappedBy; import org.opengis.filter.temporal.TContains; import org.opengis.filter.temporal.TEquals; import org.opengis.filter.temporal.TOverlaps; /** * Encodes a OGC filter into a SOLR query syntax * */ public class FilterToSolr implements FilterVisitor { /* Standard java logger */ private static Logger LOGGER = org.geotools.util.logging.Logging.getLogger(FilterToSolr.class); /* Lucene characters to escape on filter expressions */ private static final String[] LUCENE_SPECIAL_CHARACTERS = new String[] { "+", "-", "&&", "||", "!", "(", ")", "{", "}", "[", "]", "^", "\"", "~", ":" }; /** Filter factory */ protected static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(null); /** Where to write the constructed string from visiting the filters. */ protected Writer out; /** The filter types that this class can encode */ protected FilterCapabilities capabilities = null; /* Store SOLR identifier attribute */ private SolrAttribute primaryKey; /* Store SOLR attribute used as layer name filter */ private String featureTypeName; /** the feature type */ private SimpleFeatureType featureType; public FilterToSolr(SimpleFeatureType featureType) { this.featureType = featureType; } /** * A single call method to encode filter to SOLR query * * @param filter * @return a string representing the filter encoded to SOLR. * @throws Exception */ public String encodeToString(Filter filter) throws Exception { StringWriter out = new StringWriter(); this.out = out; this.encode(filter); return out.getBuffer().toString(); } /** * Performs the encoding, sends the encoded SOLR string to the writer passed in. * * @param filter the Filter to be encoded. * * @throws Exception if there were io problems or unsupported filter operation */ public void encode(Filter filter) throws Exception { if (out == null) throw new Exception("Can't encode to a null writer."); if (getCapabilities().fullySupports(filter)) { try { filter.accept(this, out); } catch (Exception ioe) { LOGGER.warning("Unable to export filter" + ioe); throw new Exception("Problem writing filter: ", ioe); } } else { throw new Exception("Filter type not supported"); } } /** * Describes the capabilities of this encoder. * * <p> * Performs lazy creation of capabilities. * </p> * * If you're subclassing this class, override createFilterCapabilities to declare which * filtercapabilities you support. Don't use this method. * * @return The capabilities supported by this encoder. */ public FilterCapabilities getCapabilities() { if (capabilities == null) { capabilities = createFilterCapabilities(); } return capabilities; // maybe clone? Make immutable somehow } /** * Sets the capabilities of this filter. * * @return FilterCapabilities for this Filter */ protected FilterCapabilities createFilterCapabilities() { capabilities = new FilterCapabilities(); capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS); capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS); capabilities.addType(FilterCapabilities.FID); capabilities.addType(FilterCapabilities.BETWEEN); capabilities.addType(FilterCapabilities.LIKE); capabilities.addType(FilterCapabilities.NULL_CHECK); capabilities.addType(FilterCapabilities.SPATIAL_BBOX); // JD: as of solr 5 the disjoint filter no longer works directly so we disable it // See https://issues.apache.org/jira/browse/LUCENE-5692 //capabilities.addType(FilterCapabilities.SPATIAL_DISJOINT); capabilities.addType(FilterCapabilities.SPATIAL_WITHIN); capabilities.addType(FilterCapabilities.SPATIAL_INTERSECT); capabilities.addType(FilterCapabilities.SPATIAL_CONTAINS); // temporal filters capabilities.addType(After.class); capabilities.addType(Before.class); capabilities.addType(Begins.class); capabilities.addType(BegunBy.class); capabilities.addType(During.class); capabilities.addType(Ends.class); capabilities.addType(EndedBy.class); capabilities.addType(TContains.class); capabilities.addType(TEquals.class); return capabilities; } /* * The current implementation does exactly nothing */ @Override public Object visitNullFilter(Object extraData) { return extraData; } /* * Writes the query for the IncludeFilter by writing "FALSE". */ @Override public Object visit(ExcludeFilter filter, Object extraData) { StringWriter output = asStringWriter(extraData); output.append("-*:*"); return output; } /* * Writes the query for the IncludeFilter by writing "TRUE". */ @Override public Object visit(IncludeFilter filter, Object extraData) { StringWriter output = asStringWriter(extraData); output.append("*:*"); return output; } @Override public Object visit(And filter, Object extraData) { return buildBinaryLogicalOperator("AND", this, filter, extraData); } @Override public Object visit(Id filter, Object extraData) { StringWriter output = asStringWriter(extraData); Set<Identifier> ids = filter.getIdentifiers(); output.append(" ("); for (Iterator<Identifier> i = ids.iterator(); i.hasNext();) { Identifier id = i.next(); String fid = decodeFID(id.toString()); output.write(primaryKey.getName() + ":" + "\"" + fid + "\""); if (i.hasNext()) { output.write(" OR "); } } output.append(") "); return output; } @Override public Object visit(Not filter, Object extraData) { StringWriter output = asStringWriter(extraData); output.append("NOT ("); filter.getFilter().accept(this, output); output.append(")"); return output; } @Override public Object visit(Or filter, Object extraData) { return buildBinaryLogicalOperator("OR", this, filter, extraData); } @Override public Object visit(PropertyIsBetween filter, Object extraData) { checkExpressionIsProperty(filter.getExpression()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) filter.getExpression(); propertyName.accept(visitor, output); output.append(":["); filter.getLowerBoundary().accept(visitor, output); output.append(" TO "); filter.getUpperBoundary().accept(visitor, output); output.append("]"); return output; } @Override public Object visit(PropertyIsEqualTo filter, Object extraData) { return buildComparison(filter, extraData); } @Override public Object visit(PropertyIsNotEqualTo filter, Object extraData) { return buildComparison(filter, extraData); } @Override public Object visit(PropertyIsGreaterThan filter, Object extraData) { Expression[] expr = binaryFilterVisitorNormalizer(filter.getExpression1(), filter.getExpression2()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) expr[0]; propertyName.accept(visitor, output); output.append(":{"); expr[1].accept(visitor, output); output.append(" TO *}"); return output; } @Override public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { Expression[] expr = binaryFilterVisitorNormalizer(filter.getExpression1(), filter.getExpression2()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) expr[0]; propertyName.accept(visitor, output); output.append(":["); expr[1].accept(visitor, output); output.append(" TO *]"); return output; } @Override public Object visit(PropertyIsLessThan filter, Object extraData) { Expression[] expr = binaryFilterVisitorNormalizer(filter.getExpression1(), filter.getExpression2()); checkExpressionIsProperty(filter.getExpression1()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) expr[0]; propertyName.accept(visitor, output); output.append(":{* TO "); expr[1].accept(visitor, output); output.append("}"); return output; } @Override public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { Expression[] expr = binaryFilterVisitorNormalizer(filter.getExpression1(), filter.getExpression2()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) expr[0]; propertyName.accept(visitor, output); output.append(":[* TO "); expr[1].accept(visitor, output); output.append("]"); return output; } @Override public Object visit(PropertyIsLike filter, Object extraData) { checkExpressionIsProperty(filter.getExpression()); StringWriter output = asStringWriter(extraData); String pattern = escapeSpecialCharacters(filter.getLiteral(), filter.getEscape()); pattern = pattern.replace(filter.getWildCard(), ".*"); pattern = pattern.replace(filter.getSingleChar(), ".{1,1}"); Expression expr = filter.getExpression(); ExpressionToSolr visitor = new ExpressionToSolr(); expr.accept(visitor, output); output.append(":/"); output.append(pattern); output.append("/ "); return output; } @Override public Object visit(PropertyIsNull filter, Object extraData) { checkExpressionIsProperty(filter.getExpression()); StringWriter output = asStringWriter(extraData); ExpressionToSolr visitor = new ExpressionToSolr(); PropertyName propertyName = (PropertyName) filter.getExpression(); output.append("-"); propertyName.accept(visitor, output); output.append(":[* TO *]"); return output; } @Override public Object visit(PropertyIsNil filter, Object extraData) { throw new UnsupportedOperationException("PropertyIsNil filter not supported"); } @Override public Object visit(BBOX filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(Contains filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(Disjoint filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(Equals filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(Within filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(Intersects filter, Object extraData) { return visitBinarySpatialOperator(filter, extraData); } @Override public Object visit(After after, Object extraData) { return visitBinaryTemporalOperator(after, extraData); } @Override public Object visit(Before before, Object extraData) { return visitBinaryTemporalOperator(before, extraData); } @Override public Object visit(Begins begins, Object extraData) { return visitBinaryTemporalOperator(begins, extraData); } @Override public Object visit(BegunBy begunBy, Object extraData) { return visitBinaryTemporalOperator(begunBy, extraData); } @Override public Object visit(Ends ends, Object extraData) { return visitBinaryTemporalOperator(ends, extraData); } @Override public Object visit(EndedBy endedBy, Object extraData) { return visitBinaryTemporalOperator(endedBy, extraData); } @Override public Object visit(During during, Object extraData) { return visitBinaryTemporalOperator(during, extraData); } @Override public Object visit(TContains contains, Object extraData) { return visitBinaryTemporalOperator(contains, extraData); } @Override public Object visit(TEquals equals, Object extraData) { return visitBinaryTemporalOperator(equals, extraData); } /* UNSUPPORTED */ @Override public Object visit(Beyond filter, Object extraData) { throw new UnsupportedOperationException("Beyond filter not supported"); } @Override public Object visit(Crosses filter, Object extraData) { throw new UnsupportedOperationException("Crosses filter not supported"); } @Override public Object visit(DWithin filter, Object extraData) { throw new UnsupportedOperationException("DWithin filter not supported"); } @Override public Object visit(Overlaps filter, Object extraData) { throw new UnsupportedOperationException("Overlaps filter not supported"); } @Override public Object visit(Touches filter, Object extraData) { throw new UnsupportedOperationException("Touches filter not supported"); } @Override public Object visit(AnyInteracts anyInteracts, Object extraData) { throw new UnsupportedOperationException("AnyInteracts filter not supported"); } @Override public Object visit(Meets meets, Object extraData) { throw new UnsupportedOperationException("Meets filter not supported"); } @Override public Object visit(MetBy metBy, Object extraData) { throw new UnsupportedOperationException("MetBy filter not supported"); } @Override public Object visit(OverlappedBy overlappedBy, Object extraData) { throw new UnsupportedOperationException("OverlappedBy filter not supported"); } @Override public Object visit(TOverlaps contains, Object extraData) { throw new UnsupportedOperationException("TOverlaps filter not supported"); } /** * Sets the {@link FilterToSolr#featureTypeName} */ public void setFeatureTypeName(String featureTypeName) { this.featureTypeName = featureTypeName; } /** * Sets the {@link FilterToSolr#primaryKey} */ public void setPrimaryKey(SolrAttribute primaryKey) { this.primaryKey = primaryKey; } /** * Convert extraData parameter to StringWriter or create new one if not exists </p> This method * is called at the start of each visit method to obtain output to write in * * @param extraData output to write in * */ protected static StringWriter asStringWriter(Object extraData) { if (extraData instanceof StringWriter) { return (StringWriter) extraData; } return new StringWriter(); } /** * * Escape with "\\" the phrase according to Lucene special characters and other characters * passed as input * * @see {@link FilterToSolr#LUCENE_SPECIAL_CHARACTERS} * * @param searchPhrase the phrase to escape * @param otherEscapes additional parameters to escape other than * {@link FilterToSolr#LUCENE_SPECIAL_CHARACTERS} * * @return the escaped string */ protected static String escapeSpecialCharacters(String searchPhrase, String... otherEscapes) { for (int i = 0; i < LUCENE_SPECIAL_CHARACTERS.length; i++) { searchPhrase = searchPhrase.replace(LUCENE_SPECIAL_CHARACTERS[i], "\\" + LUCENE_SPECIAL_CHARACTERS[i]); } for (String e : otherEscapes) { searchPhrase = searchPhrase.replace(e, "\\" + e); } return searchPhrase; } /* * Check if Expression is a Property */ private void checkExpressionIsProperty(Expression expr) { if (!(expr instanceof PropertyName)) { throw new RuntimeException("SOLR requires a PropertyName"); } } /* * Check if Expression is a Literal */ private void checkExpressionIsLiteral(Expression expr) { if (!(expr instanceof Literal)) { throw new RuntimeException("SOLR requires a Literal"); } } /* * Writes the SOLR query for binary comparison operator : EQUAL, NOT EQUAL * * @param filter binary comparison operator to encode */ private Object buildComparison(BinaryComparisonOperator filter, Object extraData) { StringWriter output = asStringWriter(extraData); if (filter instanceof PropertyIsNotEqualTo) { output.append("-"); } Expression[] expr = binaryFilterVisitorNormalizer(filter.getExpression1(), filter.getExpression2()); ExpressionToSolr visitor = new ExpressionToSolr(); expr[0].accept(visitor, output); output.append(":"); expr[1].accept(visitor, output); return output; } /* * Writes the SOLR query for binary logical operator : AND, OR * * @param operator the operator to use in encode * * @param visitor encoder to manage nested inner expressions * * @param filter binary logical operator to encode */ private Object buildBinaryLogicalOperator(final String operator, FilterVisitor visitor, BinaryLogicOperator filter, Object extraData) { StringWriter output = asStringWriter(extraData); List<Filter> children = filter.getChildren(); if (children != null) { for (Iterator<Filter> i = children.iterator(); i.hasNext();) { Filter child = i.next(); if (child instanceof BinaryLogicOperator) { output.append("("); } child.accept(visitor, output); if (child instanceof BinaryLogicOperator) { output.append(")"); } if (i.hasNext()) { output.append(" ").append(operator).append(" "); } } } return output; } /* * Writes the SOLR query for temporal operator : After, Before, Begins, Ends, TEquals, BegunBy, * EndedBy, During, TContains * * @param filter temporal operator to encode */ private Object visitBinaryTemporalOperator(BinaryTemporalOperator filter, Object extraData) { StringWriter output = asStringWriter(extraData); Expression e1 = filter.getExpression1(); Expression e2 = filter.getExpression2(); ExpressionToSolr visitor = new ExpressionToSolr(filter); if (filter instanceof After) { checkExpressionIsProperty(e1); checkExpressionIsLiteral(e2); PropertyName propertyName = (PropertyName) e1; propertyName.accept(visitor, output); output.append(":{"); e2.accept(visitor, output); output.append(" TO *}"); } if (filter instanceof Before) { checkExpressionIsProperty(e1); checkExpressionIsLiteral(e2); PropertyName propertyName = (PropertyName) e1; propertyName.accept(visitor, output); output.append(":{* TO "); e2.accept(visitor, output); output.append("}"); } if (filter instanceof Begins || filter instanceof Ends || filter instanceof TEquals) { checkExpressionIsProperty(e1); checkExpressionIsLiteral(e2); PropertyName propertyName = (PropertyName) e1; propertyName.accept(visitor, output); output.append(":"); e2.accept(visitor, output); } if (filter instanceof BegunBy || filter instanceof EndedBy) { checkExpressionIsProperty(e2); checkExpressionIsLiteral(e1); PropertyName propertyName = (PropertyName) e2; propertyName.accept(visitor, output); output.append(":"); e1.accept(visitor, output); } if (filter instanceof During) { checkExpressionIsProperty(e1); checkExpressionIsLiteral(e2); PropertyName propertyName = (PropertyName) e1; propertyName.accept(visitor, output); output.append(":{"); e2.accept(visitor, output); output.append("}"); } if (filter instanceof TContains) { checkExpressionIsProperty(e2); checkExpressionIsLiteral(e1); PropertyName propertyName = (PropertyName) e2; propertyName.accept(visitor, output); output.append(":{"); e1.accept(visitor, output); output.append("}"); } return output; } /* * Writes the SOLR query for spatial operator : BBOX, IsWithin, IsDisjointTo, IsWithin, * Intersects, Contains * * @param filter spatial operator to encode */ private Object visitBinarySpatialOperator(BinarySpatialOperator filter, Object extraData) { StringWriter output = asStringWriter(extraData); Expression e1 = filter.getExpression1(); checkExpressionIsProperty(e1); Expression e2 = filter.getExpression2(); checkExpressionIsLiteral(e2); ExpressionToSolr visitor = new ExpressionToSolr(); // initialize spatial strategy AttributeDescriptor spatialAtt = (AttributeDescriptor) e1.evaluate(featureType); if (spatialAtt != null && spatialAtt instanceof GeometryDescriptor) { visitor.setSpatialStrategy(SolrSpatialStrategy.createStrategy((GeometryDescriptor) spatialAtt)); } else { LOGGER.warning("Spatial field: " + e1.toString() + " resolved to null or non-spatial"); } e1.accept(visitor, extraData); if (filter instanceof BBOX) { output.append(":\"Intersects("); e2.accept(visitor, extraData); output.append(")\""); } else if (filter instanceof Disjoint) { output.append(":\"IsDisjointTo("); e2.accept(visitor, extraData); output.append(")\""); } else if (filter instanceof Within) { output.append(":\"IsWithin("); e2.accept(visitor, extraData); output.append(")\""); } else if (filter instanceof Intersects) { output.append(":\"Intersects("); e2.accept(visitor, extraData); output.append(")\""); } else if (filter instanceof Contains) { output.append(":\"Contains("); e2.accept(visitor, extraData); output.append(")\""); } else { throw new RuntimeException("Unsupported filter type " + filter.getClass()); } return output; } /* * Decodes a fid into its components based on a primary key. */ private String decodeFID(String FID) { if (FID.startsWith(this.featureTypeName + ".")) { FID = FID.substring(this.featureTypeName.length() + 1); } try { FID = URLDecoder.decode(FID, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } return FID; } /* * Swap the PropertyName / Literal for binary filtering to obtain the correct order: first * PropertyName next Literal * * @return an array with PropertyName as first element and Literal as second element * * @throws UnsupportedOperationException if the arguments are both Literal or PropertyName */ private Expression[] binaryFilterVisitorNormalizer(Expression expr1, Expression expr2) { Expression e1 = null; Expression e2 = null; if (expr1 instanceof PropertyName && expr2 instanceof Literal) { e1 = expr1; e2 = expr2; } else if (expr2 instanceof Literal && expr2 instanceof PropertyName) { e1 = expr2; e2 = expr1; } else { throw new UnsupportedOperationException( "Expressions must be one PropertyName and one Literal"); } return new Expression[] { e1, e2 }; } }