Skip to content

Commit

Permalink
Implement a new sort method for FindOptions that accepts a Meta and a…
Browse files Browse the repository at this point in the history
… Sort args

clean up some javadoc

fixes #2342
  • Loading branch information
evanchooly committed May 30, 2023
1 parent 5759aed commit e4a5062
Show file tree
Hide file tree
Showing 10 changed files with 251 additions and 45 deletions.
6 changes: 3 additions & 3 deletions core/src/main/java/dev/morphia/EntityListener.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public interface EntityListener<T> {
* @param document the Document form of the entity
* @param mapper the Mapper being used
* @see PostLoad
* @deprecated use {@link #postPersist(T, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
* @deprecated use {@link #postPersist(Object, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
*/
@Deprecated(forRemoval = true)
default void postLoad(T entity, Document document, Mapper mapper) {
Expand Down Expand Up @@ -62,7 +62,7 @@ default void postPersist(T entity, Document document, Datastore datastore) {
* @param document the Document form of the entity
* @param mapper the Mapper being used
* @see PreLoad
* @deprecated use {@link #preLoad(T, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
* @deprecated use {@link #preLoad(Object, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
* necessary
*/
@Deprecated(forRemoval = true)
Expand All @@ -85,7 +85,7 @@ default void preLoad(T entity, Document document, Datastore datastore) {
* @param document the Document form of the entity
* @param mapper the Mapper being used
* @see PrePersist
* @deprecated use {@link #prePersist(T, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
* @deprecated use {@link #prePersist(Object, Document, Datastore)} instead and access the Mapper via {@link Datastore#getMapper()} if
* necessary
*/
@Deprecated(forRemoval = true)
Expand Down
28 changes: 26 additions & 2 deletions core/src/main/java/dev/morphia/query/FindOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import dev.morphia.internal.PathTarget;
import dev.morphia.internal.ReadConfigurable;
import dev.morphia.mapping.Mapper;
import dev.morphia.mapping.NotMappableException;
import dev.morphia.mapping.codec.pojo.EntityModel;
import dev.morphia.sofia.Sofia;

Expand Down Expand Up @@ -144,11 +145,16 @@ public <T> FindIterable<T> apply(FindIterable<T> iterable, Mapper mapper, Class<
iterable.skip(skip);
if (sort != null) {
Document mapped = new Document();
EntityModel model = mapper.getEntityModel(type);
EntityModel model = null;
try {
model = mapper.getEntityModel(type);
} catch (NotMappableException ignored) {
}

for (Entry<String, Object> entry : sort.entrySet()) {
Object value = entry.getValue();
boolean metaScore = value instanceof Document && ((Document) value).get("$meta") != null;
mapped.put(new PathTarget(mapper, model, entry.getKey(), !metaScore).translatedPath(), value);
mapped.put(new PathTarget(mapper, model, entry.getKey(), model != null && !metaScore).translatedPath(), value);
}
iterable.sort(mapped);
}
Expand Down Expand Up @@ -803,6 +809,24 @@ public FindOptions sort(Meta meta) {
return sort(meta.toDatabase());
}

/**
* Sets to the sort to use
*
* @param meta the meta data to sort by
* @param sorts additional sort elements
* @return this
* @since 2.4
*/
public FindOptions sort(Meta meta, Sort... sorts) {
projection().project(meta);
sort(meta.toDatabase());
for (Sort sort : sorts) {
this.sort.append(sort.getField(), sort.getOrder());
}

return this;
}

/**
* Sets to the sort to use
*
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/dev/morphia/query/Meta.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ public class Meta {
* @param metaDataKeyword metadata keyword to create
* @param fieldName the field to store the value in
* @since 2.2
* @deprecated use the various factory methods. these constructors will go private in a future release.
*/
@Deprecated(forRemoval = true)
public Meta(String metaDataKeyword, String fieldName) {
this.metaDataKeyword = metaDataKeyword;
this.field = fieldName;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,7 @@

package dev.morphia.test.aggregation;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
Expand All @@ -31,22 +27,20 @@

import dev.morphia.aggregation.Aggregation;
import dev.morphia.aggregation.AggregationImpl;
import dev.morphia.test.TestBase;
import dev.morphia.test.aggregation.expressions.TemplatedTestBase;

import org.bson.Document;
import org.jetbrains.annotations.NotNull;
import org.testng.Assert;

import static java.lang.Character.toLowerCase;
import static java.lang.String.format;
import static java.util.Arrays.stream;
import static java.util.stream.Collectors.toList;
import static org.bson.json.JsonWriterSettings.builder;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.fail;

@SuppressWarnings({ "unused", "MismatchedQueryAndUpdateOfCollection" })
public class AggregationTest extends TestBase {
public class AggregationTest extends TemplatedTestBase {

protected final ObjectMapper mapper = new ObjectMapper();

Expand All @@ -73,10 +67,6 @@ public void testPipeline(double serverVersion, String resourceName, boolean remo
}
}

protected void loadData(String collection, String resourceName) {
insert(collection, loadJson(format("%s/%s/data.json", prefix(), resourceName)));
}

@SuppressWarnings({ "unchecked", "rawtypes" })
protected List<Document> runPipeline(String pipelineTemplate, Aggregation<Document> aggregation) {
String pipelineName = format("%s/%s/pipeline.json", prefix(), pipelineTemplate);
Expand Down Expand Up @@ -104,32 +94,6 @@ protected List<Document> runPipeline(String pipelineTemplate, Aggregation<Docume

}

protected @NotNull List<Document> loadExpected(String resourceName) {
return loadJson(format("%s/%s/expected.json", prefix(), resourceName));
}

@NotNull
protected List<Document> loadJson(String name) {
List<Document> data = new ArrayList<>();
InputStream stream = getClass().getResourceAsStream(name);
if (stream == null) {
fail("missing data file: " + name);
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (reader.ready()) {
data.add(Document.parse(reader.readLine()));
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
return data;
}

public final String prefix() {
String root = getClass().getSimpleName().replace("Test", "");
return toLowerCase(root.charAt(0)) + root.substring(1);
}

protected void cakeSales() {
insert("cakeSales", parseDocs(
"{ _id: 0, type: 'chocolate', orderDate: ISODate('2020-05-18T14:10:30Z'), state: 'CA', price: 13, quantity: 120 }",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import dev.morphia.aggregation.expressions.impls.Expression;
import dev.morphia.aggregation.stages.Projection;
import dev.morphia.mapping.codec.writer.DocumentWriter;
import dev.morphia.test.TestBase;
import dev.morphia.test.models.User;

import org.bson.Document;
Expand All @@ -15,7 +14,7 @@
import static dev.morphia.aggregation.codecs.ExpressionHelper.document;
import static org.testng.Assert.assertEquals;

public class ExpressionsTestBase extends TestBase {
public class ExpressionsTestBase extends TemplatedTestBase {
@BeforeMethod
public void seed() {
getDs().getCollection(User.class).drop();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package dev.morphia.test.aggregation.expressions;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

import com.fasterxml.jackson.databind.ObjectMapper;

import dev.morphia.test.TestBase;

import org.bson.Document;
import org.jetbrains.annotations.NotNull;

import static java.lang.Character.toLowerCase;
import static java.lang.String.format;
import static org.testng.Assert.fail;

public class TemplatedTestBase extends TestBase {
public final String prefix() {
String root = getClass().getSimpleName().substring(4);
return toLowerCase(root.charAt(0)) + root.substring(1);
}

protected void loadData(String collection, String resourceName) {
insert(collection, loadJson(format("%s/%s/data.json", prefix(), resourceName)));
}

protected @NotNull List<Document> loadExpected(String resourceName) {
return loadJson(format("%s/%s/expected.json", prefix(), resourceName));
}

protected @NotNull <T> List<T> loadExpected(Class<T> type, String resourceName) {
return loadJson(type, format("%s/%s/expected.json", prefix(), resourceName));
}

@NotNull
protected List<Document> loadJson(String name) {
List<Document> data = new ArrayList<>();
InputStream stream = getClass().getResourceAsStream(name);
if (stream == null) {
fail("missing data file: " + name);
}
try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (reader.ready()) {
data.add(Document.parse(reader.readLine()));
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
return data;
}

@NotNull
protected <T> List<T> loadJson(Class<T> type, String name) {
List<T> data = new ArrayList<>();
InputStream stream = getClass().getResourceAsStream(name);
if (stream == null) {
fail("missing data file: " + name);
}
ObjectMapper mapper = new ObjectMapper();

try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
while (reader.ready()) {
data.add(mapper.readValue(reader.readLine(), type));
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
return data;
}
}
131 changes: 131 additions & 0 deletions core/src/test/java/dev/morphia/test/query/TestSorts.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package dev.morphia.test.query;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.List;
import java.util.Objects;

import com.mongodb.lang.NonNull;

import dev.morphia.annotations.Entity;
import dev.morphia.annotations.Id;
import dev.morphia.annotations.Text;
import dev.morphia.mapping.codec.reader.DocumentReader;
import dev.morphia.query.FindOptions;
import dev.morphia.query.Query;
import dev.morphia.test.aggregation.expressions.TemplatedTestBase;

import org.bson.Document;
import org.bson.codecs.DecoderContext;
import org.testng.annotations.Test;

import static dev.morphia.query.Meta.textScore;
import static dev.morphia.query.Sort.ascending;
import static dev.morphia.query.filters.Filters.text;
import static java.lang.String.format;
import static java.util.stream.Collectors.*;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;

public class TestSorts extends TemplatedTestBase {
public <D> void testQuery(double serverVersion, String resourceName, boolean orderMatters, Query<D> query, FindOptions options) {
checkMinServerVersion(serverVersion);

loadData(getDs().getCollection(query.getEntityClass()).getNamespace().getCollectionName(), resourceName);

List<D> actual = runQuery(resourceName, query, options);

List<D> expected = map(query.getEntityClass(), loadExpected(resourceName));

if (orderMatters) {
assertEquals(actual, expected);
} else {
assertListEquals(actual, expected);
}
}

private <D> List<D> map(Class<D> entityClass, List<Document> documents) {
var codec = getDs().getCodecRegistry().get(entityClass);

DecoderContext context = DecoderContext.builder().build();
return documents.stream()
.map(document -> {
return codec.decode(new DocumentReader(document), context);
})
.collect(toList());
}

@Test
public void metaAndSorts() {
getMapper().map(Article.class);
getDs().ensureIndexes();

Query<Article> query = getDs().find(Article.class)
.filter(text("coffee"));
FindOptions options = new FindOptions()
.logQuery()
.sort(textScore("textScore"),
ascending("subject"));

testQuery(0, "metaAndSorts", true, query, options);
}

@NonNull
protected <D> List<D> runQuery(@NonNull String queryTemplate, @NonNull Query<D> query,
@NonNull FindOptions options) {
String queryName = format("%s/%s/query.json", prefix(), queryTemplate);
try {

InputStream stream = getClass().getResourceAsStream(queryName);
assertNotNull(stream, "Could not find query template: " + queryName);
Document expectedQuery;
try (InputStreamReader reader = new InputStreamReader(stream)) {
expectedQuery = Document.parse(new BufferedReader(reader).readLine());
}

assertDocumentEquals(query.toDocument(), expectedQuery);

try (var cursor = query.iterator(options)) {
return cursor.toList();
}
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}

@Entity(useDiscriminator = false)
private static class Article {
@Id
int id;
@Text
String subject;
String author;
int views;
double textScore;

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Article)) {
return false;
}
Article article = (Article) o;
return id == article.id && views == article.views && Double.compare(article.textScore, textScore) == 0 &&
Objects.equals(subject, article.subject) && Objects.equals(author, article.author);
}

@Override
public int hashCode() {
return Objects.hash(id, subject, author, views, textScore);
}

@Override
public String toString() {
return format("Article{id=%d, subject='%s', author='%s', views=%d, textScore=%s}", id, subject, author, views, textScore);
}
}
}
Loading

0 comments on commit e4a5062

Please sign in to comment.