MongoComplexUtilities.java (geotools-24.0-project) | : | MongoComplexUtilities.java (geotools-24.1-project) | ||
---|---|---|---|---|
skipping to change at line 19 | skipping to change at line 19 | |||
* License as published by the Free Software Foundation; | * License as published by the Free Software Foundation; | |||
* version 2.1 of the License. | * version 2.1 of the License. | |||
* | * | |||
* This library is distributed in the hope that it will be useful, | * This library is distributed in the hope that it will be useful, | |||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
* Lesser General Public License for more details. | * Lesser General Public License for more details. | |||
*/ | */ | |||
package org.geotools.data.mongodb.complex; | package org.geotools.data.mongodb.complex; | |||
import static org.geotools.referencing.CRS.findMathTransform; | ||||
import com.mongodb.BasicDBList; | import com.mongodb.BasicDBList; | |||
import com.mongodb.DBCursor; | import com.mongodb.DBCursor; | |||
import com.mongodb.DBObject; | import com.mongodb.DBObject; | |||
import java.util.ArrayList; | import java.util.ArrayList; | |||
import java.util.Collections; | import java.util.Collections; | |||
import java.util.HashMap; | import java.util.HashMap; | |||
import java.util.List; | import java.util.List; | |||
import java.util.Map; | import java.util.Map; | |||
import java.util.Set; | import java.util.Set; | |||
import java.util.function.Supplier; | ||||
import java.util.logging.Level; | import java.util.logging.Level; | |||
import java.util.logging.Logger; | import java.util.logging.Logger; | |||
import org.apache.commons.lang3.tuple.MutablePair; | ||||
import org.apache.commons.lang3.tuple.Pair; | ||||
import org.geotools.data.mongodb.AbstractCollectionMapper; | import org.geotools.data.mongodb.AbstractCollectionMapper; | |||
import org.geotools.data.mongodb.MongoFeature; | import org.geotools.data.mongodb.MongoFeature; | |||
import org.geotools.data.mongodb.MongoGeometryBuilder; | import org.geotools.data.mongodb.MongoGeometryBuilder; | |||
import org.geotools.geometry.jts.GeometryCoordinateSequenceTransformer; | ||||
import org.geotools.util.logging.Logging; | import org.geotools.util.logging.Logging; | |||
import org.locationtech.jts.geom.Geometry; | ||||
import org.opengis.feature.Feature; | import org.opengis.feature.Feature; | |||
import org.opengis.feature.simple.SimpleFeature; | ||||
import org.opengis.referencing.FactoryException; | ||||
import org.opengis.referencing.crs.CoordinateReferenceSystem; | ||||
import org.opengis.referencing.operation.MathTransform; | ||||
/** This class contains utilities methods for dealing with MongoDB complex featu res. */ | /** This class contains utilities methods for dealing with MongoDB complex featu res. */ | |||
public final class MongoComplexUtilities { | public final class MongoComplexUtilities { | |||
private static final Logger LOG = Logging.getLogger(MongoComplexUtilities.cl ass); | private static final Logger LOG = Logging.getLogger(MongoComplexUtilities.cl ass); | |||
// key used to store the parent JSON path in a feature user data map | // key used to store the parent JSON path in a feature user data map | |||
public static final String MONGO_PARENT_PATH = "MONGO_PARENT_PATH"; | public static final String MONGO_PARENT_PATH = "MONGO_PARENT_PATH"; | |||
private static ThreadLocal< | ||||
Pair<CoordinateReferenceSystem, GeometryCoordinateSequenceTr | ||||
ansformer>> | ||||
transformerLocal = new ThreadLocal<>(); | ||||
private MongoComplexUtilities() {} | private MongoComplexUtilities() {} | |||
/** Concat the parent path if it exists to the provided JSON path. */ | /** Concat the parent path if it exists to the provided JSON path. */ | |||
public static String resolvePath(Feature feature, String jsonPath) { | public static String resolvePath(Feature feature, String jsonPath) { | |||
Object parentPath = feature.getUserData().get(MONGO_PARENT_PATH); | Object parentPath = feature.getUserData().get(MONGO_PARENT_PATH); | |||
return parentPath == null ? jsonPath : parentPath + "." + jsonPath; | return parentPath == null ? jsonPath : parentPath + "." + jsonPath; | |||
} | } | |||
/** Store the parent path in a feature user data map. */ | /** Store the parent path in a feature user data map. */ | |||
public static void setParentPath(Feature feature, String parentPath) { | public static void setParentPath(Feature feature, String parentPath) { | |||
feature.getUserData().put(MONGO_PARENT_PATH, parentPath); | feature.getUserData().put(MONGO_PARENT_PATH, parentPath); | |||
} | } | |||
/** | /** | |||
* Will try to extract from the provided object the value that correspond to the given json | * Will try to extract from the provided object the value that correspond to the given json | |||
* path. | * path. | |||
*/ | */ | |||
public static Object getValue(Object object, String jsonPath) { | public static Object getValue(Object object, String jsonPath) { | |||
// let's make sure we have a feature | // let's make sure we have a feature | |||
Feature feature = extractFeature(object, jsonPath); | if (!(object instanceof Feature)) { | |||
if (feature instanceof MongoFeature) { | // not a feature so nothing to do | |||
throw invalidFeature(object, jsonPath); | ||||
} | ||||
Feature feature = (Feature) object; | ||||
// try before to resolve jsonpath against the feature. If it is SimpleFe | ||||
ature | ||||
// and we are on the root appSchema object there is no need to retrieve | ||||
attributes | ||||
// from the DBObject | ||||
if (feature instanceof SimpleFeature) { | ||||
SimpleFeature sf = (SimpleFeature) feature; | ||||
Object value = sf.getAttribute(jsonPath); | ||||
if (value != null) return value; | ||||
} | ||||
Feature extracted = extractFeature(feature, jsonPath); | ||||
if (extracted instanceof MongoFeature) { | ||||
// no a nested element mongo feature | // no a nested element mongo feature | |||
MongoFeature mongoFeature = (MongoFeature) feature; | MongoFeature mongoFeature = (MongoFeature) extracted; | |||
return getValue(mongoFeature.getMongoObject(), jsonPath); | ||||
// if the feature is a MongoFeature then the geometry attribute | ||||
// needs no reprojection | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer = | ||||
feature instanceof MongoFeature ? null : getTransformer(feat | ||||
ure, mongoFeature); | ||||
return getValue(mongoFeature.getMongoObject(), jsonPath, transformer | ||||
); | ||||
} | } | |||
if (feature instanceof MongoCollectionFeature) { | if (extracted instanceof MongoCollectionFeature) { | |||
// a mongo feature in the context of a nested element | // a mongo feature in the context of a nested element | |||
MongoCollectionFeature collectionFeature = (MongoCollectionFeature) | MongoCollectionFeature collectionFeature = (MongoCollectionFeature) | |||
object; | extracted; | |||
MongoFeature mongoFeature = collectionFeature.getMongoFeature(); | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer = | ||||
getTransformer(feature, mongoFeature); | ||||
return getValue( | return getValue( | |||
collectionFeature.getMongoFeature().getMongoObject(), | collectionFeature.getMongoFeature().getMongoObject(), | |||
collectionFeature.getCollectionsIndexes(), | collectionFeature.getCollectionsIndexes(), | |||
jsonPath); | jsonPath, | |||
transformer); | ||||
} | } | |||
// could not find a mongo feature, we can do nothing | // could not find a mongo feature, we can do nothing | |||
throw invalidFeature(feature, jsonPath); | throw invalidFeature(feature, jsonPath); | |||
} | } | |||
/** Method for extracting or casting a feature from the provided object. */ | static Supplier<GeometryCoordinateSequenceTransformer> getTransformer( | |||
Feature feature, MongoFeature mongoFeature) { | ||||
// helper method to retrieve a transformation if the CRS between the Fea | ||||
ture | ||||
// object and the MongoFeature is different. This might happen if a Comp | ||||
lexFeature | ||||
// is reprojected. In that case the feature is rebuilt with the MongoFea | ||||
ture | ||||
// still having a not reprojected geometry. | ||||
// The method return a Supplier to allow lazy evaluation and cache the t | ||||
ransformer | ||||
// in a ThreadLocal to avoid the creation of a new object for each featu | ||||
re. | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformerSupplier = nu | ||||
ll; | ||||
CoordinateReferenceSystem crs = mongoFeature.getOriginalCRS(); | ||||
CoordinateReferenceSystem target = | ||||
feature.getDefaultGeometryProperty().getDescriptor().getCoordina | ||||
teReferenceSystem(); | ||||
Pair<CoordinateReferenceSystem, GeometryCoordinateSequenceTransformer> p | ||||
air = | ||||
transformerLocal.get(); | ||||
if (pair != null && pair.getLeft().equals(target)) { | ||||
final GeometryCoordinateSequenceTransformer cachedTransformer = pair | ||||
.getRight(); | ||||
return () -> cachedTransformer; | ||||
} | ||||
pair = new MutablePair<>(target, null); | ||||
try { | ||||
if (crs != null && target != null && !crs.equals(target)) { | ||||
MathTransform transform = findMathTransform(crs, target); | ||||
GeometryCoordinateSequenceTransformer transformer = | ||||
new GeometryCoordinateSequenceTransformer(); | ||||
transformer.setMathTransform(transform); | ||||
pair.setValue(transformer); | ||||
// return a supplier to allow lazy evaluation | ||||
transformerSupplier = () -> transformer; | ||||
} | ||||
} catch (FactoryException e) { | ||||
LOG.log( | ||||
Level.WARNING, | ||||
"Unable to find transformation for " | ||||
+ crs.getName().getCode() | ||||
+ "and " | ||||
+ target.getName().getCode()); | ||||
} | ||||
transformerLocal.set(pair); | ||||
// store the CRS in the featureType to avoid the rebuilding of the trans | ||||
former | ||||
// for each feature | ||||
return transformerSupplier; | ||||
} | ||||
public static Feature extractFeature(Object feature, String jsonPath) { | public static Feature extractFeature(Object feature, String jsonPath) { | |||
// we should have a feature | ||||
if (!(feature instanceof Feature)) { | if (!(feature instanceof Feature)) { | |||
// not a feature so nothing to do | // not a feature so nothing to do | |||
throw invalidFeature(feature, jsonPath); | throw invalidFeature(feature, jsonPath); | |||
} | } | |||
return extractFeature((Feature) feature, jsonPath); | ||||
} | ||||
/** Method for extracting or casting a feature from the provided object. */ | ||||
public static Feature extractFeature(Feature feature, String jsonPath) { | ||||
// let's see if we have the a mongo feature in the user data | // let's see if we have the a mongo feature in the user data | |||
Object mongoFeature = | Object mongoFeature = | |||
((Feature) feature) | feature.getUserData().get(AbstractCollectionMapper.MONGO_OBJECT_ | |||
.getUserData() | FEATURE_KEY); | |||
.get(AbstractCollectionMapper.MONGO_OBJECT_FEATURE_KEY); | ||||
// if we could not find a mongo feature in the user data we stick we the original feature | // 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 ; | return mongoFeature == null ? feature : (Feature) mongoFeature; | |||
} | } | |||
/** | /** | |||
* Helper method that creates an exception for when the provided object is n ot of the correct | * Helper method that creates an exception for when the provided object is n ot of the correct | |||
* type. | * type. | |||
*/ | */ | |||
private static RuntimeException invalidFeature(Object feature, String jsonPa th) { | static RuntimeException invalidFeature(Object feature, String jsonPath) { | |||
return new RuntimeException( | return new RuntimeException( | |||
String.format( | String.format( | |||
"No possible to obtain a mongo object from '%s' to extra ct '%s'.", | "No possible to obtain a mongo object from '%s' to extra ct '%s'.", | |||
feature.getClass(), jsonPath)); | feature.getClass(), jsonPath)); | |||
} | } | |||
/** | /** | |||
* Will extract from the mongo db object the value that correspond to the gi ven json path. If | * Will extract from the mongo db object the value that correspond to the gi ven json path. If | |||
* the path contain a nested list of values an exception will be throw. | * the path contain a nested list of values an exception will be throw. | |||
*/ | */ | |||
public static Object getValue(DBObject mongoObject, String jsonPath) { | public static Object getValue( | |||
return getValue(mongoObject, Collections.emptyMap(), jsonPath); | DBObject mongoObject, | |||
String jsonPath, | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer) { | ||||
return getValue(mongoObject, Collections.emptyMap(), jsonPath, transform | ||||
er); | ||||
} | } | |||
/** | /** | |||
* Will extract from the mongo db object the value that correspond to the gi ven json path. The | * Will extract from the mongo db object the value that correspond to the gi ven json path. The | |||
* provided collections indexes will be used to select the proper element fo r the collections | * provided collections indexes will be used to select the proper element fo r the collections | |||
* present in the path. | * present in the path. | |||
*/ | */ | |||
public static Object getValue( | public static Object getValue( | |||
DBObject mongoObject, Map<String, Integer> collectionsIndexes, Strin | DBObject mongoObject, | |||
g jsonPath) { | Map<String, Integer> collectionsIndexes, | |||
String jsonPath, | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer) { | ||||
MongoObjectWalker walker = new MongoObjectWalker(mongoObject, collection sIndexes, jsonPath); | MongoObjectWalker walker = new MongoObjectWalker(mongoObject, collection sIndexes, jsonPath); | |||
// try to convert the founded value to a geometry | // try to convert the founded value to a geometry | |||
return convertGeometry(walker.getValue()); | return convertGeometry(walker.getValue(), transformer); | |||
} | } | |||
/** | /** | |||
* Helper method that checks if a mongodb value is a geometry and perform th e proper conversion. | * Helper method that checks if a mongodb value is a geometry and perform th e proper conversion. | |||
*/ | */ | |||
private static Object convertGeometry(Object value) { | private static Object convertGeometry( | |||
Object value, Supplier<GeometryCoordinateSequenceTransformer> transf | ||||
ormer) { | ||||
if (!(value instanceof DBObject) || value instanceof List) { | if (!(value instanceof DBObject) || value instanceof List) { | |||
// not a mongodb object or a list of values so nothing to do | // not a mongodb object or a list of values so nothing to do | |||
return value; | return value; | |||
} | } | |||
DBObject object = (DBObject) value; | DBObject object = (DBObject) value; | |||
Set keys = object.keySet(); | Set keys = object.keySet(); | |||
if (keys.size() != 2 || !keys.contains("coordinates") || !keys.contains( "type")) { | if (keys.size() != 2 || !keys.contains("coordinates") || !keys.contains( "type")) { | |||
// is mongo db object but not a geometry | // is mongo db object but not a geometry | |||
return value; | return value; | |||
} | } | |||
// we have a geometry so let's try to convert it | // we have a geometry so let's try to convert it | |||
MongoGeometryBuilder builder = new MongoGeometryBuilder(); | MongoGeometryBuilder builder = new MongoGeometryBuilder(); | |||
try { | try { | |||
// return the converted geometry | // return the converted geometry | |||
return builder.toGeometry(object); | Geometry geom = builder.toGeometry(object); | |||
if (transformer != null) { | ||||
geom = transformer.get().transform(geom); | ||||
} | ||||
return geom; | ||||
} catch (Exception exception) { | } catch (Exception exception) { | |||
// well could not convert the mongo db object to a geometry | // well could not convert the mongo db object to a geometry | |||
} | } | |||
return value; | return value; | |||
} | } | |||
/** Utility class class to extract information from a MongoDB object giving a certain path. */ | /** Utility class class to extract information from a MongoDB object giving a certain path. */ | |||
private static final class MongoObjectWalker { | private static final class MongoObjectWalker { | |||
private final Map<String, Integer> collectionsIndexes; | private final Map<String, Integer> collectionsIndexes; | |||
skipping to change at line 271 | skipping to change at line 367 | |||
return getValues(collectionFeature.getMongoFeature().getMongoObject( ), jsonPath); | return getValues(collectionFeature.getMongoFeature().getMongoObject( ), jsonPath); | |||
} | } | |||
// could not find a mongo feature, we can do nothing | // could not find a mongo feature, we can do nothing | |||
throw invalidFeature(feature, jsonPath); | throw invalidFeature(feature, jsonPath); | |||
} | } | |||
/** | /** | |||
* Will extract from the mongo db object all the values that correspond to t he given json path. | * Will extract from the mongo db object all the values that correspond to t he given json path. | |||
* If the path contains nested collections the values from all the branches will be merged. | * If the path contains nested collections the values from all the branches will be merged. | |||
*/ | */ | |||
public static Object getValues(DBObject dbObject, String jsonPath) { | public static Object getValues( | |||
DBObject dbObject, | ||||
String jsonPath, | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer) { | ||||
if (jsonPath == null || jsonPath.isEmpty() || dbObject == null) { | if (jsonPath == null || jsonPath.isEmpty() || dbObject == null) { | |||
// nothing to do here | // nothing to do here | |||
return Collections.emptyList(); | return Collections.emptyList(); | |||
} | } | |||
// let's split the json path in parts which will give us the necessary k eys | // let's split the json path in parts which will give us the necessary k eys | |||
String[] jsonPathParts = jsonPath.split("\\."); | String[] jsonPathParts = jsonPath.split("\\."); | |||
// recursively get the values using an helper function | // recursively get the values using an helper function | |||
List<Object> values = getValuesHelper(dbObject, jsonPathParts, new Array | List<Object> values = | |||
List<>(), 0); | getValuesHelper(dbObject, jsonPathParts, new ArrayList<>(), 0, t | |||
ransformer); | ||||
if (values.size() == 1) { | if (values.size() == 1) { | |||
// we only have a single value, let's extract it | // we only have a single value, let's extract it | |||
return values.get(0); | return values.get(0); | |||
} | } | |||
return values; | return values; | |||
} | } | |||
/** | /** | |||
* Helper function that will walk a mongo db object and retrieve all the val ues for a certain | * Helper function that will walk a mongo db object and retrieve all the val ues for a certain | |||
* path. | * path. | |||
*/ | */ | |||
private static List<Object> getValuesHelper( | private static List<Object> getValuesHelper( | |||
DBObject dbObject, String[] jsonPathParts, List<Object> values, int | DBObject dbObject, | |||
index) { | String[] jsonPathParts, | |||
List<Object> values, | ||||
int index, | ||||
Supplier<GeometryCoordinateSequenceTransformer> transformer) { | ||||
// get the object corresponding to the current index | // get the object corresponding to the current index | |||
Object object = dbObject.get(jsonPathParts[index]); | Object object = dbObject.get(jsonPathParts[index]); | |||
if (object == null) { | if (object == null) { | |||
// we are done | // we are done | |||
return values; | return values; | |||
} | } | |||
// check if we reach the end of the json path | // check if we reach the end of the json path | |||
boolean finalPath = index == jsonPathParts.length - 1; | boolean finalPath = index == jsonPathParts.length - 1; | |||
index++; | index++; | |||
if (object instanceof List) { | if (object instanceof List) { | |||
if (finalPath) { | if (finalPath) { | |||
// we reached the end of the json path and we have a list, so le t's add all the | // we reached the end of the json path and we have a list, so le t's add all the | |||
// elements of the list | // elements of the list | |||
for (Object value : (List) object) { | for (Object value : (List) object) { | |||
values.add(convertGeometry(value)); | values.add(convertGeometry(value, transformer)); | |||
} | } | |||
} else { | } else { | |||
// well we have a list so we need to interact over each element of the list | // well we have a list so we need to interact over each element of the list | |||
for (Object element : (List) object) { | for (Object element : (List) object) { | |||
getValuesHelper((DBObject) element, jsonPathParts, values, i ndex); | getValuesHelper((DBObject) element, jsonPathParts, values, i ndex, transformer); | |||
} | } | |||
} | } | |||
} else { | } else { | |||
if (finalPath) { | if (finalPath) { | |||
// we reached the end of the json path so let's add this object | // we reached the end of the json path so let's add this object | |||
values.add(convertGeometry(object)); | values.add(convertGeometry(object, transformer)); | |||
} else { | } else { | |||
// we need to go deeper in this object | // we need to go deeper in this object | |||
getValuesHelper((DBObject) object, jsonPathParts, values, index) ; | getValuesHelper((DBObject) object, jsonPathParts, values, index, transformer); | |||
} | } | |||
} | } | |||
// we return the list of founded values for commodity | // we return the list of founded values for commodity | |||
return values; | return values; | |||
} | } | |||
/** Simple method that adds an element ot a json path. */ | /** Simple method that adds an element ot a json path. */ | |||
private static String concatPath(String parentPath, String path) { | private static String concatPath(String parentPath, String path) { | |||
if (parentPath == null || parentPath.isEmpty()) { | if (parentPath == null || parentPath.isEmpty()) { | |||
// first element of the path | // first element of the path | |||
End of changes. 30 change blocks. | ||||
31 lines changed or deleted | 151 lines changed or added |