/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2015, Open Source Geospatial Foundation (OSGeo) * (C) 2014-2015, Boundless * * 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.mongodb; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; import com.vividsolutions.jts.geom.Envelope; import com.vividsolutions.jts.geom.Geometry; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import java.util.regex.Pattern; import org.bson.types.ObjectId; import org.geotools.data.mongodb.complex.JsonSelectAllFunction; import org.geotools.data.mongodb.complex.JsonSelectFunction; import org.geotools.util.Converters; import org.geotools.util.logging.Logging; import static org.geotools.util.Converters.convert; import org.opengis.feature.simple.SimpleFeatureType; import org.opengis.feature.type.AttributeDescriptor; import org.opengis.feature.type.PropertyDescriptor; import org.opengis.filter.And; import org.opengis.filter.BinaryComparisonOperator; import org.opengis.filter.ExcludeFilter; import org.opengis.filter.Filter; 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.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.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.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; /** * Visitor responsible for generating a BasicDBObject to use as a MongoDB query. * * @author Gerald Gay, Data Tactics Corp. * @author Alan Mangan, Data Tactics Corp. * @author Tom Kunicki, Boundless Spatial Inc. * @source $URL$ (C) 2011, Open Source Geospatial Foundation (OSGeo) * @see The GNU Lesser General Public License (LGPL) */ public class FilterToMongo implements FilterVisitor, ExpressionVisitor { private static final Logger LOGGER = Logging.getLogger(FilterToMongo.class); static final SimpleDateFormat ISO8601_SDF = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); final CollectionMapper mapper; final MongoGeometryBuilder geometryBuilder; /** The schmema the encoder will use as reference to drive filter encoding */ SimpleFeatureType featureType; public FilterToMongo(CollectionMapper mapper) { this(mapper, new MongoGeometryBuilder()); } public FilterToMongo(CollectionMapper mapper, MongoGeometryBuilder geometryBuilder) { this.mapper = mapper; this.geometryBuilder = geometryBuilder; } protected BasicDBObject asDBObject(Object extraData) { if ((extraData != null) || (extraData instanceof BasicDBObject)) { return (BasicDBObject) extraData; } return new BasicDBObject(); } /** * Sets the feature type the encoder is encoding a filter for. * <p> * The type of the attributes may drive how the filter is translated to a mongodb query document. * </p> * * @param featureType */ public void setFeatureType(SimpleFeatureType featureType) { this.featureType = featureType; } // // primitives // @Override public Object visit(Literal expression, Object extraData) { Class<?> targetType = null; if (extraData != null && extraData instanceof Class) { targetType = (Class<?>) extraData; } return encodeLiteral(expression.getValue(), targetType); } @Override public Object visit(PropertyName expression, Object extraData) { String prop = expression.getPropertyName(); if (extraData == Geometry.class) { return mapper.getGeometryPath(); } return mapper.getPropertyPath(prop); } @Override public Object visit(ExcludeFilter filter, Object extraData) { BasicDBObject output = asDBObject(extraData); output.put("foo", "not_likely_to_exist"); return output; } // An empty object should be an "all" query @Override public Object visit(IncludeFilter filter, Object extraData) { return new BasicDBObject(); } // // logical // @Override public Object visit(And filter, Object extraData) { BasicDBObject output = asDBObject(extraData); List<Filter> children = filter.getChildren(); BasicDBList andList = new BasicDBList(); if (children != null) { for (Filter child : children) { BasicDBObject item = (BasicDBObject) child.accept(this, null); andList.add(item); } output.put("$and", andList); } return output; } @Override public Object visit(Or filter, Object extraData) { BasicDBObject output = asDBObject(extraData); List<Filter> children = filter.getChildren(); BasicDBList orList = new BasicDBList(); if (children != null) { for (Filter child : children) { BasicDBObject item = (BasicDBObject) child.accept(this, null); orList.add(item); } output.put("$or", orList); } return output; } @Override public Object visit(Not filter, Object extraData) { BasicDBObject output = asDBObject(extraData); BasicDBObject expr = (BasicDBObject) filter.getFilter().accept(this, null); output.put("$not", expr); return output; } // // comparison // @Override public Object visit(PropertyIsBetween filter, Object extraData) { BasicDBObject output = asDBObject(extraData); String propName = convert(filter.getExpression().accept(this, null), String.class); Object lower = filter.getLowerBoundary().accept(this, getValueType(filter.getExpression())); Object upper = filter.getUpperBoundary().accept(this, getValueType(filter.getExpression())); BasicDBObject dbo = new BasicDBObject(); dbo.put("$gte", lower); dbo.put("$lte", upper); output.put(propName, dbo); return output; } @Override public Object visit(PropertyIsEqualTo filter, Object extraData) { return encodeBinaryComparisonOp(filter, null, extraData); } BasicDBObject encodeBinaryComparisonOp(BinaryComparisonOperator filter, String op, Object extraData) { BasicDBObject output = asDBObject(extraData); Expression left = filter.getExpression1(); Expression right = filter.getExpression2(); Class<?> leftValueType = getValueType(right), rightValueType = getValueType(left); Object leftValue = filter.getExpression1().accept(this, leftValueType); Object rightValue = filter.getExpression2().accept(this, rightValueType); if (rightValue instanceof String && !(leftValue instanceof String)) { // reverse Object tmp = leftValue; leftValue = rightValue; rightValue = tmp; } output.put((String) leftValue, op == null ? rightValue : new BasicDBObject(op, rightValue)); return output; } private Class getJsonSelectType(Expression expression) { if (expression instanceof JsonSelectFunction) { PropertyDescriptor descriptor = featureType.getDescriptor(((JsonSelectFunction) expression).getJsonPath()); return descriptor == null ? null : descriptor.getType().getBinding(); } if (expression instanceof JsonSelectAllFunction) { PropertyDescriptor descriptor = featureType.getDescriptor(((JsonSelectAllFunction) expression).getJsonPath()); return descriptor == null ? null : descriptor.getType().getBinding(); } return null; } private Class<?> getValueType(Expression e) { Class<?> valueType = null; valueType = getJsonSelectType(e); if (valueType != null) { return valueType; } if (e instanceof PropertyName && featureType != null) { // we should get the value type from the correspondent attribute descriptor AttributeDescriptor attType = (AttributeDescriptor) e.evaluate(featureType); if (attType != null) { valueType = attType.getType().getBinding(); } } else if (e instanceof Function) { // get the value type from the function return type Class<?> ret = getFunctionReturnType((Function) e); if (ret != null) { valueType = ret; } } return valueType; } private Class<?> getFunctionReturnType(Function f) { Class<?> clazz = Object.class; if (f.getFunctionName() != null && f.getFunctionName().getReturn() != null) { clazz = f.getFunctionName().getReturn().getType(); } if (clazz == Object.class) { clazz = null; } return clazz; } @Override public Object visit(PropertyIsNotEqualTo filter, Object extraData) { return encodeBinaryComparisonOp(filter, "$ne", extraData); } @Override public Object visit(PropertyIsGreaterThan filter, Object extraData) { return encodeBinaryComparisonOp(filter, "$gt", extraData); } @Override public Object visit(PropertyIsGreaterThanOrEqualTo filter, Object extraData) { return encodeBinaryComparisonOp(filter, "$gte", extraData); } @Override public Object visit(PropertyIsLessThan filter, Object extraData) { return encodeBinaryComparisonOp(filter, "$lt", extraData); } @Override public Object visit(PropertyIsLessThanOrEqualTo filter, Object extraData) { return encodeBinaryComparisonOp(filter, "$lte", extraData); } /** * Encode LIKE using MongoDB Regex. * * <ul> * <li>filter.getWildCard() returns SQL-like '%'</li> * <li>filter.getSingleChar() returns SQL-like '_'</li> * </ul> * As an example "foo_bar%" converts to foo.bar.* */ @Override public Object visit(PropertyIsLike filter, Object extraData) { BasicDBObject output = asDBObject(extraData); Expression filterExpression = filter.getExpression(); // Mongo's $regex operator only works on fields if (!(filterExpression instanceof PropertyName)) { throw new UnsupportedOperationException("LIKE only works with propertyName"); } String expr = convert(filterExpression.accept(this, null), String.class); String multi = filter.getWildCard(); String single = filter.getSingleChar(); int flags = (filter.isMatchingCase()) ? 0 : Pattern.CASE_INSENSITIVE; String regex = filter.getLiteral().replace(multi, ".*").replace(single, "."); // force full string match regex = "^" + regex + "$"; Pattern p = Pattern.compile(regex, flags); output.put((String) expr, p); return output; } // There is no "NULL" in MongoDB, but I assume that TODO add null support // the non-existence of a column is the same... @Override public Object visit(PropertyIsNull filter, Object extraData) { BasicDBObject output = asDBObject(extraData); String prop = convert(filter.accept(this, null), String.class); output.put(prop, new BasicDBObject("$exists", false)); return output; } @Override public Object visit(Id filter, Object extraData) { BasicDBObject output = asDBObject(extraData); Set<Identifier> ids = filter.getIdentifiers(); List<ObjectId> objectIds = new ArrayList<ObjectId>(ids.size()); for (Identifier id : ids) { objectIds.add(new ObjectId(id.toString())); } Object objectIdDBO = (objectIds.size() > 1) ? new BasicDBObject("$in", objectIds) : objectIds.get(0); output.put("_id", objectIdDBO); return output; } // // spatial // @Override public Object visit(BBOX filter, Object extraData) { BasicDBObject output = asDBObject(extraData); // TODO: handle swapping of operands Object e1 = filter.getExpression1().accept(this, Geometry.class); Envelope envelope = filter.getExpression2().evaluate(null, Envelope.class); DBObject geometryDBObject = geometryBuilder.toObject(envelope); addCrsToGeometryDBObject(geometryDBObject); DBObject dbo = BasicDBObjectBuilder.start().push("$geoIntersects") .add("$geometry", geometryDBObject).get(); output.put((String) e1, dbo); return output; } @Override public Object visit(Intersects filter, Object extraData) { BasicDBObject output = asDBObject(extraData); // TODO: handle swapping of operands Object e1 = filter.getExpression1().accept(this, Geometry.class); Geometry geometry = filter.getExpression2().evaluate(null, Geometry.class); DBObject geometryDBObject = geometryBuilder.toObject(geometry); addCrsToGeometryDBObject(geometryDBObject); DBObject dbo = BasicDBObjectBuilder.start().push("$geoIntersects") .add("$geometry", geometryDBObject).get(); output.put((String) e1, dbo); return output; } @Override public Object visit(Within filter, Object extraData) { BasicDBObject output = asDBObject(extraData); // TODO: handle swapping of operands Object e1 = filter.getExpression1().accept(this, Geometry.class); Geometry geometry = filter.getExpression2().evaluate(null, Geometry.class); DBObject geometryDBObject = geometryBuilder.toObject(geometry); addCrsToGeometryDBObject(geometryDBObject); DBObject dbo = BasicDBObjectBuilder.start().push("$geoWithin") .add("$geometry", geometryDBObject).get(); output.put((String) e1, dbo); return output; } // // currently unsupported // @Override public Object visitNullFilter(Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(NilExpression expression, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Beyond filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Contains filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Crosses filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Disjoint filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(DWithin filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Equals filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Overlaps filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Touches filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Add expression, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Divide expression, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Function function, Object extraData) { if (function instanceof JsonSelectFunction) { return ((JsonSelectFunction) function).getJsonPath(); } if (function instanceof JsonSelectAllFunction) { return ((JsonSelectAllFunction) function).getJsonPath(); } throw new UnsupportedOperationException(); } @Override public Object visit(Multiply expression, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Subtract expression, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(PropertyIsNil filter, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(After after, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(AnyInteracts anyInteracts, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Before before, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Begins begins, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(BegunBy begunBy, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(During during, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(EndedBy endedBy, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Ends ends, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(Meets meets, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(MetBy metBy, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(OverlappedBy overlappedBy, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(TContains contains, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(TEquals equals, Object extraData) { throw new UnsupportedOperationException(); } @Override public Object visit(TOverlaps contains, Object extraData) { throw new UnsupportedOperationException(); } Object encodeLiteral(Object literal, Class<?> targetType) { if (literal instanceof Envelope) { return geometryBuilder.toObject((Envelope) literal); } else if (literal instanceof Geometry) { return geometryBuilder.toObject((Geometry) literal); } else if (literal instanceof Date) { if (targetType != null && Date.class.isAssignableFrom(targetType)) { // return date object as is, will be correctly encoded by BasicDBObject return literal; } // by default, convert date to ISO-8601 string return ISO8601_SDF.format((Date) literal); } else if (literal instanceof String) { if (targetType != null && Date.class.isAssignableFrom(targetType)) { // try parse string assuming it's ISO-8601 formatted try { return ISO8601_SDF.parse((String) literal); } catch (ParseException e) { LOGGER.log(Level.WARNING, "Could not parse String literal as ISO-8601 date", e); } } // try to convert to the expected type return convertLiteral(literal, targetType); } else { // try to convert to the expected type return convertLiteral(literal, targetType); } } /** * Helper method that tries to convert a literal to the expected type. */ private Object convertLiteral(Object literal, Class<?> targetType) { if (literal == null || targetType == null) { // return the literal as is return literal; } Object converted = Converters.convert(literal, targetType); if (converted == null) { // no conversion found return the literal as string return literal.toString(); } // return the converted value return converted; } void addCrsToGeometryDBObject(DBObject geometryDBObject) { geometryDBObject.put("crs", BasicDBObjectBuilder.start().add("type", "name") .push("properties").add("name", "urn:x-mongodb:crs:strictwinding:EPSG:4326").get()); } }