/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2017, 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.mongodb.complex;
import com.mongodb.BasicDBList;
import com.mongodb.DBObject;
import org.geotools.data.mongodb.AbstractCollectionMapper;
import org.geotools.data.mongodb.MongoFeature;
import org.geotools.data.mongodb.MongoGeometryBuilder;
import org.opengis.feature.Feature;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* This class contains utilities methods for dealing with MongoDB complex features.
*/
public final class MongoComplexUtilities {
private MongoComplexUtilities() {
}
/**
* Will try to extract from the provided object the value that correspond to the given json path.
*/
public static Object getValue(Object object, String jsonPath) {
// let's make sure we have a feature
Feature feature = extractFeature(object, jsonPath);
if (feature instanceof MongoFeature) {
// no a nested element mongo feature
MongoFeature mongoFeature = (MongoFeature) feature;
return getValue(mongoFeature.getMongoObject(), jsonPath);
}
if (feature instanceof MongoCollectionFeature) {
// a mongo feature in the context of a nested element
MongoCollectionFeature collectionFeature = (MongoCollectionFeature) object;
return getValue(collectionFeature.getMongoFeature().getMongoObject(), collectionFeature.getCollectionsIndexes(), jsonPath);
}
// could not find a mongo feature, we can do nothing
throw invalidFeature(feature, jsonPath);
}
/**
* Method for extracting or casting a feature from the provided object.
*/
public static Feature extractFeature(Object feature, String jsonPath) {
// we should have a feature
if (!(feature instanceof Feature)) {
// not a feature so nothing to do
throw invalidFeature(feature, jsonPath);
}
// let's see if we have the a mongo feature in the user data
Object mongoFeature = ((Feature) feature).getUserData().get(AbstractCollectionMapper.MONGO_OBJECT_FEATURE_KEY);
// if we could not find a mongo feature in the user data we stick we the original feature
return mongoFeature == null ? (Feature) feature : (Feature) mongoFeature;
}
/**
* Helper method that creates an exception for when the provided object is not of the correct type.
*/
private static RuntimeException invalidFeature(Object feature, String jsonPath) {
return new RuntimeException(String.format(
"No possible to obtain a mongo object from '%s' to extract '%s'.",
feature.getClass(), jsonPath));
}
/**
* Will extract from the mongo db object the value that correspond to the given json path.
* If the path contain a nested list of values an exception will be throw.
*/
public static Object getValue(DBObject mongoObject, String jsonPath) {
return getValue(mongoObject, Collections.emptyMap(), jsonPath);
}
/**
* Will extract from the mongo db object the value that correspond to the given json path.
* The provided collections indexes will be used to select the proper element for the
* collections present in the path.
*/
public static Object getValue(DBObject mongoObject, Map<String, Integer> collectionsIndexes, String jsonPath) {
MongoObjectWalker walker = new MongoObjectWalker(mongoObject, collectionsIndexes, jsonPath);
// try to convert the founded value to a geometry
return convertGeometry(walker.getValue());
}
/**
* Helper method that checks if a mongodb value is a geometry and perform the proper conversion.
*/
private static Object convertGeometry(Object value) {
if (!(value instanceof DBObject) || value instanceof List) {
// not a mongodb object or a list of values so nothing to do
return value;
}
DBObject object = (DBObject) value;
Set keys = object.keySet();
if (keys.size() != 2 || !keys.contains("coordinates") || !keys.contains("type")) {
// is mongo db object but not a geometry
return value;
}
// we have a geometry so let's try to convert it
MongoGeometryBuilder builder = new MongoGeometryBuilder();
try {
// return the converted geometry
return builder.toGeometry(object);
} catch (Exception exception) {
// well could not convert the mongo db object to a geometry
}
return value;
}
/**
* Utility class class to extract information from a MongoDB object giving a certain path.
*/
private static final class MongoObjectWalker {
private final Map<String, Integer> collectionsIndexes;
private final String[] jsonPathParts;
private String currentJsonPath;
private int currentJsonPathPartIndex;
private Object currentObject;
MongoObjectWalker(DBObject mongoObject, Map<String, Integer> collectionsIndexes, String jsonPath) {
this.collectionsIndexes = collectionsIndexes;
this.jsonPathParts = jsonPath.split("\\.");
this.currentJsonPath = "";
this.currentJsonPathPartIndex = 0;
this.currentObject = mongoObject;
}
Object getValue() {
while (hasNext() && currentObject != null) {
next();
}
// end of the walked path or NULL value found
return currentObject;
}
private boolean hasNext() {
// we have a next element if we still have paths parts or we are currently
// walking a collection and there is a index defined for this collection
return currentJsonPathPartIndex < jsonPathParts.length
|| (currentObject instanceof BasicDBList && collectionsIndexes.get(currentJsonPath) != null);
}
private void next() {
// let's walk to the next path part using the current object
if (currentObject instanceof List) {
// the current object is a list, we need to select the current index
currentObject = next((List) currentObject);
} else if (currentObject instanceof DBObject) {
currentObject = next((DBObject) currentObject);
} else {
throw new RuntimeException(String.format(
"Trying to get data from a non MongoDB object, current json path is '%s'.", currentJsonPath));
}
}
private Object next(DBObject dbObject) {
// we have a mongo db object, let's update the current json path
currentJsonPath = concatPath(currentJsonPath, jsonPathParts[currentJsonPathPartIndex]);
// get the value from the mongo db object that correspond to the current json path part
Object result = dbObject.get(jsonPathParts[currentJsonPathPartIndex]);
currentJsonPathPartIndex++;
return result;
}
private Object next(List basicDBList) {
// let's find current index for this collection
Integer rawCollectionIndex = collectionsIndexes.get(currentJsonPath);
if (rawCollectionIndex == null && basicDBList.size() == 1) {
// this collection only has a single element and there is not index information for this collection
return basicDBList.get(0);
}
if (rawCollectionIndex == null) {
throw new RuntimeException(String.format(
"There is no index available for collection '%s'.", currentJsonPath));
}
// just return the list element that matches the current index
return basicDBList.get(rawCollectionIndex);
}
}
/**
* Will try to extract from the provided object all the values that correspond to the given json path.
*/
public static Object getValues(Object object, String jsonPath) {
// let's make sure we have a feature
Feature feature = extractFeature(object, jsonPath);
if (feature instanceof MongoFeature) {
// no a nested element mongo feature
MongoFeature mongoFeature = (MongoFeature) feature;
return getValues(mongoFeature.getMongoObject(), jsonPath);
}
if (feature instanceof MongoCollectionFeature) {
// a mongo feature in the context of a nested element
MongoCollectionFeature collectionFeature = (MongoCollectionFeature) object;
return getValues(collectionFeature.getMongoFeature().getMongoObject(), jsonPath);
}
// could not find a mongo feature, we can do nothing
throw invalidFeature(feature, jsonPath);
}
/**
* Will extract from the mongo db object all the values that correspond to the given json path.
* If the path contains nested collections the values from all the branches will be merged.
*/
public static Object getValues(DBObject dbObject, String jsonPath) {
if (jsonPath == null || jsonPath.isEmpty() || dbObject == null) {
// nothing to do here
return Collections.emptyList();
}
// let's split the json path in parts which will give us the necessary keys
String[] jsonPathParts = jsonPath.split("\\.");
// recursively get the values using an helper function
List<Object> values = getValuesHelper(dbObject, jsonPathParts, new ArrayList<>(), 0);
if (values.size() == 1) {
// we only have a single value, let's extract it
return values.get(0);
}
return values;
}
/**
* Helper function that will walk a mongo db object and retrieve all the values for a certain path.
*/
private static List<Object> getValuesHelper(DBObject dbObject, String[] jsonPathParts, List<Object> values, int index) {
// get the object corresponding to the current index
Object object = dbObject.get(jsonPathParts[index]);
if (object == null) {
// we are done
return values;
}
// check if we reach the end of the json path
boolean finalPath = index == jsonPathParts.length - 1;
index++;
if (object instanceof List) {
if (finalPath) {
// we reached the end of the json path and we have a list, so let's add all the elements of the list
for (Object value : (List) object) {
values.add(convertGeometry(value));
}
} else {
// well we have a list so we need to interact over each element of the list
for (Object element : (List) object) {
getValuesHelper((DBObject) element, jsonPathParts, values, index);
}
}
} else {
if (finalPath) {
// we reached the end of the json path so let's add this object
values.add(convertGeometry(object));
} else {
// we need to go deeper in this object
getValuesHelper((DBObject) object, jsonPathParts, values, index);
}
}
// we return the list of founded values for commodity
return values;
}
/**
* Simple method that adds an element ot a json path.
*/
private static String concatPath(String parentPath, String path) {
if (parentPath == null || parentPath.isEmpty()) {
// first element of the path
return path;
}
return parentPath + "." + path;
}
/**
* Compute the mappings for a mongo db object, this can be used to create a feature mapping.
*/
public static Map<String, Class> findMappings(DBObject dbObject) {
Map<String, Class> mappings = new HashMap<>();
findMappingsHelper(dbObject, "", mappings);
return mappings;
}
/**
* Helper method that will recursively walk a mongo db object and compute is mappings.
*/
private static void findMappingsHelper(Object object, String parentPath, Map<String, Class> mappings) {
if (object == null) {
return;
}
if (object instanceof DBObject) {
DBObject dbObject = (DBObject) object;
for (String key : dbObject.keySet()) {
Object value = dbObject.get(key);
if (value == null) {
continue;
}
String path = concatPath(parentPath, key);
if (value instanceof List) {
List list = (List) value;
if (!list.isEmpty()) {
findMappingsHelper(list.get(0), path, mappings);
}
} else if (value instanceof DBObject) {
findMappingsHelper(value, path, mappings);
} else {
mappings.put(path, value.getClass());
}
}
} else {
mappings.put(parentPath, object.getClass());
}
}
}