/*
* 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.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.MongoClient;
import com.mongodb.MongoClientURI;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import org.geotools.data.FeatureWriter;
import org.geotools.data.Transaction;
import org.geotools.data.store.ContentDataStore;
import org.geotools.data.store.ContentEntry;
import org.geotools.data.store.ContentFeatureSource;
import org.geotools.data.store.ContentState;
import org.geotools.feature.simple.SimpleFeatureTypeBuilder;
import org.geotools.filter.FilterCapabilities;
import org.geotools.referencing.CRS;
import org.geotools.referencing.crs.DefaultGeographicCRS;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;
import org.opengis.feature.type.AttributeDescriptor;
import org.opengis.feature.type.Name;
import org.opengis.filter.And;
import org.opengis.filter.Filter;
import org.opengis.filter.Id;
import org.opengis.filter.Not;
import org.opengis.filter.PropertyIsBetween;
import org.opengis.filter.PropertyIsLike;
import org.opengis.filter.PropertyIsNull;
import org.opengis.filter.spatial.BBOX;
import org.opengis.filter.spatial.Intersects;
import org.opengis.filter.spatial.Within;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
public class MongoDataStore extends ContentDataStore {
final static String KEY_mapping = "mapping";
final static String KEY_encoding = "encoding";
final static String KEY_collection = "collection";
final MongoSchemaStore schemaStore;
final MongoClient dataStoreClient;
final DB dataStoreDB;
@SuppressWarnings("deprecation")
FilterCapabilities filterCapabilities;
public MongoDataStore(String dataStoreURI) {
this(dataStoreURI, null);
}
public MongoDataStore(String dataStoreURI, String schemaStoreURI) {
this(dataStoreURI, schemaStoreURI, true);
}
public MongoDataStore(String dataStoreURI, String schemaStoreURI, boolean createDatabaseIfNeeded) {
MongoClientURI dataStoreClientURI = createMongoClientURI(dataStoreURI);
dataStoreClient = createMongoClient(dataStoreClientURI);
dataStoreDB = createDB(dataStoreClient, dataStoreClientURI.getDatabase(), !createDatabaseIfNeeded);
if (dataStoreDB == null) {
dataStoreClient.close(); // This smells bad...
throw new IllegalArgumentException("Unknown mongodb database, \"" + dataStoreClientURI.getDatabase() + "\"");
}
schemaStore = createSchemaStore(schemaStoreURI);
if (schemaStore == null) {
dataStoreClient.close(); // This smells bad too...
throw new IllegalArgumentException("Unable to initialize schema store with URI \"" + schemaStoreURI + "\"");
}
filterCapabilities = createFilterCapabilties();
}
final MongoClientURI createMongoClientURI(String dataStoreURI) {
if (dataStoreURI == null) {
throw new IllegalArgumentException("dataStoreURI may not be null");
}
if (!dataStoreURI.startsWith("mongodb://")) {
throw new IllegalArgumentException("incorrect scheme for URI, expected to begin with \"mongodb://\", found URI of \"" + dataStoreURI + "\"");
}
return new MongoClientURI(dataStoreURI.toString());
}
final MongoClient createMongoClient(MongoClientURI mongoClientURI) {
try {
return new MongoClient(mongoClientURI);
} catch (Exception e) {
throw new IllegalArgumentException("Unknown mongodb host(s)", e);
}
}
final DB createDB(MongoClient mongoClient, String databaseName, boolean databaseMustExist) {
if (databaseMustExist && !mongoClient.getDatabaseNames().contains(databaseName)) {
return null;
}
return mongoClient.getDB(databaseName);
}
private MongoSchemaStore createSchemaStore(String schemaStoreURI) {
if (schemaStoreURI.startsWith("file:")) {
try {
return new MongoSchemaFileStore(schemaStoreURI);
} catch (URISyntaxException e) {
LOGGER.log(Level.SEVERE, "Unable to create file-based schema store with URI \"" + schemaStoreURI + "\"", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Unable to create file-based schema store with URI \"" + schemaStoreURI + "\"", e);
}
} else if (schemaStoreURI.startsWith("mongodb:")) {
try {
return new MongoSchemaDBStore(schemaStoreURI);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Unable to create mongodb-based schema store with URI \"" + schemaStoreURI + "\"", e);
}
} else {
try {
return new MongoSchemaFileStore("file:" + schemaStoreURI);
} catch (URISyntaxException e) {
LOGGER.log(Level.SEVERE, "Unable to create file-based schema store with URI \"" + schemaStoreURI + "\"", e);
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Unable to create file-based schema store with URI \"" + schemaStoreURI + "\"", e);
}
}
LOGGER.log(Level.SEVERE, "Unsupported URI \"{0}\" for schema store", schemaStoreURI);
return null;
}
@SuppressWarnings("deprecation")
final FilterCapabilities createFilterCapabilties() {
FilterCapabilities capabilities = new FilterCapabilities();
/* disable FilterCapabilities.LOGICAL_OPENGIS since it contains
Or.class (in addtions to And.class and Not.class. MongodB 2.4
doesn't supprt '$or' with spatial operations.
*/
// capabilities.addAll(FilterCapabilities.LOGICAL_OPENGIS);
capabilities.addType(And.class);
capabilities.addType(Not.class);
capabilities.addAll(FilterCapabilities.SIMPLE_COMPARISONS_OPENGIS);
capabilities.addType(PropertyIsNull.class);
capabilities.addType(PropertyIsBetween.class);
capabilities.addType(PropertyIsLike.class);
capabilities.addType(BBOX.class);
capabilities.addType(Intersects.class);
capabilities.addType(Within.class);
capabilities.addType(Id.class);
/*
capabilities.addType(IncludeFilter.class);
capabilities.addType(ExcludeFilter.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);*/
return capabilities;
}
public FilterCapabilities getFilterCapabilities() {
return filterCapabilities;
}
@Override
public void createSchema(SimpleFeatureType incoming) throws IOException {
final String geometryMapping = "geometry";
CoordinateReferenceSystem incomingCRS = incoming.getCoordinateReferenceSystem();
if (incomingCRS == null) {
incoming.getGeometryDescriptor().getCoordinateReferenceSystem();
}
if (!CRS.equalsIgnoreMetadata(incomingCRS, DefaultGeographicCRS.WGS84)) {
throw new IllegalArgumentException("Unsupported coordinate reference system, only WGS84 supported");
}
// Need to generate FeatureType instance with proper namespace URI
SimpleFeatureTypeBuilder builder = new SimpleFeatureTypeBuilder();
builder.init(incoming);
builder.setName(name(incoming.getTypeName()));
incoming = builder.buildFeatureType();
String gdName = incoming.getGeometryDescriptor().getLocalName();
for (AttributeDescriptor ad : incoming.getAttributeDescriptors()) {
String adName = ad.getLocalName();
if (gdName.equals(adName)) {
ad.getUserData().put(KEY_mapping, geometryMapping);
ad.getUserData().put(KEY_encoding, "GeoJSON");
} else {
ad.getUserData().put(KEY_mapping, "properties." + adName );
}
}
// pre-populating this makes view creation easier...
incoming.getUserData().put(KEY_collection, incoming.getTypeName());
// Collection needs to exist (with index) so that it's returned with createTypeNames()
dataStoreDB.createCollection(incoming.getTypeName(), new BasicDBObject()).createIndex(new BasicDBObject(geometryMapping, "2dsphere"));
// Store FeatureType instance since it can't be inferred (no documents)
ContentEntry entry = entry (incoming.getName());
ContentState state = entry.getState(null);
state.setFeatureType(incoming);
schemaStore.storeSchema(incoming);
}
private String collectionNameFromType(SimpleFeatureType type) {
String collectionName = (String)type.getUserData().get(KEY_collection);
return collectionName != null ? collectionName : type.getTypeName();
}
@Override
protected List<Name> createTypeNames() throws IOException {
Set<String> collectionNames = new LinkedHashSet<String>(dataStoreDB.getCollectionNames());
Set<String> typeNameSet = new LinkedHashSet<String>();
for (String candidateTypeName : getSchemaStore().typeNames()) {
try {
SimpleFeatureType candidateSchema = getSchemaStore().retrieveSchema(name(candidateTypeName));
// extract collection that schema maps to either from user data attribute
// or, if that's missing, the schema type name.
String candidateCollectionName = collectionNameFromType(candidateSchema);
// verify collection exists in db and has geometry index
if (collectionNames.contains(candidateCollectionName)) {
// verify geometry exists and has mapping.
String geometryName = candidateSchema.getGeometryDescriptor().getLocalName();
String geometryMapping = (String)candidateSchema.getDescriptor(geometryName).getUserData().get(KEY_mapping);
if (geometryMapping != null) {
DBCollection collection = dataStoreDB.getCollection(candidateCollectionName);
Set<String> geometryIndices = MongoUtil.findIndexedGeometries(
collection);
// verify geometry mapping is indexed...
if (geometryIndices.contains(geometryMapping)) {
typeNameSet.add(candidateTypeName);
} else {
LOGGER.log(Level.WARNING, "Ignoring type \"{0}\", the geometry attribute, \"{1}\", is mapped to document key \"{2}\" but it is not spatialy indexed in collection {3}",
new Object[] { name(candidateTypeName), geometryName, geometryMapping, collection.getFullName()});
}
} else {
LOGGER.log(Level.WARNING, "Ignoring type \"{0}\", the geometry attribute \"{1}\" is not mapped to a document key",
new Object[] { name(candidateTypeName), geometryName});
}
} else {
LOGGER.log(Level.WARNING, "Ignoring type \"{0}\", the collection it maps \"{1}.{2}\" does not exist",
new Object[] { name(candidateTypeName), dataStoreDB.getName(), candidateCollectionName});
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Ignoring type \"{0}\", an exception was thrown while attempting to retrieve the schema: {1}",
new Object[] { name(candidateTypeName), e});
}
}
// Create set of collections w/o named schema
Collection<String> collectionsToCheck = new LinkedList<String>(collectionNames);
collectionsToCheck.removeAll(typeNameSet);
// Check collection set to see if we can use any of them
for(String collectionName : collectionsToCheck) {
// make sure it's not system collection
if (!collectionName.startsWith("system.")) {
DBCollection collection = dataStoreDB.getCollection(collectionName);
Set<String> geometryIndexSet = MongoUtil.findIndexedGeometries(
collection);
// verify collection has an indexed geometry property
if(!geometryIndexSet.isEmpty()) {
typeNameSet.add(collectionName);
} else {
LOGGER.log(Level.INFO, "Ignoring collection \"{0}\", unable to find key with spatial index",
new Object[] { collection.getFullName() });
}
}
}
List<Name> typeNameList = new ArrayList<Name>();
for (String name : typeNameSet) {
typeNameList.add(name(name));
}
return typeNameList;
}
@Override
protected ContentFeatureSource createFeatureSource(ContentEntry entry) throws IOException {
ContentState state = entry.getState(null);
SimpleFeatureType type = state.getFeatureType();
if (type == null) {
type = schemaStore.retrieveSchema(entry.getName());
if (type != null) {
state.setFeatureType(type);
}
}
String collectionName = type != null ?
collectionNameFromType(type) :
entry.getTypeName();
return new MongoFeatureStore(entry, null, dataStoreDB.getCollection(collectionName));
}
@Override
public FeatureWriter<SimpleFeatureType, SimpleFeature> getFeatureWriter(
String typeName, Filter filter, Transaction tx) throws IOException {
if (tx != Transaction.AUTO_COMMIT) {
throw new IllegalArgumentException("Transactions not currently supported");
}
return super.getFeatureWriter(typeName, filter, tx);
}
@Override
protected ContentState createContentState(ContentEntry entry) {
ContentState state = super.createContentState(entry);
try {
SimpleFeatureType type = schemaStore.retrieveSchema(entry.getName());
if (type != null) {
state.setFeatureType(type);
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception thrown while attempting to retrieve the schema for {0}: {1}",
new Object[] {entry.getName(), e});
}
return state;
}
final MongoSchemaStore getSchemaStore() {
return schemaStore;
}
@Override
public void dispose() {
dataStoreClient.close();
schemaStore.close();
super.dispose();
}
}