/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2002-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 mil.nga.giat.data.elasticsearch;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.ANALYZED;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.DATE_FORMAT;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.MATCH_ALL;
import static mil.nga.giat.data.elasticsearch.ElasticConstants.NESTED;
import org.geotools.data.Query;
import org.geotools.factory.CommonFactoryFinder;
import org.geotools.factory.Hints;
import org.geotools.filter.FilterCapabilities;
import org.geotools.geojson.geom.GeometryJSON;
import org.geotools.util.ConverterFactory;
import org.geotools.util.Converters;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
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.Add;
import org.opengis.filter.expression.BinaryExpression;
import org.opengis.filter.expression.Divide;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.ExpressionVisitor;
import org.opengis.filter.expression.Function;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.Multiply;
import org.opengis.filter.expression.NilExpression;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.expression.Subtract;
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;
import org.opengis.temporal.Period;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.vividsolutions.jts.geom.CoordinateSequence;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.LinearRing;
import mil.nga.giat.shaded.es.common.joda.Joda;
import mil.nga.giat.shaded.joda.time.format.DateTimeFormatter;
/**
* Encodes an OGC {@link Filter} and creates a filter for an Elasticsearch query.
* Optionally applies SQL View parameters from {@link Query} defining Elasticsearch
* {@link QueryBuilder} ("Q") and filter ("F") directly. If provided, specified filter is added to
* the generated filter.
*
* Based on org.geotools.data.jdbc.FilterToSQL in the GeoTools library/jdbc module.
*/
public class FilterToElastic implements FilterVisitor, ExpressionVisitor {
/** Standard java logger */
static Logger LOGGER = Logging.getLogger(FilterToElastic.class);
/** filter factory */
private static FilterFactory filterFactory = CommonFactoryFinder.getFilterFactory(null);
private static final ObjectMapper mapper = new ObjectMapper();
private static final ObjectReader mapReader = mapper.readerWithView(Map.class).forType(HashMap.class);
private static final DateTimeFormatter DEFAULT_DATE_FORMATTER = Joda.forPattern("date_optional_time").printer();
/** The filter types that this class can encode */
private FilterCapabilities capabilities = null;
/** the schmema the encoder will use */
SimpleFeatureType featureType;
Geometry currentGeometry;
Object field;
Map<String,Object> currentShapeBuilder;
Boolean fullySupported;
Map<String,Object> queryBuilder;
Map<String,Object> nativeQueryBuilder;
Map<String,Map<String,Map<String,Object>>> aggregations;
private FilterToElasticHelper helper;
private String key;
private Object lower;
private Object upper;
private Boolean nested;
private String path;
private String pattern;
private Boolean analyzed;
private String type;
private List<String> ids;
private Period period;
private String op;
private Object begin;
private Object end;
private Map<String,String> parameters;
private Boolean nativeOnly;
private DateTimeFormatter dateFormatter;
public FilterToElastic() {
queryBuilder = MATCH_ALL;
nativeQueryBuilder = ImmutableMap.of("match_all", Collections.EMPTY_MAP);
helper = new FilterToElasticHelper(this);
}
/**
* Performs the encoding.
*
* @param filter the Filter to be encoded.
*
* @throws FilterToElasticException If there were io problems.
*/
public void encode(Filter filter) throws FilterToElasticException {
fullySupported = getCapabilities().fullySupports(filter);
filter.accept(this, null);
}
/**
* Performs the encoding.
* If SQL View parameters are provided in the query hints, they will be used
* to define and/or update the {@link FilteredQueryBuilder}.
*
* @param query the Query to be encoded.
*
* @throws FilterToElasticException If there were io problems.
*/
public void encode(Query query) throws FilterToElasticException {
encode(query.getFilter());
addViewParams(query);
}
/**
* Sets the featuretype the encoder is encoding for.
* <p>
* This is used for context for attribute expressions.
* </p>
*
* @param featureType
*/
public void setFeatureType(SimpleFeatureType featureType) {
this.featureType = featureType;
}
/**
* Sets the capabilities of this filter.
*
* @return FilterCapabilities for this Filter
*/
protected FilterCapabilities createFilterCapabilities() {
FilterCapabilities capabilities = new FilterCapabilities();
capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS);
capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
capabilities.addType(PropertyIsNull.class);
capabilities.addType(PropertyIsBetween.class);
capabilities.addType(Id.class);
capabilities.addType(IncludeFilter.class);
capabilities.addType(ExcludeFilter.class);
capabilities.addType(PropertyIsLike.class);
// spatial filters
capabilities.addType(BBOX.class);
capabilities.addType(Contains.class);
//capabilities.addType(Crosses.class);
capabilities.addType(Disjoint.class);
//capabilities.addType(Equals.class);
capabilities.addType(Intersects.class);
//capabilities.addType(Overlaps.class);
//capabilities.addType(Touches.class);
capabilities.addType(Within.class);
capabilities.addType(DWithin.class);
capabilities.addType(Beyond.class);
//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;
}
/**
* 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 synchronized final FilterCapabilities getCapabilities() {
if (capabilities == null) {
capabilities = createFilterCapabilities();
}
return capabilities; //maybe clone? Make immutable somehow
}
// BEGIN IMPLEMENTING org.opengis.filter.FilterVisitor METHODS
/**
* Writes the FilterBuilder for the ExcludeFilter.
*
* @param filter the filter to be visited
*/
public Object visit(ExcludeFilter filter, Object extraData) {
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", MATCH_ALL));
return extraData;
}
/**
* Writes the FilterBuilder for the IncludeFilter.
*
* @param filter the filter to be visited
*
*/
public Object visit(IncludeFilter filter, Object extraData) {
queryBuilder = MATCH_ALL;
return extraData;
}
/**
* Writes the FilterBuilder for the PropertyIsBetween Filter.
*
* @param filter the Filter to be visited.
*
*/
public Object visit(PropertyIsBetween filter, Object extraData) {
LOGGER.finest("exporting PropertyIsBetween");
Expression expr = (Expression) filter.getExpression();
Expression lowerbounds = (Expression) filter.getLowerBoundary();
Expression upperbounds = (Expression) filter.getUpperBoundary();
Class context;
nested = false;
AttributeDescriptor attType = (AttributeDescriptor)expr.evaluate(featureType);
if (attType != null) {
context = attType.getType().getBinding();
if (attType.getUserData().containsKey(NESTED)) {
nested = (Boolean) attType.getUserData().get(NESTED);
}
if (Date.class.isAssignableFrom(context)) {
updateDateFormatter(attType);
}
} else {
//assume it's a string?
context = String.class;
}
expr.accept(this, extraData);
key = (String) field;
lowerbounds.accept(this, context);
lower = field;
upperbounds.accept(this, context);
upper = field;
if(nested) {
path = extractNestedPath(key);
}
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gte", lower, "lte", upper)));
if(nested) {
queryBuilder = ImmutableMap.of("nested", ImmutableMap.of("path", path, "query", queryBuilder));
}
return extraData;
}
/**
* Writes the FilterBuilder for the Like Filter.
*
* @param filter the filter to be visited
*
*/
public Object visit(PropertyIsLike filter, Object extraData) {
char esc = filter.getEscape().charAt(0);
char multi = filter.getWildCard().charAt(0);
char single = filter.getSingleChar().charAt(0);
boolean matchCase = false;
if (filter.isMatchingCase()) {
LOGGER.fine("Case sensitive search not supported");
}
String literal = filter.getLiteral();
Expression att = filter.getExpression();
AttributeDescriptor attType = (AttributeDescriptor) att.evaluate(featureType);
analyzed = false;
nested = false;
if (attType != null) {
if (attType.getUserData().containsKey(ANALYZED)) {
analyzed = (Boolean) attType.getUserData().get(ANALYZED);
}
if (attType.getUserData().containsKey(NESTED)) {
nested = (Boolean) attType.getUserData().get(NESTED);
}
if (Date.class.isAssignableFrom(attType.getType().getBinding())) {
updateDateFormatter(attType);
}
}
att.accept(this, extraData);
key = (String) field;
if (analyzed) {
// use query string query post filter for analyzed fields
pattern = convertToQueryString(esc, multi, single, matchCase, literal);
} else {
// default to regexp filter
pattern = convertToRegex(esc, multi, single, matchCase, literal);
}
if (nested) {
path = extractNestedPath(key);
}
if (analyzed) {
// use query string query for analyzed fields
queryBuilder = ImmutableMap.of("query_string", ImmutableMap.of("query", pattern, "default_field", key));
} else {
// default to regexp query
queryBuilder = ImmutableMap.of("regexp", ImmutableMap.of(key, pattern));
}
if (nested) {
queryBuilder = ImmutableMap.of("nested", ImmutableMap.of("path", path, "query", queryBuilder));
}
return extraData;
}
/**
* Write the FilterBuilder for an And filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(And filter, Object extraData) {
return visit((BinaryLogicOperator)filter, "AND");
}
/**
* Write the FilterBuilder for a Not filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(Not filter, Object extraData) {
if(filter.getFilter() instanceof PropertyIsNull) {
Expression expr = ((PropertyIsNull) filter.getFilter()).getExpression();
expr.accept(this, extraData);
} else {
filter.getFilter().accept(this, extraData);
}
if(filter.getFilter() instanceof PropertyIsNull) {
queryBuilder = ImmutableMap.of("exists", ImmutableMap.of("field", field));
} else {
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", queryBuilder));
}
return extraData;
}
/**
* Write the FilterBuilder for an Or filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(Or filter, Object extraData) {
return visit((BinaryLogicOperator)filter, "OR");
}
/**
* Common implementation for BinaryLogicOperator filters. This way
* they're all handled centrally.
*
* @param filter the logic statement.
* @param extraData extra filter data. Not modified directly by this method.
*/
protected Object visit(BinaryLogicOperator filter, Object extraData) {
LOGGER.finest("exporting LogicFilter");
final List<Map<String,Object>> filters = new ArrayList<>();
for (final Filter child : filter.getChildren()) {
child.accept(this, extraData);
filters.add(queryBuilder);
}
if (extraData.equals("AND")) {
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must", filters));
} else if (extraData.equals("OR")) {
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("should", filters));
}
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsEqualTo filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, "=");
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, ">=");
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsGreaterThan filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, ">");
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsLessThan filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, "<");
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, "<=");
return extraData;
}
/**
* Write the FilterBuilder for this kind of filter
*
* @param filter the filter to visit
* @param extraData extra data (unused by this method)
*
*/
public Object visit(PropertyIsNotEqualTo filter, Object extraData) {
visitBinaryComparisonOperator((BinaryComparisonOperator)filter, "!=");
return extraData;
}
/**
* Common implementation for BinaryComparisonOperator filters.
*
* @param filter the comparison.
*
*/
protected void visitBinaryComparisonOperator(BinaryComparisonOperator filter, Object extraData) {
LOGGER.finest("exporting FilterBuilder ComparisonFilter");
Expression left = filter.getExpression1();
Expression right = filter.getExpression2();
if (isBinaryExpression(left) || isBinaryExpression(right)) {
throw new UnsupportedOperationException("Binary expressions not supported");
}
AttributeDescriptor attType = null;
Class leftContext = null, rightContext = null;
if (left instanceof PropertyName) {
// It's a propertyname, we should get the class and pass it in
// as context to the tree walker.
attType = (AttributeDescriptor)left.evaluate(featureType);
if (attType != null) {
rightContext = attType.getType().getBinding();
}
}
if (right instanceof PropertyName) {
attType = (AttributeDescriptor)right.evaluate(featureType);
if (attType != null) {
leftContext = attType.getType().getBinding();
}
}
nested = false;
if (attType != null) {
if (attType.getUserData().containsKey(NESTED)) {
nested = (Boolean) attType.getUserData().get(NESTED);
}
if (Date.class.isAssignableFrom(attType.getType().getBinding())) {
updateDateFormatter(attType);
}
}
//case sensitivity
if ( !filter.isMatchingCase() ) {
//we only do for = and !=
if ( filter instanceof PropertyIsEqualTo ||
filter instanceof PropertyIsNotEqualTo ) {
//and only for strings
if ( String.class.equals( leftContext )
|| String.class.equals( rightContext ) ) {
//matchCase = false;
LOGGER.fine("Case insensitive filter not supported");
}
}
}
type = (String) extraData;
if (left instanceof PropertyName) {
left.accept(this, null);
key = (String) field;
right.accept(this, rightContext);
} else {
right.accept(this, null);
key = (String) field;
left.accept(this, leftContext);
}
if (nested) {
path = extractNestedPath(key);
}
if (type.equals("=")) {
queryBuilder = ImmutableMap.of("term", ImmutableMap.of(key, field));
} else if (type.equals("!=")) {
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", ImmutableMap.of("term", ImmutableMap.of(key, field))));
} else if (type.equals(">")) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gt", field)));
} else if (type.equals(">=")) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gte", field)));
} else if (type.equals("<")) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("lt", field)));
} else if (type.equals("<=")) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("lte", field)));
}
if (nested) {
queryBuilder = ImmutableMap.of("nested", ImmutableMap.of("path", path, "query", queryBuilder));
}
}
/*
* determines if the function is a binary expression
*/
boolean isBinaryExpression(Expression e) {
return e instanceof BinaryExpression;
}
/**
* Writes the FilterBuilder for the Null Filter.
*
* @param filter the null filter.
*
*/
public Object visit(PropertyIsNull filter, Object extraData) {
LOGGER.finest("exporting NullFilter");
Expression expr = filter.getExpression();
expr.accept(this, extraData);
queryBuilder = ImmutableMap.of("bool", ImmutableMap.of("must_not", ImmutableMap.of("exists", ImmutableMap.of("field", field))));
return extraData;
}
public Object visit(PropertyIsNil filter, Object extraData) {
throw new UnsupportedOperationException("isNil not supported");
}
/**
* Encodes an Id filter
*
* @param filter the
*
*/
public Object visit(Id filter, Object extraData) {
final List<String> idList = new ArrayList<>();
for (final Identifier id : filter.getIdentifiers()) {
idList.add(id.toString());
}
ids = idList;
queryBuilder = ImmutableMap.of("ids", ImmutableMap.of("values", ids));
return extraData;
}
public Object visit(BBOX filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Beyond filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Contains filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Crosses filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Disjoint filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(DWithin filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Equals filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Intersects filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Overlaps filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Touches filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
public Object visit(Within filter, Object extraData) {
return visitBinarySpatialOperator((BinarySpatialOperator)filter, extraData);
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter,
Object extraData) {
// basic checks
if (filter == null)
throw new NullPointerException(
"Filter to be encoded cannot be null");
// extract the property name and the geometry literal
BinarySpatialOperator op = (BinarySpatialOperator) filter;
Expression e1 = op.getExpression1();
Expression e2 = op.getExpression2();
if (e1 instanceof Literal && e2 instanceof PropertyName) {
e1 = (PropertyName) op.getExpression2();
e2 = (Literal) op.getExpression1();
}
if (e1 instanceof PropertyName && e2 instanceof Literal) {
//call the "regular" method
return visitBinarySpatialOperator(filter, (PropertyName)e1, (Literal)e2, filter
.getExpression1() instanceof Literal, extraData);
}
else {
//call the join version
return visitBinarySpatialOperator(filter, e1, e2, extraData);
}
}
protected Object visitBinaryTemporalOperator(BinaryTemporalOperator filter,
Object extraData) {
if (filter == null) {
throw new NullPointerException("Null filter");
}
Expression e1 = filter.getExpression1();
Expression e2 = filter.getExpression2();
if (e1 instanceof Literal && e2 instanceof PropertyName) {
e1 = (PropertyName) filter.getExpression2();
e2 = (Literal) filter.getExpression1();
}
if (e1 instanceof PropertyName && e2 instanceof Literal) {
//call the "regular" method
return visitBinaryTemporalOperator(filter, (PropertyName)e1, (Literal)e2,
filter.getExpression1() instanceof Literal, extraData);
}
else {
//call the join version
return visitBinaryTemporalOperator(filter, e1, e2, extraData);
}
}
/**
* Handles the common case of a PropertyName,Literal geometry binary temporal operator.
* <p>
* Subclasses should override if they support more temporal operators than what is handled in
* this base class.
* </p>
*/
protected Object visitBinaryTemporalOperator(BinaryTemporalOperator filter,
PropertyName property, Literal temporal, boolean swapped, Object extraData) {
AttributeDescriptor attType = (AttributeDescriptor)property.evaluate(featureType);
Class typeContext = null;
nested = false;
if (attType != null) {
typeContext = attType.getType().getBinding();
if (attType.getUserData().containsKey(NESTED)) {
nested = (Boolean) attType.getUserData().get(NESTED);
}
updateDateFormatter(attType);
}
//check for time period
period = null;
if (temporal.evaluate(null) instanceof Period) {
period = (Period) temporal.evaluate(null);
}
//verify that those filters that require a time period have one
if ((filter instanceof Begins || filter instanceof BegunBy || filter instanceof Ends ||
filter instanceof EndedBy || filter instanceof During || filter instanceof TContains) &&
period == null) {
if (period == null) {
throw new IllegalArgumentException("Filter requires a time period");
}
}
if (filter instanceof TEquals && period != null) {
throw new IllegalArgumentException("TEquals filter does not accept time period");
}
//ensure the time period is the correct argument
if ((filter instanceof Begins || filter instanceof Ends || filter instanceof During) &&
swapped) {
throw new IllegalArgumentException("Time period must be second argument of Filter");
}
if ((filter instanceof BegunBy || filter instanceof EndedBy || filter instanceof TContains) &&
!swapped) {
throw new IllegalArgumentException("Time period must be first argument of Filter");
}
key = "";
if (filter instanceof After || filter instanceof Before) {
op = filter instanceof After ? " > " : " < ";
if (period != null) {
property.accept(this, extraData);
key = (String) field;
visitBegin(period, extraData);
begin = field;
visitEnd(period, extraData);
end = field;
}
else {
property.accept(this, extraData);
key = (String) field;
temporal.accept(this, typeContext);
}
}
else if (filter instanceof Begins || filter instanceof Ends ||
filter instanceof BegunBy || filter instanceof EndedBy ) {
property.accept(this, extraData);
key = (String) field;
if (filter instanceof Begins || filter instanceof BegunBy) {
visitBegin(period, extraData);
}
else {
visitEnd(period, extraData);
}
}
else if (filter instanceof During || filter instanceof TContains){
property.accept(this, extraData);
key = (String) field;
visitBegin(period, extraData);
lower = field;
visitEnd(period, extraData);
}
else if (filter instanceof TEquals) {
property.accept(this, extraData);
key = (String) field;
temporal.accept(this, typeContext);
}
if (nested) {
path = extractNestedPath(key);
}
if (filter instanceof After || filter instanceof Before) {
if (period != null) {
if ((op.equals(" > ") && !swapped) || (op.equals(" < ") && swapped)) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gt", end)));
} else {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("lt", begin)));
}
}
else {
if (op.equals(" < ") || swapped) {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("lt", field)));
} else {
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gt", field)));
}
}
}
else if (filter instanceof Begins || filter instanceof Ends ||
filter instanceof BegunBy || filter instanceof EndedBy ) {
queryBuilder = ImmutableMap.of("term", ImmutableMap.of(key, field));
}
else if (filter instanceof During || filter instanceof TContains){
queryBuilder = ImmutableMap.of("range", ImmutableMap.of(key, ImmutableMap.of("gt", lower, "lt", field)));
}
else if (filter instanceof TEquals) {
queryBuilder = ImmutableMap.of("term", ImmutableMap.of(key, field));
}
if (nested) {
queryBuilder = ImmutableMap.of("nested", ImmutableMap.of("path", path, "query", queryBuilder));
}
return extraData;
}
void visitBegin(Period p, Object extraData) {
filterFactory.literal(p.getBeginning().getPosition().getDate()).accept(this, extraData);
}
void visitEnd(Period p, Object extraData) {
filterFactory.literal(p.getEnding().getPosition().getDate()).accept(this, extraData);
}
/**
* Handles the general case of two expressions in a binary temporal filter.
* <p>
* Subclasses should override if they support more temporal operators than what is handled in
* this base class.
* </p>
*/
protected Object visitBinaryTemporalOperator(BinaryTemporalOperator filter, Expression e1,
Expression e2, Object extraData) {
throw new UnsupportedOperationException("Join version of binary temporal operator not supported");
}
/**
* Encodes a null filter value. The current implementation
* does exactly nothing.
* @param extraData extra data to be used to evaluate the filter
* @return the untouched extraData parameter
*/
public Object visitNullFilter(Object extraData) {
return extraData;
}
// END IMPLEMENTING org.opengis.filter.FilterVisitor METHODS
// START IMPLEMENTING org.opengis.filter.ExpressionVisitor METHODS
/**
* Writes the FilterBuilder for the attribute Expression.
*
* @param expression the attribute.
*
*/
@Override
public Object visit(PropertyName expression, Object extraData) {
LOGGER.finest("exporting PropertyName");
SimpleFeatureType featureType = this.featureType;
Class target = null;
if(extraData instanceof Class) {
target = (Class) extraData;
}
//first evaluate expression against feature type get the attribute,
AttributeDescriptor attType = (AttributeDescriptor) expression.evaluate(featureType);
String encodedField;
if ( attType != null ) {
encodedField = attType.getLocalName();
if(target != null && target.isAssignableFrom(attType.getType().getBinding())) {
// no need for casting, it's already the right type
target = null;
}
} else {
// fall back to just encoding the property name
encodedField = expression.getPropertyName();
}
if (target != null) {
LOGGER.fine("PropertyName type casting not implemented");
}
field = encodedField;
return extraData;
}
/**
* Export the contents of a Literal Expresion
*
* @param expression
* the Literal to export
*
* @throws FilterToElasticException If there were io problems.
*/
@Override
public Object visit(Literal expression, Object context)
throws FilterToElasticException {
LOGGER.finest("exporting LiteralExpression");
// type to convert the literal to
Class target = null;
if ( context instanceof Class ) {
target = (Class) context;
}
try {
//evaluate the expression
Object literal = evaluateLiteral( expression, target );
// handle geometry case
if (literal instanceof Geometry) {
// call this method for backwards compatibility with subclasses
visitLiteralGeometry(filterFactory.literal(literal));
}
else {
// write out the literal allowing subclasses to override this
// behaviour (for writing out dates and the like using the BDMS custom functions)
writeLiteral(literal);
}
} catch (IOException e) {
throw new FilterToElasticException("IO problems writing literal", e);
}
return context;
}
protected Object evaluateLiteral(Literal expression, Class target ) {
Object literal = null;
// HACK: let expression figure out the right value for numbers,
// since the context is almost always improperly set and the
// numeric converters try to force floating points to integrals
// JD: the above is no longer true, so instead do a safe conversion
if(target != null) {
// use the target type
if (Number.class.isAssignableFrom(target)) {
literal = safeConvertToNumber(expression, target);
if (literal == null) {
literal = safeConvertToNumber(expression, Number.class);
}
}
else {
literal = expression.evaluate(null, target);
}
}
//check for conversion to number
if (target == null) {
// we don't know the target type, check for a conversion to a number
Number number = safeConvertToNumber(expression, Number.class);
if (number != null) {
literal = number;
}
}
// if the target was not known, of the conversion failed, try the
// type guessing dance literal expression does only for the following
// method call
if(literal == null)
literal = expression.evaluate(null);
// if that failed as well, grab the value as is
if(literal == null)
literal = expression.getValue();
return literal;
}
/**
* Writes out a non null, non geometry literal. The base class properly handles
* null, numeric and booleans (true|false), and turns everything else into a string.
* Subclasses are expected to override this shall they need a different treatment
* (e.g. for dates)
* @param literal
*/
protected void writeLiteral(Object literal) {
field = literal;
if (Date.class.isAssignableFrom(literal.getClass())) {
field = dateFormatter.print(((Date) literal).getTime());
}
}
protected void visitLiteralTimePeriod(Period expression) {
throw new UnsupportedOperationException("Time periods not supported, subclasses must implement this " +
"method to support encoding timeperiods");
}
public Object visit(Add expression, Object extraData) {
throw new UnsupportedOperationException("Add expressions not supported");
}
public Object visit(Divide expression, Object extraData) {
throw new UnsupportedOperationException("Divide expressions not supported");
}
public Object visit(Multiply expression, Object extraData) {
throw new UnsupportedOperationException("Multiply expressions not supported");
}
public Object visit(Subtract expression, Object extraData) {
throw new UnsupportedOperationException("Subtract expressions not supported");
}
public Object visit(NilExpression expression, Object extraData) {
field = null;
return extraData;
}
//temporal filters, not supported
public Object visit(After after, Object extraData) {
return visitBinaryTemporalOperator(after, extraData);
}
public Object visit(AnyInteracts anyInteracts, Object extraData) {
return visitBinaryTemporalOperator(anyInteracts, extraData);
}
public Object visit(Before before, Object extraData) {
return visitBinaryTemporalOperator(before, extraData);
}
public Object visit(Begins begins, Object extraData) {
return visitBinaryTemporalOperator(begins, extraData);
}
public Object visit(BegunBy begunBy, Object extraData) {
return visitBinaryTemporalOperator(begunBy, extraData);
}
public Object visit(During during, Object extraData) {
return visitBinaryTemporalOperator(during, extraData);
}
public Object visit(EndedBy endedBy, Object extraData) {
return visitBinaryTemporalOperator(endedBy, extraData);
}
public Object visit(Ends ends, Object extraData) {
return visitBinaryTemporalOperator(ends, extraData);
}
public Object visit(Meets meets, Object extraData) {
return visitBinaryTemporalOperator(meets, extraData);
}
public Object visit(MetBy metBy, Object extraData) {
return visitBinaryTemporalOperator(metBy, extraData);
}
public Object visit(OverlappedBy overlappedBy, Object extraData) {
return visitBinaryTemporalOperator(overlappedBy, extraData);
}
public Object visit(TContains contains, Object extraData) {
return visitBinaryTemporalOperator(contains, extraData);
}
public Object visit(TEquals equals, Object extraData) {
return visitBinaryTemporalOperator(equals, extraData);
}
public Object visit(TOverlaps contains, Object extraData) {
return visitBinaryTemporalOperator(contains, extraData);
}
protected void visitLiteralGeometry(Literal expression) throws IOException {
// evaluate the literal and store it for later
currentGeometry = (Geometry) evaluateLiteral(expression, Geometry.class);
if ( currentGeometry instanceof LinearRing ) {
// convert LinearRing to LineString
final GeometryFactory factory = currentGeometry.getFactory();
final LinearRing linearRing = (LinearRing) currentGeometry;
final CoordinateSequence coordinates;
coordinates = linearRing.getCoordinateSequence();
currentGeometry = factory.createLineString(coordinates);
}
final String geoJson = new GeometryJSON().toString(currentGeometry);
currentShapeBuilder = mapReader.readValue(geoJson);
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter,
PropertyName property, Literal geometry, boolean swapped,
Object extraData) {
return helper.visitBinarySpatialOperator(filter, property, geometry,
swapped, extraData);
}
protected Object visitBinarySpatialOperator(BinarySpatialOperator filter, Expression e1,
Expression e2, Object extraData) {
return helper.visitBinarySpatialOperator(filter, e1, e2, extraData);
}
@Override
public Object visit(Function function, Object extraData) {
throw new UnsupportedOperationException("Function support not implemented");
}
// END IMPLEMENTING org.opengis.filter.ExpressionVisitor METHODS
protected void updateDateFormatter(AttributeDescriptor attType) {
dateFormatter = DEFAULT_DATE_FORMATTER;
if (attType != null) {
final String format = (String) attType.getUserData().get(DATE_FORMAT);
if (format != null) {
dateFormatter = Joda.forPattern(format).printer();
}
}
}
/*
* helper to do a safe convesion of expression to a number
*/
Number safeConvertToNumber(Expression expression, Class target) {
return (Number) Converters.convert(expression.evaluate(null), target,
new Hints(ConverterFactory.SAFE_CONVERSION, true));
}
protected void addViewParams(Query query) {
if (query.getHints() != null && query.getHints().get(Hints.VIRTUAL_TABLE_PARAMETERS) != null) {
parameters = (Map) query.getHints().get(Hints.VIRTUAL_TABLE_PARAMETERS);
nativeOnly = false;
for (final Map.Entry<String, String> entry : parameters.entrySet()) {
if (entry.getKey().equalsIgnoreCase("native-only")) {
nativeOnly = Boolean.valueOf(entry.getValue());
}
}
if (nativeOnly) {
LOGGER.fine("Ignoring GeoServer filter (Elasticsearch native query/post filter only)");
queryBuilder = MATCH_ALL;
}
for (final Map.Entry<String, String> entry : parameters.entrySet()) {
if (entry.getKey().equalsIgnoreCase("q")) {
final String value = entry.getValue();
try {
nativeQueryBuilder = mapReader.readValue(value);
} catch (Exception e) {
throw new FilterToElasticException("Unable to parse native query",e);
}
}
if (entry.getKey().equalsIgnoreCase("a")) {
final ObjectMapper mapper = new ObjectMapper();
try {
final TypeReference<Map<String, Map<String,Map<String,Object>>>> type;
type = new TypeReference<Map<String, Map<String,Map<String,Object>>>>() {};
this.aggregations = mapper.readValue(entry.getValue(), type);
} catch (Exception e) {
throw new FilterToElasticException("Unable to parse aggregation",e);
}
}
}
}
}
public static String convertToQueryString(char escape, char multi, char single,
boolean matchCase, String pattern ) {
StringBuffer result = new StringBuffer(pattern.length()+5);
for (int i = 0; i < pattern.length(); i++) {
char chr = pattern.charAt(i);
if (chr == escape) {
// emit the next char and skip it
if (i!= (pattern.length()-1) ) {
result.append("\\");
result.append( pattern.charAt(i+1) );
}
i++; // skip next char
} else if (chr == single) {
result.append('?');
} else if (chr == multi) {
result.append('*');
} else {
result.append(chr);
}
}
return result.toString();
}
public static String convertToRegex(char escape, char multi, char single,
boolean matchCase, String pattern) {
StringBuffer result = new StringBuffer(pattern.length()+5);
for (int i = 0; i < pattern.length(); i++) {
char chr = pattern.charAt(i);
if (chr == escape) {
// emit the next char and skip it
if (i!= (pattern.length()-1) ) {
result.append("\\");
result.append( pattern.charAt(i+1) );
}
i++; // skip next char
} else if (chr == single) {
result.append('.');
} else if (chr == multi) {
result.append(".*");
} else {
result.append(chr);
}
}
return result.toString();
}
private static String extractNestedPath(String field) {
final String[] parts = field.split("\\.");
final String base = parts[parts.length-1];
return field.replace("." + base, "");
}
public Boolean getFullySupported() {
return fullySupported;
}
public Map<String,Object> getNativeQueryBuilder() {
return nativeQueryBuilder;
}
public Map<String,Object> getQueryBuilder() {
final Map<String,Object> queryBuilder;
if (nativeQueryBuilder.equals(MATCH_ALL)) {
queryBuilder = this.queryBuilder;
} else if (this.queryBuilder.equals(MATCH_ALL)) {
queryBuilder = nativeQueryBuilder;
} else {
queryBuilder = ImmutableMap.of("bool",
ImmutableMap.of("must", ImmutableList.of(nativeQueryBuilder, this.queryBuilder)));
}
return queryBuilder;
}
public Map<String,Map<String,Map<String,Object>>> getAggregations() {
return aggregations;
}
}