/*
* 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 java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.data.DataUtilities;
import org.geotools.data.FeatureReader;
import org.geotools.data.FilteringFeatureReader;
import org.geotools.data.Query;
import org.geotools.data.ReTypeFeatureReader;
import org.geotools.data.mongodb.complex.JsonSelectAllFunction;
import org.geotools.data.mongodb.complex.JsonSelectFunction;
import org.geotools.data.store.ContentEntry;
import org.geotools.data.store.ContentFeatureSource;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.visitor.PostPreProcessFilterSplittingVisitor;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.util.logging.Logging;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.filter.BinaryComparisonOperator;
import org.opengis.filter.Filter;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.expression.Expression;
import org.opengis.filter.expression.Literal;
import org.opengis.filter.expression.PropertyName;
import org.opengis.filter.sort.SortBy;
import org.opengis.filter.sort.SortOrder;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
public class MongoFeatureSource extends ContentFeatureSource {
static Logger LOG = Logging.getLogger("org.geotools.data.mongodb");
final DBCollection collection;
CollectionMapper mapper;
public MongoFeatureSource(ContentEntry entry, Query query, DBCollection collection) {
super(entry, query);
this.collection = collection;
initMapper();
}
final void initMapper() {
// use schema with mapping info if it exists
SimpleFeatureType type = entry.getState(null).getFeatureType();
setMapper(type != null ? new MongoSchemaMapper(type) : new MongoInferredMapper());
}
public DBCollection getCollection() {
return collection;
}
public CollectionMapper getMapper() {
return mapper;
}
public void setMapper(CollectionMapper mapper) {
this.mapper = mapper;
}
@Override
protected SimpleFeatureType buildFeatureType() throws IOException {
SimpleFeatureType type = mapper.buildFeatureType(entry.getName(), collection);
getDataStore().getSchemaStore().storeSchema(type);
return type;
}
@Override
public MongoDataStore getDataStore() {
return (MongoDataStore) super.getDataStore();
}
@Override
protected ReferencedEnvelope getBoundsInternal(Query query) throws IOException {
// TODO: crs?
FeatureReader<SimpleFeatureType, SimpleFeature> r = getReader(query);
try {
ReferencedEnvelope e = new ReferencedEnvelope();
if (r.hasNext()) {
e.init(r.next().getBounds());
}
while (r.hasNext()) {
e.include(r.next().getBounds());
}
return e;
} finally {
r.close();
}
}
@Override
protected int getCountInternal(Query query) throws IOException {
Filter f = query.getFilter();
if (isAll(f)) {
LOG.fine("count(all)");
return (int) collection.count();
}
Filter[] split = splitFilter(f);
if (!isAll(split[1])) {
return -1;
}
DBObject q = toQuery(f);
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("count(" + q + ")");
}
return (int) collection.count(q);
}
@Override
protected FeatureReader<SimpleFeatureType, SimpleFeature> getReaderInternal(Query query)
throws IOException {
List<Filter> postFilterList = new ArrayList<Filter>();
List<String> postFilterAttributes = new ArrayList<String>();
DBCursor cursor = toCursor(query, postFilterList, postFilterAttributes);
FeatureReader<SimpleFeatureType, SimpleFeature> r = new MongoFeatureReader(cursor, this);
if (!postFilterList.isEmpty() && !isAll(postFilterList.get(0))) {
r = new FilteringFeatureReader<SimpleFeatureType, SimpleFeature>(r, postFilterList.get(0));
// check whether attributes not present in the original query have been
// added to the set of retrieved attributes for the sake of
// post-filters; if so, wrap the FeatureReader in a ReTypeFeatureReader
if (!postFilterAttributes.isEmpty()) {
// build the feature type returned by this query
SimpleFeatureType returnedSchema = SimpleFeatureTypeBuilder.retype(
getSchema(), query.getPropertyNames());
r = new ReTypeFeatureReader(r, returnedSchema);
}
}
return r;
}
@Override
protected boolean canOffset() {
return true;
}
@Override
protected boolean canLimit() {
return true;
}
@Override
protected boolean canRetype() {
return true;
}
@Override
protected boolean canSort() {
return true;
}
@Override
protected boolean canFilter() {
return true;
}
DBCursor toCursor(Query q, List<Filter> postFilter, List<String> postFilterAttrs) {
DBObject query = new BasicDBObject();
Filter f = q.getFilter();
if (!isAll(f)) {
Filter[] split = splitFilter(f);
query = toQuery(split[0]);
if (!isAll(split[1])) {
postFilter.add(split[1]);
}
}
DBCursor c;
if (q.getPropertyNames() != Query.ALL_NAMES) {
BasicDBObject keys = new BasicDBObject();
for (String p : q.getPropertyNames()) {
keys.put(mapper.getPropertyPath(p), 1);
}
// add properties from post filters
for (Filter filter: postFilter) {
String[] attributeNames = DataUtilities.attributeNames(filter);
for (String attrName: attributeNames) {
if (attrName != null && !attrName.isEmpty() && !keys.containsField(attrName)) {
keys.put(mapper.getPropertyPath(attrName), 1);
postFilterAttrs.add(attrName);
}
}
}
if (!keys.containsField(mapper.getGeometryPath())) {
keys.put(mapper.getGeometryPath(), 1);
}
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(String.format("find(%s, %s)", query, keys));
}
c = collection.find(query, keys);
} else {
if (LOG.isLoggable(Level.FINE)) {
LOG.fine(String.format("find(%s)", query));
}
c = collection.find(query);
}
if (q.getStartIndex() != null && q.getStartIndex() != 0) {
c = c.skip(q.getStartIndex());
}
if (q.getMaxFeatures() != Integer.MAX_VALUE) {
c = c.limit(q.getMaxFeatures());
}
if (q.getSortBy() != null) {
BasicDBObject orderBy = new BasicDBObject();
for (SortBy sortBy : q.getSortBy()) {
String propName = sortBy.getPropertyName().getPropertyName();
orderBy.append(propName, sortBy.getSortOrder() == SortOrder.ASCENDING ? 1 : -1);
}
c = c.sort(orderBy);
}
return c;
}
DBObject toQuery(Filter f) {
if (isAll(f)) {
return new BasicDBObject();
}
FilterToMongo v = new FilterToMongo(mapper);
v.setFeatureType(getSchema());
return (DBObject) f.accept(v, null);
}
boolean isAll(Filter f) {
return f == null || f == Filter.INCLUDE;
}
@SuppressWarnings("deprecation")
Filter[] splitFilter(Filter f) {
PostPreProcessFilterSplittingVisitor splitter = new PostPreProcessFilterSplittingVisitor(
getDataStore().getFilterCapabilities(), null, null){
@Override
protected void visitBinaryComparisonOperator(BinaryComparisonOperator filter) {
Expression expression1 = filter.getExpression1();
Expression expression2 = filter.getExpression2();
if ((expression1 instanceof JsonSelectFunction
|| expression1 instanceof JsonSelectAllFunction) && expression2 instanceof Literal) {
preStack.push(filter);
} else if ((expression2 instanceof JsonSelectFunction
|| expression2 instanceof JsonSelectAllFunction) && expression1 instanceof Literal) {
preStack.push(filter);
}
}
public Object visit(PropertyIsLike filter, Object notUsed) {
if (original == null)
original = filter;
if (!fcs.supports(PropertyIsLike.class)) {
// MongoDB can only encode like expressions using propertyName
postStack.push(filter);
return null;
}
if(!(filter.getExpression() instanceof PropertyName)){
// MongoDB can only encode like expressions using propertyName
postStack.push(filter);
return null;
}
int i = postStack.size();
filter.getExpression().accept(this, null);
if (i < postStack.size()) {
postStack.pop();
postStack.push(filter);
return null;
}
preStack.pop(); // value
preStack.push(filter);
return null;
}
};
f.accept(splitter, null);
return new Filter[] { splitter.getFilterPre(), splitter.getFilterPost() };
}
}