Skip to content

Commit

Permalink
Continue with Mongo support
Browse files Browse the repository at this point in the history
  • Loading branch information
hghianni committed Oct 23, 2023
1 parent 9ba39b2 commit 24ebc24
Show file tree
Hide file tree
Showing 25 changed files with 631 additions and 145 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ private double calculateDistance(QueryOperation operation, Object doc) {
if (operation instanceof TypeOperation) return calculateDistanceForType((TypeOperation) operation, doc);
if (operation instanceof InvertedTypeOperation)
return calculateDistanceForInvertedType((InvertedTypeOperation) operation, doc);
if (operation instanceof NearSphereOperation)
return calculateDistanceForNearSphere((NearSphereOperation) operation, doc);
return Double.MAX_VALUE;
}

Expand Down Expand Up @@ -281,6 +283,50 @@ private double calculateDistanceForInvertedType(InvertedTypeOperation operation,
return !Objects.equals(actualType, expectedType) ? 0.0 : MIN_DISTANCE_TO_TRUE_VALUE;
}

private double calculateDistanceForNearSphere(NearSphereOperation operation, Object doc) {
String field = operation.getFieldName();
Object actualPoint = getValue(doc, field);

double x1 = Math.toRadians(operation.getLongitude());
double y1 = Math.toRadians(operation.getLatitude());
double x2;
double y2;

// GeoJsonPoint in document
if (isDocument(actualPoint) && getValue(actualPoint, "type").equals("Point") && getValue(actualPoint, "coordinates") instanceof List<?>) {

List<?> coordinates = (List<?>) getValue(actualPoint, "coordinates");
x2 = Math.toRadians((Double) coordinates.get(0));
y2 = Math.toRadians((Double) coordinates.get(1));
} else {
return Double.MAX_VALUE;
}

double distanceBetweenPoints = haversineDistance(x1, y1, x2, y2);

double max = operation.getMaxDistance() == null ? Double.MAX_VALUE : operation.getMaxDistance();
double min = operation.getMinDistance() == null ? 0.0 : operation.getMinDistance();

if (min <= distanceBetweenPoints && distanceBetweenPoints <= max) {
return 0.0;
} else {
return distanceBetweenPoints > max ? Math.abs(distanceBetweenPoints - max) : Math.abs(distanceBetweenPoints - min);
}
}

private static double haversineDistance(double x1, double y1, double x2, double y2) {
// Earth's radius in meters
double radius = 6371000.0;

double dLat = y2 - y1;
double dLon = x2 - x1;

double a = Math.pow(Math.sin(dLat / 2), 2) + Math.cos(y1) * Math.cos(y2) * Math.pow(Math.sin(dLon / 2), 2);

double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return radius * c;
}

private QueryOperation invertOperation(QueryOperation operation) {
if (operation instanceof EqualsOperation<?>) {
EqualsOperation<?> op = (EqualsOperation<?>) operation;
Expand Down Expand Up @@ -386,6 +432,19 @@ private double compareValues(Object val1, Object val2) {
return val1 == val2 ? 0d : 1d;
}

if (val1 instanceof String && isObjectId(val2)) {
return (double) DistanceHelper.getLeftAlignmentDistance((String) val1, val2.toString());
}

if (val2 instanceof String && isObjectId(val1)) {
return (double) DistanceHelper.getLeftAlignmentDistance(val1.toString(), (String) val2);
}

if (isObjectId(val2) && isObjectId(val1)) {
return (double) DistanceHelper.getLeftAlignmentDistance(val1.toString(), val2.toString());
}


if (val1 instanceof List<?> && val2 instanceof List<?>) {
// Modify
return Double.MAX_VALUE;
Expand All @@ -394,6 +453,10 @@ private double compareValues(Object val1, Object val2) {
return Double.MAX_VALUE;
}

private static boolean isObjectId(Object obj) {
return obj.getClass().getName().equals("org.bson.types.ObjectId");
}

private double distanceToClosestElem(List<?> list, Object value) {
double minDist = Double.MAX_VALUE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class QueryParser {
new NotSelector(),
new ExistsSelector(),
new TypeSelector(),
new NearSphereSelector(),
new ImplicitSelector()
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.evomaster.client.java.controller.mongo.operations;

/**
* Represent $nearSphere operation.
* Specifies a point for which a geospatial query returns the documents from nearest to farthest.
*/
public class NearSphereOperation extends QueryOperation {
private final String fieldName;
private final Double longitude;
private final Double latitude;
private final Double maxDistance;
private final Double minDistance;


public NearSphereOperation(String fieldName, Double longitude, Double latitude, Double maxDistance, Double minDistance) {
this.fieldName = fieldName;
this.longitude = longitude;
this.latitude = latitude;
this.maxDistance = maxDistance;
this.minDistance = minDistance;
}

public String getFieldName() {
return fieldName;
}

public Double getLongitude() {
return longitude;
}

public Double getLatitude() {
return latitude;
}

public Double getMaxDistance() {
return maxDistance;
}

public Double getMinDistance() {
return minDistance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.evomaster.client.java.controller.mongo.selectors;

import org.evomaster.client.java.controller.mongo.operations.NearSphereOperation;
import org.evomaster.client.java.controller.mongo.operations.QueryOperation;

import java.util.List;
import java.util.Set;

import static org.evomaster.client.java.controller.mongo.utils.BsonHelper.*;

/**
* { field: { $nearSphere: [ x, y ], $maxDistance: value, $minDistance: value } }
* or
* { field: { $nearSphere: {$geometry: {type: "Point", coordinates: [ longitude, latitude ]}, $maxDistance: value, $minDistance: value}}
*/
public class NearSphereSelector extends QuerySelector {

public static final int EARTH_RADIUS_IN_METERS = 6371000;

@Override
public QueryOperation getOperation(Object query) {
String fieldName = extractFieldName(query);
Object innerDoc = getValue(query, fieldName);

if (!isDocument(innerDoc) || !hasTheExpectedOperator(query)) return null;

Object point = getValue(innerDoc, operator());
Object geometry = getValue(point, "$geometry");
boolean legacyCoordinates = geometry == null;

return parseValue(fieldName, innerDoc, legacyCoordinates);
}

protected String extractOperator(Object query) {
String fieldName = extractFieldName(query);
Set<String> keys = documentKeys(getValue(query, fieldName));
return keys.stream().findFirst().orElse(null);
}

@Override
protected String operator() {
return "$nearSphere";
}

public QueryOperation parseValue(String fieldName, Object innerDoc, boolean legacyCoordinates) {
Object longitude;
Object latitude;
Object maxDistance = null;
Object minDistance = null;

Object point = getValue(innerDoc, operator());

if (legacyCoordinates) {
Object maxDistanceInRadians = getValue(innerDoc, "$maxDistance");
Object minDistanceInRadians = getValue(innerDoc, "$minDistance");

if (maxDistanceInRadians instanceof Double) {
maxDistance = radiansToMeters((double) maxDistanceInRadians);
}

if (minDistanceInRadians instanceof Double) {
minDistance = radiansToMeters((double) minDistanceInRadians);
}

longitude = getValue(point, "x");
latitude = getValue(point, "y");
} else {
Object geometry = getValue(point, "$geometry");
Object coordinates = getValue(geometry, "coordinates");

if (coordinates instanceof List<?> && ((List<?>) coordinates).size() == 2) {
longitude = ((List<?>) coordinates).get(0);
latitude = ((List<?>) coordinates).get(1);
} else {
return null;
}

maxDistance = getValue(point, "$maxDistance");
minDistance = getValue(point, "$minDistance");
}

if (longitude instanceof Double && latitude instanceof Double && (maxDistance == null || maxDistance instanceof Double) && (minDistance == null || minDistance instanceof Double)) {
return new NearSphereOperation(fieldName, (Double) longitude, (Double) latitude, (Double) maxDistance, (Double) minDistance);
}
return null;
}

private static double radiansToMeters(double radians) {
return EARTH_RADIUS_IN_METERS * radians;
}

private String extractFieldName(Object query) {
Set<String> keys = documentKeys(query);
return keys.stream().findFirst().orElse(null);
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package org.evomaster.client.java.controller.internal.db.mongo;

import com.mongodb.client.model.Filters;
import org.bson.BsonDocument;
import org.bson.BsonType;
import org.bson.Document;
import org.bson.*;
import org.bson.codecs.DecoderContext;
import org.bson.codecs.DocumentCodec;
import org.bson.conversions.Bson;
Expand Down Expand Up @@ -108,6 +106,7 @@ public void testAnd() {
assertEquals(0.0, distanceMatch);
assertEquals(4.0, distanceNotMatch);
}

@Test
public void testImplicitAnd() {
Document doc = new Document().append("age", 10).append("kg", 50);
Expand Down Expand Up @@ -241,7 +240,19 @@ public void testElemMatch() {
assertEquals(2.0, distanceNotMatch);
}

private static Document convertToDocument(Bson filter){
@Test
public void testNearSphere() {
Document doc = new Document().append("location", new Document().append("type", "Point").append("coordinates", Arrays.asList(-74.044502, 40.689247)));
BsonDocument point = new BsonDocument().append("type", new BsonString("Point")).append("coordinates", new BsonArray(Arrays.asList(new BsonDouble(2.29441692356368), new BsonDouble(48.858504187164684))));
Bson bsonTrue = Filters.nearSphere("location", point, 6000000.0, 0.0);
Bson bsonFalse = Filters.nearSphere("location", point, 5000000.0, 0.0);
Double distanceMatch = new MongoHeuristicsCalculator().computeExpression(convertToDocument(bsonTrue), doc);
Double distanceNotMatch = new MongoHeuristicsCalculator().computeExpression(convertToDocument(bsonFalse), doc);
assertEquals(0.0, distanceMatch);
assertEquals(837402.9310023151, distanceNotMatch);
}

private static Document convertToDocument(Bson filter) {
BsonDocument bsonDocument = filter.toBsonDocument();
DocumentCodec documentCodec = new DocumentCodec();
return documentCodec.decode(bsonDocument.asBsonReader(), DecoderContext.builder().build());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public static Map<String, String> extractAsSchema(List<String> jvmDtoNames) {
continue;
}
clazz = Class.forName(dtoName);
schemas.putAll(ClassToSchema.getOrDeriveSchemaAndNestedClasses(clazz, false));
schemas.putAll(ClassToSchema.getOrDeriveSchemaAndNestedClasses(clazz, false, Collections.emptyList()));
} catch (ClassNotFoundException e) {
SimpleLogger.uniqueWarn("Fail to extract Jvm DTO as schema:"+e.getMessage());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static List<MethodReplacementClass> getList() {
new ByteClassReplacement(),
new CharacterClassReplacement(),
new CollectionClassReplacement(),
new CursorPreparerClassReplacement(),
new DateClassReplacement(),
new DateFormatClassReplacement(),
new DoubleClassReplacement(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package org.evomaster.client.java.instrumentation.coverage.methodreplacement.thirdpartyclasses;

import org.evomaster.client.java.instrumentation.coverage.methodreplacement.Replacement;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.UsageFilter;
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
import org.evomaster.client.java.instrumentation.shared.ReplacementType;

import java.lang.reflect.Field;
import java.util.function.Function;

/**
* This replacement should ideally be unnecessary due to the existence of MongoOperationClassReplacement.
* However, there appears to be an issue as operations related to IDs are not being captured by this replacement,
* and the root cause for this behavior is not immediately apparent.
* Until this issue is resolved, this workaround should address the problem.
*/
public class CursorPreparerClassReplacement extends MongoOperationClassReplacement {
private static final CursorPreparerClassReplacement singleton = new CursorPreparerClassReplacement();

@Override
protected String getNameOfThirdPartyTargetClass() {
return "org.springframework.data.mongodb.core.CursorPreparer";
}

@Replacement(replacingStatic = false, type = ReplacementType.TRACKER, id = "initiateFind", usageFilter = UsageFilter.ANY, category = ReplacementCategory.MONGO, castTo = "com.mongodb.client.FindIterable")
public static Object initiateFind(Object preparer, @ThirdPartyCast(actualType = "com.mongodb.client.MongoCollection") Object mongoCollection, Function<Object, Object> find) {
return handleFind(mongoCollection, find);
}

private static Object handleFind(Object mongoCollection, Function<Object, Object> find) {
long startTime = System.currentTimeMillis();

Object argument = getField(find, "arg$1");
Object query = getField(argument, "query");
Object result = find.apply(mongoCollection);

long endTime = System.currentTimeMillis();

handleMongo(mongoCollection, query, true, endTime - startTime);

return result;
}

private static Object getField(Object object, String fieldName) {
try {
Field field = object.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyCast;
import org.evomaster.client.java.instrumentation.coverage.methodreplacement.ThirdPartyMethodReplacementClass;
import org.evomaster.client.java.instrumentation.object.ClassToSchema;
import org.evomaster.client.java.instrumentation.object.CustomTypeToOasConverter;
import org.evomaster.client.java.instrumentation.object.GeoJsonPointToOasConverter;
import org.evomaster.client.java.instrumentation.shared.ReplacementCategory;
import org.evomaster.client.java.instrumentation.shared.ReplacementType;
import org.evomaster.client.java.instrumentation.staticstate.ExecutionTracer;
Expand Down Expand Up @@ -89,7 +91,8 @@ private static void handleMappingMongoEntityInformationConstructor(String id, Li
addInstance(mappingMongoEntityInformation);
String collectionName = (String) mappingMongoEntityInformation.getClass().getMethod("getCollectionName").invoke(mappingMongoEntityInformation);
Class<?> repositoryType = (Class<?>) mappingMongoEntityInformation.getClass().getMethod("getJavaType").invoke(mappingMongoEntityInformation);
String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(repositoryType, true);
List<CustomTypeToOasConverter> converters = Collections.singletonList(new GeoJsonPointToOasConverter());
String schema = ClassToSchema.getOrDeriveSchemaWithItsRef(repositoryType, true, converters);
ExecutionTracer.addMongoCollectionInfo(new MongoCollectionInfo(collectionName, schema));
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
throw new RuntimeException(e);
Expand Down
Loading

0 comments on commit 24ebc24

Please sign in to comment.