Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 693f5dd

Browse files
christophstroblodrotbohm
authored andcommittedMar 17, 2016
DATAMONGO-1245 - Add support for Query By Example.
An explorative approach to QBE trying find possibilities and limitations. We now support querying documents by providing a sample of the given object holding compare values. For the sake of partial matching we flatten out nested structures so we can create different queries for matching like: { _id : 1, nested : { value : "conflux" } } { _id : 1, nested.value : { "conflux" } } This is useful when you want so search using a only partially filled nested document. String matching can be configured to wrap strings with $regex which creates { firstname : { $regex : "^foo", $options: "i" } } when using StringMatchMode.STARTING along with the ignoreCaseOption. DBRefs and geo structures such as Point or GeoJsonPoint is converted to their according structure. Related tickets: DATACMNS-810. Original pull request: #341.
1 parent ece655f commit 693f5dd

23 files changed

+1730
-161
lines changed
 

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java

Lines changed: 59 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.data.annotation.Id;
5252
import org.springframework.data.authentication.UserCredentials;
5353
import org.springframework.data.convert.EntityReader;
54+
import org.springframework.data.domain.Example;
5455
import org.springframework.data.geo.Distance;
5556
import org.springframework.data.geo.GeoResult;
5657
import org.springframework.data.geo.GeoResults;
@@ -340,8 +341,8 @@ public CloseableIterator<T> doInCollection(DBCollection collection) throws Mongo
340341
DBCursor cursor = collection.find(mappedQuery, mappedFields);
341342
QueryCursorPreparer cursorPreparer = new QueryCursorPreparer(query, entityType);
342343

343-
ReadDbObjectCallback<T> readCallback = new ReadDbObjectCallback<T>(mongoConverter, entityType,
344-
collection.getName());
344+
ReadDbObjectCallback<T> readCallback = new ReadDbObjectCallback<T>(mongoConverter, entityType, collection
345+
.getName());
345346

346347
return new CloseableIterableCursorAdapter<T>(cursorPreparer.prepare(cursor), exceptionTranslator, readCallback);
347348
}
@@ -374,8 +375,8 @@ public CommandResult doInDB(DB db) throws MongoException, DataAccessException {
374375
*/
375376
@Deprecated
376377
public CommandResult executeCommand(final DBObject command, final int options) {
377-
return executeCommand(command,
378-
(options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred() : ReadPreference.primary());
378+
return executeCommand(command, (options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred()
379+
: ReadPreference.primary());
379380
}
380381

381382
/*
@@ -421,8 +422,7 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan
421422
* @param preparer allows for customization of the {@link DBCursor} used when iterating over the result set, (apply
422423
* limits, skips and so on).
423424
*/
424-
protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch,
425-
CursorPreparer preparer) {
425+
protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch, CursorPreparer preparer) {
426426

427427
Assert.notNull(query);
428428

@@ -637,6 +637,17 @@ public <T> T findById(Object id, Class<T> entityClass, String collectionName) {
637637
return doFindOne(collectionName, new BasicDBObject(idKey, id), null, entityClass);
638638
}
639639

640+
public <S extends T, T> List<T> findByExample(S sample) {
641+
return findByExample(new Example<S>(sample));
642+
}
643+
644+
@SuppressWarnings("unchecked")
645+
public <S extends T, T> List<T> findByExample(Example<S> sample) {
646+
647+
Assert.notNull(sample, "Sample object must not be null!");
648+
return (List<T>) find(new Query(new Criteria().alike(sample)), sample.getSampleType());
649+
}
650+
640651
public <T> GeoResults<T> geoNear(NearQuery near, Class<T> entityClass) {
641652
return geoNear(near, entityClass, determineCollectionName(entityClass));
642653
}
@@ -672,8 +683,8 @@ public <T> GeoResults<T> geoNear(NearQuery near, Class<T> entityClass, String co
672683
List<Object> results = (List<Object>) commandResult.get("results");
673684
results = results == null ? Collections.emptyList() : results;
674685

675-
DbObjectCallback<GeoResult<T>> callback = new GeoNearResultDbObjectCallback<T>(
676-
new ReadDbObjectCallback<T>(mongoConverter, entityClass, collectionName), near.getMetric());
686+
DbObjectCallback<GeoResult<T>> callback = new GeoNearResultDbObjectCallback<T>(new ReadDbObjectCallback<T>(
687+
mongoConverter, entityClass, collectionName), near.getMetric());
677688
List<GeoResult<T>> result = new ArrayList<GeoResult<T>>(results.size());
678689

679690
int index = 0;
@@ -749,9 +760,8 @@ public long count(final Query query, String collectionName) {
749760
public long count(Query query, Class<?> entityClass, String collectionName) {
750761

751762
Assert.hasText(collectionName);
752-
final DBObject dbObject = query == null ? null
753-
: queryMapper.getMappedObject(query.getQueryObject(),
754-
entityClass == null ? null : mappingContext.getPersistentEntity(entityClass));
763+
final DBObject dbObject = query == null ? null : queryMapper.getMappedObject(query.getQueryObject(),
764+
entityClass == null ? null : mappingContext.getPersistentEntity(entityClass));
755765

756766
return execute(collectionName, new CollectionCallback<Long>() {
757767
public Long doInCollection(DBCollection collection) throws MongoException, DataAccessException {
@@ -1030,8 +1040,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat
10301040
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName,
10311041
entityClass, dbDoc, null);
10321042
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
1033-
WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc)
1034-
: collection.insert(dbDoc, writeConcernToUse);
1043+
WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc) : collection.insert(dbDoc,
1044+
writeConcernToUse);
10351045
handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.INSERT);
10361046
return dbDoc.get(ID_FIELD);
10371047
}
@@ -1052,8 +1062,8 @@ public Void doInCollection(DBCollection collection) throws MongoException, DataA
10521062
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT_LIST, collectionName, null,
10531063
null, null);
10541064
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
1055-
WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList)
1056-
: collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse);
1065+
WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList) : collection.insert(
1066+
dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse);
10571067
handleAnyWriteResultErrors(writeResult, null, MongoActionOperation.INSERT_LIST);
10581068
return null;
10591069
}
@@ -1083,8 +1093,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat
10831093
MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass,
10841094
dbDoc, null);
10851095
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
1086-
WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc)
1087-
: collection.save(dbDoc, writeConcernToUse);
1096+
WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc) : collection.save(dbDoc,
1097+
writeConcernToUse);
10881098
handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.SAVE);
10891099
return dbDoc.get(ID_FIELD);
10901100
}
@@ -1137,10 +1147,10 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
11371147

11381148
increaseVersionForUpdateIfNecessary(entity, update);
11391149

1140-
DBObject queryObj = query == null ? new BasicDBObject()
1141-
: queryMapper.getMappedObject(query.getQueryObject(), entity);
1142-
DBObject updateObj = update == null ? new BasicDBObject()
1143-
: updateMapper.getMappedObject(update.getUpdateObject(), entity);
1150+
DBObject queryObj = query == null ? new BasicDBObject() : queryMapper.getMappedObject(query.getQueryObject(),
1151+
entity);
1152+
DBObject updateObj = update == null ? new BasicDBObject() : updateMapper.getMappedObject(
1153+
update.getUpdateObject(), entity);
11441154

11451155
if (LOGGER.isDebugEnabled()) {
11461156
LOGGER.debug("Calling update using query: {} and update: {} in collection: {}",
@@ -1281,9 +1291,9 @@ private void assertUpdateableIdIfNotSet(Object entity) {
12811291
Object idValue = persistentEntity.getPropertyAccessor(entity).getProperty(idProperty);
12821292

12831293
if (idValue == null && !MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(idProperty.getType())) {
1284-
throw new InvalidDataAccessApiUsageException(
1285-
String.format("Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(),
1286-
entity.getClass().getName()));
1294+
throw new InvalidDataAccessApiUsageException(String.format(
1295+
"Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(), entity.getClass()
1296+
.getName()));
12871297
}
12881298
}
12891299

@@ -1322,12 +1332,12 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException
13221332
WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction);
13231333

13241334
if (LOGGER.isDebugEnabled()) {
1325-
LOGGER.debug("Remove using query: {} in collection: {}.",
1326-
new Object[] { serializeToJsonSafely(dboq), collection.getName() });
1335+
LOGGER.debug("Remove using query: {} in collection: {}.", new Object[] { serializeToJsonSafely(dboq),
1336+
collection.getName() });
13271337
}
13281338

1329-
WriteResult wr = writeConcernToUse == null ? collection.remove(dboq)
1330-
: collection.remove(dboq, writeConcernToUse);
1339+
WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq,
1340+
writeConcernToUse);
13311341

13321342
handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE);
13331343

@@ -1343,8 +1353,8 @@ public <T> List<T> findAll(Class<T> entityClass) {
13431353
}
13441354

13451355
public <T> List<T> findAll(Class<T> entityClass, String collectionName) {
1346-
return executeFindMultiInternal(new FindCallback(null), null,
1347-
new ReadDbObjectCallback<T>(mongoConverter, entityClass, collectionName), collectionName);
1356+
return executeFindMultiInternal(new FindCallback(null), null, new ReadDbObjectCallback<T>(mongoConverter,
1357+
entityClass, collectionName), collectionName);
13481358
}
13491359

13501360
public <T> MapReduceResults<T> mapReduce(String inputCollectionName, String mapFunction, String reduceFunction,
@@ -1360,8 +1370,8 @@ public <T> MapReduceResults<T> mapReduce(String inputCollectionName, String mapF
13601370

13611371
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
13621372
String reduceFunction, Class<T> entityClass) {
1363-
return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, new MapReduceOptions().outputTypeInline(),
1364-
entityClass);
1373+
return mapReduce(query, inputCollectionName, mapFunction, reduceFunction,
1374+
new MapReduceOptions().outputTypeInline(), entityClass);
13651375
}
13661376

13671377
public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName, String mapFunction,
@@ -1372,9 +1382,8 @@ public <T> MapReduceResults<T> mapReduce(Query query, String inputCollectionName
13721382
DBCollection inputCollection = getCollection(inputCollectionName);
13731383

13741384
MapReduceCommand command = new MapReduceCommand(inputCollection, mapFunc, reduceFunc,
1375-
mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(),
1376-
query == null || query.getQueryObject() == null ? null
1377-
: queryMapper.getMappedObject(query.getQueryObject(), null));
1385+
mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), query == null
1386+
|| query.getQueryObject() == null ? null : queryMapper.getMappedObject(query.getQueryObject(), null));
13781387

13791388
copyMapReduceOptionsToCommand(query, mapReduceOptions, command);
13801389

@@ -1710,8 +1719,8 @@ protected <T> T doFindOne(String collectionName, DBObject query, DBObject fields
17101719
mappedFields, entityClass, collectionName);
17111720
}
17121721

1713-
return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields),
1714-
new ReadDbObjectCallback<T>(this.mongoConverter, entityClass, collectionName), collectionName);
1722+
return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), new ReadDbObjectCallback<T>(
1723+
this.mongoConverter, entityClass, collectionName), collectionName);
17151724
}
17161725

17171726
/**
@@ -1725,8 +1734,8 @@ protected <T> T doFindOne(String collectionName, DBObject query, DBObject fields
17251734
* @return the List of converted objects.
17261735
*/
17271736
protected <T> List<T> doFind(String collectionName, DBObject query, DBObject fields, Class<T> entityClass) {
1728-
return doFind(collectionName, query, fields, entityClass, null,
1729-
new ReadDbObjectCallback<T>(this.mongoConverter, entityClass, collectionName));
1737+
return doFind(collectionName, query, fields, entityClass, null, new ReadDbObjectCallback<T>(this.mongoConverter,
1738+
entityClass, collectionName));
17301739
}
17311740

17321741
/**
@@ -1744,8 +1753,8 @@ protected <T> List<T> doFind(String collectionName, DBObject query, DBObject fie
17441753
*/
17451754
protected <T> List<T> doFind(String collectionName, DBObject query, DBObject fields, Class<T> entityClass,
17461755
CursorPreparer preparer) {
1747-
return doFind(collectionName, query, fields, entityClass, preparer,
1748-
new ReadDbObjectCallback<T>(mongoConverter, entityClass, collectionName));
1756+
return doFind(collectionName, query, fields, entityClass, preparer, new ReadDbObjectCallback<T>(mongoConverter,
1757+
entityClass, collectionName));
17491758
}
17501759

17511760
protected <S, T> List<T> doFind(String collectionName, DBObject query, DBObject fields, Class<S> entityClass,
@@ -1898,8 +1907,8 @@ private <T> T executeFindOneInternal(CollectionCallback<DBObject> collectionCall
18981907
DbObjectCallback<T> objectCallback, String collectionName) {
18991908

19001909
try {
1901-
T result = objectCallback
1902-
.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), collectionName)));
1910+
T result = objectCallback.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(),
1911+
collectionName)));
19031912
return result;
19041913
} catch (RuntimeException e) {
19051914
throw potentiallyConvertRuntimeException(e, exceptionTranslator);
@@ -1924,8 +1933,8 @@ private <T> T executeFindOneInternal(CollectionCallback<DBObject> collectionCall
19241933
* @param collectionName the collection to be queried
19251934
* @return
19261935
*/
1927-
private <T> List<T> executeFindMultiInternal(CollectionCallback<DBCursor> collectionCallback, CursorPreparer preparer,
1928-
DbObjectCallback<T> objectCallback, String collectionName) {
1936+
private <T> List<T> executeFindMultiInternal(CollectionCallback<DBCursor> collectionCallback,
1937+
CursorPreparer preparer, DbObjectCallback<T> objectCallback, String collectionName) {
19291938

19301939
try {
19311940

@@ -2015,8 +2024,8 @@ String determineCollectionName(Class<?> entityClass) {
20152024

20162025
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(entityClass);
20172026
if (entity == null) {
2018-
throw new InvalidDataAccessApiUsageException(
2019-
"No Persistent Entity information found for the class " + entityClass.getName());
2027+
throw new InvalidDataAccessApiUsageException("No Persistent Entity information found for the class "
2028+
+ entityClass.getName());
20202029
}
20212030
return entity.getCollection();
20222031
}
@@ -2080,8 +2089,8 @@ private void handleCommandError(CommandResult result, DBObject source) {
20802089
String error = result.getErrorMessage();
20812090
error = error == null ? "NO MESSAGE" : error;
20822091

2083-
throw new InvalidDataAccessApiUsageException(
2084-
"Command execution failed: Error [" + error + "], Command = " + source, ex);
2092+
throw new InvalidDataAccessApiUsageException("Command execution failed: Error [" + error + "], Command = "
2093+
+ source, ex);
20852094
}
20862095
}
20872096

@@ -2277,8 +2286,7 @@ public T doWith(DBObject object) {
22772286

22782287
class UnwrapAndReadDbObjectCallback<T> extends ReadDbObjectCallback<T> {
22792288

2280-
public UnwrapAndReadDbObjectCallback(EntityReader<? super T, DBObject> reader, Class<T> type,
2281-
String collectionName) {
2289+
public UnwrapAndReadDbObjectCallback(EntityReader<? super T, DBObject> reader, Class<T> type, String collectionName) {
22822290
super(reader, type, collectionName);
22832291
}
22842292

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ public Point convert(DBObject source) {
117117

118118
Assert.isTrue(source.keySet().size() == 2, "Source must contain 2 elements");
119119

120+
if (source.containsField("type")) {
121+
return DbObjectToGeoJsonPointConverter.INSTANCE.convert(source);
122+
}
123+
120124
return new Point((Double) source.get("x"), (Double) source.get("y"));
121125
}
122126
}
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert;
17+
18+
import java.util.ArrayList;
19+
import java.util.Arrays;
20+
import java.util.Iterator;
21+
import java.util.List;
22+
import java.util.Map;
23+
import java.util.Stack;
24+
import java.util.regex.Pattern;
25+
26+
import org.springframework.data.domain.Example;
27+
import org.springframework.data.domain.Example.NullHandler;
28+
import org.springframework.data.domain.Example.StringMatcher;
29+
import org.springframework.data.domain.PropertySpecifier;
30+
import org.springframework.data.mapping.PropertyHandler;
31+
import org.springframework.data.mapping.context.MappingContext;
32+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
33+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
34+
import org.springframework.data.mongodb.core.query.MongoRegexCreator;
35+
import org.springframework.data.mongodb.core.query.SerializationUtils;
36+
import org.springframework.util.ObjectUtils;
37+
import org.springframework.util.StringUtils;
38+
39+
import com.mongodb.BasicDBObject;
40+
import com.mongodb.DBObject;
41+
42+
/**
43+
* @author Christoph Strobl
44+
* @since 1.8
45+
*/
46+
public class MongoExampleMapper {
47+
48+
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
49+
private final MongoConverter converter;
50+
51+
public MongoExampleMapper(MongoConverter converter) {
52+
53+
this.converter = converter;
54+
this.mappingContext = converter.getMappingContext();
55+
}
56+
57+
/**
58+
* Returns the given {@link Example} as {@link DBObject} holding matching values extracted from
59+
* {@link Example#getProbe()}.
60+
*
61+
* @param example
62+
* @return
63+
* @since 1.8
64+
*/
65+
public DBObject getMappedExample(Example<?> example) {
66+
return getMappedExample(example, mappingContext.getPersistentEntity(example.getSampleType()));
67+
}
68+
69+
/**
70+
* Returns the given {@link Example} as {@link DBObject} holding matching values extracted from
71+
* {@link Example#getProbe()}.
72+
*
73+
* @param example
74+
* @param entity
75+
* @return
76+
* @since 1.8
77+
*/
78+
public DBObject getMappedExample(Example<?> example, MongoPersistentEntity<?> entity) {
79+
80+
DBObject reference = (DBObject) converter.convertToMongoType(example.getSampleObject());
81+
82+
if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getSampleObject()).getIdentifier() == null) {
83+
reference.removeField(entity.getIdProperty().getFieldName());
84+
}
85+
86+
applyPropertySpecs("", reference, example);
87+
88+
return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, example.getNullHandler()) ? reference : new BasicDBObject(
89+
SerializationUtils.flatMap(reference));
90+
}
91+
92+
private String getMappedPropertyPath(String path, Example<?> example) {
93+
94+
MongoPersistentEntity<?> entity = mappingContext.getPersistentEntity(example.getSampleType());
95+
96+
Iterator<String> parts = Arrays.asList(path.split("\\.")).iterator();
97+
98+
final Stack<MongoPersistentProperty> stack = new Stack<MongoPersistentProperty>();
99+
100+
List<String> resultParts = new ArrayList<String>();
101+
102+
while (parts.hasNext()) {
103+
104+
final String part = parts.next();
105+
MongoPersistentProperty prop = entity.getPersistentProperty(part);
106+
107+
if (prop == null) {
108+
109+
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
110+
111+
@Override
112+
public void doWithPersistentProperty(MongoPersistentProperty property) {
113+
114+
if (property.getFieldName().equals(part)) {
115+
stack.push(property);
116+
}
117+
}
118+
});
119+
120+
if (stack.isEmpty()) {
121+
return "";
122+
}
123+
prop = stack.pop();
124+
}
125+
126+
resultParts.add(prop.getName());
127+
128+
if (prop.isEntity() && mappingContext.hasPersistentEntityFor(prop.getActualType())) {
129+
entity = mappingContext.getPersistentEntity(prop.getActualType());
130+
} else {
131+
break;
132+
}
133+
}
134+
135+
return StringUtils.collectionToDelimitedString(resultParts, ".");
136+
137+
}
138+
139+
private void applyPropertySpecs(String path, DBObject source, Example<?> example) {
140+
141+
if (!(source instanceof BasicDBObject)) {
142+
return;
143+
}
144+
145+
Iterator<Map.Entry<String, Object>> iter = ((BasicDBObject) source).entrySet().iterator();
146+
147+
while (iter.hasNext()) {
148+
149+
Map.Entry<String, Object> entry = iter.next();
150+
151+
if (entry.getKey().equals("_id") && entry.getValue() == null) {
152+
iter.remove();
153+
continue;
154+
}
155+
156+
String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey();
157+
158+
String mappedPropertyPath = getMappedPropertyPath(propertyPath, example);
159+
if (example.isIgnoredPath(propertyPath) || example.isIgnoredPath(mappedPropertyPath)) {
160+
iter.remove();
161+
continue;
162+
}
163+
164+
PropertySpecifier specifier = null;
165+
StringMatcher stringMatcher = example.getDefaultStringMatcher();
166+
Object value = entry.getValue();
167+
boolean ignoreCase = example.isIngnoreCaseEnabled();
168+
169+
if (example.hasPropertySpecifiers()) {
170+
171+
mappedPropertyPath = example.hasPropertySpecifier(propertyPath) ? propertyPath : getMappedPropertyPath(
172+
propertyPath, example);
173+
174+
specifier = example.getPropertySpecifier(mappedPropertyPath);
175+
176+
if (specifier != null) {
177+
if (specifier.hasStringMatcher()) {
178+
stringMatcher = specifier.getStringMatcher();
179+
}
180+
if (specifier.getIgnoreCase() != null) {
181+
ignoreCase = specifier.getIgnoreCase();
182+
}
183+
184+
}
185+
}
186+
187+
// TODO: should a PropertySpecifier outrule the later on string matching?
188+
if (specifier != null) {
189+
190+
value = specifier.transformValue(value);
191+
if (value == null) {
192+
iter.remove();
193+
continue;
194+
}
195+
196+
entry.setValue(value);
197+
}
198+
199+
if (entry.getValue() instanceof String) {
200+
applyStringMatcher(entry, stringMatcher, ignoreCase);
201+
} else if (entry.getValue() instanceof BasicDBObject) {
202+
applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), example);
203+
}
204+
}
205+
}
206+
207+
private void applyStringMatcher(Map.Entry<String, Object> entry, StringMatcher stringMatcher, boolean ignoreCase) {
208+
209+
BasicDBObject dbo = new BasicDBObject();
210+
211+
if (ObjectUtils.nullSafeEquals(StringMatcher.DEFAULT, stringMatcher)) {
212+
213+
if (ignoreCase) {
214+
dbo.put("$regex", Pattern.quote((String) entry.getValue()));
215+
entry.setValue(dbo);
216+
}
217+
} else {
218+
219+
String expression = MongoRegexCreator.INSTANCE.toRegularExpression((String) entry.getValue(),
220+
stringMatcher.getPartType());
221+
dbo.put("$regex", expression);
222+
entry.setValue(dbo);
223+
}
224+
225+
if (ignoreCase) {
226+
dbo.put("$options", "i");
227+
}
228+
}
229+
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.core.convert.ConversionException;
2828
import org.springframework.core.convert.ConversionService;
2929
import org.springframework.core.convert.converter.Converter;
30+
import org.springframework.data.domain.Example;
3031
import org.springframework.data.mapping.Association;
3132
import org.springframework.data.mapping.PersistentEntity;
3233
import org.springframework.data.mapping.PropertyPath;
@@ -70,6 +71,7 @@ private enum MetaMapping {
7071
private final ConversionService conversionService;
7172
private final MongoConverter converter;
7273
private final MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext;
74+
private final MongoExampleMapper exampleMapper;
7375

7476
/**
7577
* Creates a new {@link QueryMapper} with the given {@link MongoConverter}.
@@ -83,6 +85,7 @@ public QueryMapper(MongoConverter converter) {
8385
this.conversionService = converter.getConversionService();
8486
this.converter = converter;
8587
this.mappingContext = converter.getMappingContext();
88+
this.exampleMapper = new MongoExampleMapper(converter);
8689
}
8790

8891
/**
@@ -239,6 +242,10 @@ protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity<?> en
239242
return new BasicDBObject(keyword.getKey(), newConditions);
240243
}
241244

245+
if (keyword.isSample()) {
246+
return exampleMapper.getMappedExample(keyword.<Example<?>> getValue(), entity);
247+
}
248+
242249
return new BasicDBObject(keyword.getKey(), convertSimpleOrDBObject(keyword.getValue(), entity));
243250
}
244251

@@ -254,8 +261,8 @@ protected DBObject getMappedKeyword(Field property, Keyword keyword) {
254261
boolean needsAssociationConversion = property.isAssociation() && !keyword.isExists();
255262
Object value = keyword.getValue();
256263

257-
Object convertedValue = needsAssociationConversion ? convertAssociation(value, property)
258-
: getMappedValue(property.with(keyword.getKey()), value);
264+
Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) : getMappedValue(
265+
property.with(keyword.getKey()), value);
259266

260267
return new BasicDBObject(keyword.key, convertedValue);
261268
}
@@ -477,8 +484,8 @@ public Object convertId(Object id) {
477484
}
478485

479486
try {
480-
return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService.convert(id, ObjectId.class)
481-
: delegateConvertToMongoType(id, null);
487+
return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService
488+
.convert(id, ObjectId.class) : delegateConvertToMongoType(id, null);
482489
} catch (ConversionException o_O) {
483490
return delegateConvertToMongoType(id, null);
484491
}
@@ -566,6 +573,16 @@ public boolean isGeometry() {
566573
return "$geometry".equalsIgnoreCase(key);
567574
}
568575

576+
/**
577+
* Returns wheter the current keyword indicates a sample object.
578+
*
579+
* @return
580+
* @since 1.8
581+
*/
582+
public boolean isSample() {
583+
return "$sample".equalsIgnoreCase(key);
584+
}
585+
569586
public boolean hasIterableValue() {
570587
return value instanceof Iterable;
571588
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.regex.Pattern;
2727

2828
import org.bson.BSON;
29+
import org.springframework.data.domain.Example;
2930
import org.springframework.data.geo.Circle;
3031
import org.springframework.data.geo.Point;
3132
import org.springframework.data.geo.Shape;
@@ -88,6 +89,30 @@ public static Criteria where(String key) {
8889
return new Criteria(key);
8990
}
9091

92+
/**
93+
* Static factory method to create a {@link Criteria} matching an example object.
94+
*
95+
* @param example must not be {@literal null}.
96+
* @return
97+
* @see Criteria#alike(Example)
98+
* @since 1.8
99+
*/
100+
public static Criteria byExample(Object example) {
101+
return byExample(new Example<Object>(example));
102+
}
103+
104+
/**
105+
* Static factory method to create a {@link Criteria} matching an example object.
106+
*
107+
* @param example must not be {@literal null}.
108+
* @return
109+
* @see Criteria#alike(Example)
110+
* @since 1.8
111+
*/
112+
public static Criteria byExample(Example<?> example) {
113+
return new Criteria().alike(example);
114+
}
115+
91116
/**
92117
* Static factory method to create a Criteria using the provided key
93118
*
@@ -191,8 +216,8 @@ public Criteria gte(Object o) {
191216
*/
192217
public Criteria in(Object... o) {
193218
if (o.length > 1 && o[1] instanceof Collection) {
194-
throw new InvalidMongoDbApiUsageException(
195-
"You can only pass in one argument of type " + o[1].getClass().getName());
219+
throw new InvalidMongoDbApiUsageException("You can only pass in one argument of type "
220+
+ o[1].getClass().getName());
196221
}
197222
criteria.put("$in", Arrays.asList(o));
198223
return this;
@@ -498,6 +523,20 @@ public Criteria elemMatch(Criteria c) {
498523
return this;
499524
}
500525

526+
/**
527+
* Creates a criterion using the given object as a pattern.
528+
*
529+
* @param sample
530+
* @return
531+
* @since 1.8
532+
*/
533+
public Criteria alike(Example<?> sample) {
534+
535+
criteria.put("$sample", sample);
536+
this.criteriaChain.add(this);
537+
return this;
538+
}
539+
501540
/**
502541
* Creates an 'or' criteria using the $or operator for all of the provided criteria
503542
* <p>
@@ -543,8 +582,8 @@ public Criteria andOperator(Criteria... criteria) {
543582
private Criteria registerCriteriaChainElement(Criteria criteria) {
544583

545584
if (lastOperatorWasNot()) {
546-
throw new IllegalArgumentException(
547-
"operator $not is not allowed around criteria chain element: " + criteria.getCriteriaObject());
585+
throw new IllegalArgumentException("operator $not is not allowed around criteria chain element: "
586+
+ criteria.getCriteriaObject());
548587
} else {
549588
criteriaChain.add(criteria);
550589
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.query;
17+
18+
import java.util.regex.Pattern;
19+
20+
import org.springframework.data.repository.query.parser.Part.Type;
21+
import org.springframework.util.ObjectUtils;
22+
23+
/**
24+
* @author Christoph Strobl
25+
* @since 1.8
26+
*/
27+
public enum MongoRegexCreator {
28+
29+
INSTANCE;
30+
31+
private static final Pattern PUNCTATION_PATTERN = Pattern.compile("\\p{Punct}");
32+
33+
/**
34+
* Creates a regular expression String to be used with {@code $regex}.
35+
*
36+
* @param source the plain String
37+
* @param type
38+
* @return {@literal source} when {@literal source} or {@literal type} is {@literal null}.
39+
*/
40+
public String toRegularExpression(String source, Type type) {
41+
42+
if (type == null || source == null) {
43+
return source;
44+
}
45+
46+
String regex = prepareAndEscapeStringBeforeApplyingLikeRegex(source, type);
47+
48+
switch (type) {
49+
case STARTING_WITH:
50+
regex = "^" + regex;
51+
break;
52+
case ENDING_WITH:
53+
regex = regex + "$";
54+
break;
55+
case CONTAINING:
56+
case NOT_CONTAINING:
57+
regex = ".*" + regex + ".*";
58+
break;
59+
case SIMPLE_PROPERTY:
60+
case NEGATING_SIMPLE_PROPERTY:
61+
regex = "^" + regex + "$";
62+
default:
63+
}
64+
65+
return regex;
66+
}
67+
68+
private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Type type) {
69+
70+
if (!ObjectUtils.nullSafeEquals(Type.LIKE, type)) {
71+
return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source;
72+
}
73+
74+
if (source.equals("*")) {
75+
return ".*";
76+
}
77+
78+
StringBuilder sb = new StringBuilder();
79+
80+
boolean leadingWildcard = source.startsWith("*");
81+
boolean trailingWildcard = source.endsWith("*");
82+
83+
String valueToUse = source.substring(leadingWildcard ? 1 : 0,
84+
trailingWildcard ? source.length() - 1 : source.length());
85+
86+
if (PUNCTATION_PATTERN.matcher(valueToUse).find()) {
87+
valueToUse = Pattern.quote(valueToUse);
88+
}
89+
90+
if (leadingWildcard) {
91+
sb.append(".*");
92+
}
93+
sb.append(valueToUse);
94+
if (trailingWildcard) {
95+
sb.append(".*");
96+
}
97+
98+
return sb.toString();
99+
}
100+
101+
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,26 +16,92 @@
1616
package org.springframework.data.mongodb.core.query;
1717

1818
import java.util.Collection;
19+
import java.util.Collections;
20+
import java.util.HashMap;
1921
import java.util.Iterator;
2022
import java.util.Map;
2123
import java.util.Map.Entry;
2224

2325
import org.springframework.core.convert.converter.Converter;
2426

27+
import com.mongodb.BasicDBObject;
2528
import com.mongodb.DBObject;
2629
import com.mongodb.util.JSON;
2730

2831
/**
2932
* Utility methods for JSON serialization.
3033
*
3134
* @author Oliver Gierke
35+
* @author Christoph Strobl
3236
*/
3337
public abstract class SerializationUtils {
3438

3539
private SerializationUtils() {
3640

3741
}
3842

43+
/**
44+
* Flattens out a given {@link DBObject}.
45+
*
46+
* <pre>
47+
* <code>
48+
* {
49+
* _id : 1
50+
* nested : { value : "conflux"}
51+
* }
52+
* </code>
53+
* will result in
54+
* <code>
55+
* {
56+
* _id : 1
57+
* nested.value : "conflux"
58+
* }
59+
* </code>
60+
* </pre>
61+
*
62+
* @param source can be {@literal null}.
63+
* @return {@link Collections#emptyMap()} when source is {@literal null}
64+
* @since 1.8
65+
*/
66+
public static Map<String, Object> flatMap(DBObject source) {
67+
68+
if (source == null) {
69+
return Collections.emptyMap();
70+
}
71+
72+
Map<String, Object> result = new HashMap<String, Object>();
73+
toFlatMap("", source, result);
74+
return result;
75+
}
76+
77+
private static void toFlatMap(String currentPath, Object source, Map<String, Object> map) {
78+
79+
if (source instanceof BasicDBObject) {
80+
81+
BasicDBObject dbo = (BasicDBObject) source;
82+
Iterator<Map.Entry<String, Object>> iter = dbo.entrySet().iterator();
83+
String pathPrefix = currentPath.isEmpty() ? "" : currentPath + ".";
84+
85+
while (iter.hasNext()) {
86+
87+
Map.Entry<String, Object> entry = iter.next();
88+
89+
if (entry.getKey().startsWith("$")) {
90+
if (map.containsKey(currentPath)) {
91+
((BasicDBObject) map.get(currentPath)).put(entry.getKey(), entry.getValue());
92+
} else {
93+
map.put(currentPath, new BasicDBObject(entry.getKey(), entry.getValue()));
94+
}
95+
} else {
96+
97+
toFlatMap(pathPrefix + entry.getKey(), entry.getValue(), map);
98+
}
99+
}
100+
} else {
101+
map.put(currentPath, source);
102+
}
103+
}
104+
39105
/**
40106
* Serializes the given object into pseudo-JSON meaning it's trying to create a JSON representation as far as possible
41107
* but falling back to the given object's {@link Object#toString()} method if it's not serializable. Useful for

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2010-2014 the original author or authors.
2+
* Copyright 2010-2016 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,9 @@
1818
import java.io.Serializable;
1919
import java.util.List;
2020

21+
import org.springframework.data.domain.Example;
22+
import org.springframework.data.domain.Page;
23+
import org.springframework.data.domain.Pageable;
2124
import org.springframework.data.domain.Sort;
2225
import org.springframework.data.repository.NoRepositoryBean;
2326
import org.springframework.data.repository.PagingAndSortingRepository;
@@ -71,4 +74,34 @@ public interface MongoRepository<T, ID extends Serializable> extends PagingAndSo
7174
* @since 1.7
7275
*/
7376
<S extends T> List<S> insert(Iterable<S> entities);
77+
78+
/**
79+
* Returns all instances of the type specified by the given {@link Example}.
80+
*
81+
* @param example must not be {@literal null}.
82+
* @return
83+
* @since 1.8
84+
*/
85+
<S extends T> List<T> findAllByExample(Example<S> example);
86+
87+
/**
88+
* Returns all instances of the type specified by the given {@link Example}.
89+
*
90+
* @param example must not be {@literal null}.
91+
* @param sort can be {@literal null}.
92+
* @return all entities sorted by the given options
93+
* @since 1.8
94+
*/
95+
<S extends T> List<T> findAllByExample(Example<S> example, Sort sort);
96+
97+
/**
98+
* Returns a {@link Page} of entities meeting the paging restriction specified by the given {@link Example} limited to
99+
* criteria provided in the {@code Pageable} object.
100+
*
101+
* @param example must not be {@literal null}.
102+
* @param pageable can be {@literal null}.
103+
* @return a {@link Page} of entities
104+
* @since 1.8
105+
*/
106+
<S extends T> Page<T> findAllByExample(Example<S> example, Pageable pageable);
74107
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import java.util.Iterator;
2222
import java.util.List;
2323

24+
import org.springframework.data.domain.Example;
2425
import org.springframework.data.domain.Pageable;
2526
import org.springframework.data.domain.Range;
2627
import org.springframework.data.domain.Sort;
@@ -132,6 +133,11 @@ public TextCriteria getFullText() {
132133
return delegate.getFullText();
133134
}
134135

136+
@Override
137+
public Example<?> getSampleObject() {
138+
return delegate.getSampleObject();
139+
}
140+
135141
/**
136142
* Converts the given value with the underlying {@link MongoWriter}.
137143
*

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.mongodb.repository.query;
1717

18+
import org.springframework.data.domain.Example;
1819
import org.springframework.data.domain.Range;
1920
import org.springframework.data.geo.Distance;
2021
import org.springframework.data.geo.Point;
@@ -60,4 +61,12 @@ public interface MongoParameterAccessor extends ParameterAccessor {
6061
* @since 1.8
6162
*/
6263
Object[] getValues();
64+
65+
/**
66+
* Get the sample for query by example
67+
*
68+
* @return
69+
* @since 1.8
70+
*/
71+
Example<?> getSampleObject();
6372
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121

2222
import org.springframework.core.MethodParameter;
23+
import org.springframework.data.domain.Example;
2324
import org.springframework.data.domain.Range;
2425
import org.springframework.data.geo.Distance;
2526
import org.springframework.data.geo.Point;
@@ -42,6 +43,7 @@ public class MongoParameters extends Parameters<MongoParameters, MongoParameter>
4243
private final int rangeIndex;
4344
private final int maxDistanceIndex;
4445
private final Integer fullTextIndex;
46+
private final int sampleObjectIndex;
4547

4648
private Integer nearIndex;
4749

@@ -69,17 +71,20 @@ public MongoParameters(Method method, boolean isGeoNearMethod) {
6971
} else if (this.nearIndex == null) {
7072
this.nearIndex = -1;
7173
}
74+
75+
this.sampleObjectIndex = parameterTypes.indexOf(Example.class);
7276
}
7377

7478
private MongoParameters(List<MongoParameter> parameters, int maxDistanceIndex, Integer nearIndex,
75-
Integer fullTextIndex, int rangeIndex) {
79+
Integer fullTextIndex, int rangeIndex, int sampleObjectIndex) {
7680

7781
super(parameters);
7882

7983
this.nearIndex = nearIndex;
8084
this.fullTextIndex = fullTextIndex;
8185
this.maxDistanceIndex = maxDistanceIndex;
8286
this.rangeIndex = rangeIndex;
87+
this.sampleObjectIndex = sampleObjectIndex;
8388
}
8489

8590
private final int getNearIndex(List<Class<?>> parameterTypes) {
@@ -182,13 +187,22 @@ public int getRangeIndex() {
182187
return rangeIndex;
183188
}
184189

190+
/**
191+
* @return
192+
* @since 1.8
193+
*/
194+
public int getSampleObjectParameterIndex() {
195+
return sampleObjectIndex;
196+
}
197+
185198
/*
186199
* (non-Javadoc)
187200
* @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List)
188201
*/
189202
@Override
190203
protected MongoParameters createFrom(List<MongoParameter> parameters) {
191-
return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex);
204+
return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex,
205+
this.sampleObjectIndex);
192206
}
193207

194208
private int getTypeIndex(List<TypeInformation<?>> parameterTypes, Class<?> type, Class<?> componentType) {
@@ -240,7 +254,7 @@ class MongoParameter extends Parameter {
240254
@Override
241255
public boolean isSpecialParameter() {
242256
return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter()
243-
|| TextCriteria.class.isAssignableFrom(getType());
257+
|| TextCriteria.class.isAssignableFrom(getType()) || isExample();
244258
}
245259

246260
private boolean isNearParameter() {
@@ -260,6 +274,10 @@ private boolean hasNearAnnotation() {
260274
return parameter.getParameterAnnotation(Near.class) != null;
261275
}
262276

277+
private boolean isExample() {
278+
return Example.class.isAssignableFrom(getType());
279+
}
280+
263281
}
264282

265283
}

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Arrays;
1919
import java.util.List;
2020

21+
import org.springframework.data.domain.Example;
2122
import org.springframework.data.domain.Range;
2223
import org.springframework.data.geo.Distance;
2324
import org.springframework.data.geo.Point;
@@ -125,9 +126,15 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) {
125126
return ((TextCriteria) fullText);
126127
}
127128

128-
throw new IllegalArgumentException(
129-
String.format("Expected full text parameter to be one of String, Term or TextCriteria but found %s.",
130-
ClassUtils.getShortName(fullText.getClass())));
129+
throw new IllegalArgumentException(String.format(
130+
"Expected full text parameter to be one of String, Term or TextCriteria but found %s.",
131+
ClassUtils.getShortName(fullText.getClass())));
132+
}
133+
134+
public Example<?> getSampleObject() {
135+
136+
int index = method.getParameters().getSampleObjectParameterIndex();
137+
return index >= 0 ? (Example<?>) getValue(index) : null;
131138
}
132139

133140
/*

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java

Lines changed: 20 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
3939
import org.springframework.data.mongodb.core.query.Criteria;
4040
import org.springframework.data.mongodb.core.query.CriteriaDefinition;
41+
import org.springframework.data.mongodb.core.query.MongoRegexCreator;
4142
import org.springframework.data.mongodb.core.query.Query;
4243
import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator;
4344
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -46,7 +47,6 @@
4647
import org.springframework.data.repository.query.parser.Part.Type;
4748
import org.springframework.data.repository.query.parser.PartTree;
4849
import org.springframework.util.Assert;
49-
import org.springframework.util.ObjectUtils;
5050

5151
/**
5252
* Custom query creator to create Mongo criterias.
@@ -151,7 +151,20 @@ protected Criteria or(Criteria base, Criteria criteria) {
151151
@Override
152152
protected Query complete(Criteria criteria, Sort sort) {
153153

154-
Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort);
154+
Criteria toUse = null;
155+
if (accessor.getSampleObject() != null) {
156+
toUse = new Criteria().alike(accessor.getSampleObject());
157+
}
158+
159+
if (criteria != null) {
160+
if (toUse == null) {
161+
toUse = criteria;
162+
} else {
163+
toUse.andOperator(criteria);
164+
}
165+
}
166+
167+
Query query = (toUse == null ? new Query() : new Query(toUse)).with(sort);
155168

156169
if (LOG.isDebugEnabled()) {
157170
LOG.debug("Created query " + query);
@@ -285,8 +298,8 @@ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProper
285298

286299
case ALWAYS:
287300
if (path.getType() != String.class) {
288-
throw new IllegalArgumentException(
289-
String.format("Part %s must be of type String but was %s", path, path.getType()));
301+
throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", path,
302+
path.getType()));
290303
}
291304
// fall-through
292305

@@ -372,8 +385,8 @@ private <T> T nextAs(Iterator<Object> iterator, Class<T> type) {
372385
return (T) parameter;
373386
}
374387

375-
throw new IllegalArgumentException(
376-
String.format("Expected parameter type of %s but got %s!", type, parameter.getClass()));
388+
throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type,
389+
parameter.getClass()));
377390
}
378391

379392
private Object[] nextAsArray(Iterator<Object> iterator) {
@@ -390,61 +403,7 @@ private Object[] nextAsArray(Iterator<Object> iterator) {
390403
}
391404

392405
private String toLikeRegex(String source, Part part) {
393-
394-
Type type = part.getType();
395-
String regex = prepareAndEscapeStringBeforeApplyingLikeRegex(source, part);
396-
397-
switch (type) {
398-
case STARTING_WITH:
399-
regex = "^" + regex;
400-
break;
401-
case ENDING_WITH:
402-
regex = regex + "$";
403-
break;
404-
case CONTAINING:
405-
case NOT_CONTAINING:
406-
regex = ".*" + regex + ".*";
407-
break;
408-
case SIMPLE_PROPERTY:
409-
case NEGATING_SIMPLE_PROPERTY:
410-
regex = "^" + regex + "$";
411-
default:
412-
}
413-
414-
return regex;
415-
}
416-
417-
private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Part qpart) {
418-
419-
if (!ObjectUtils.nullSafeEquals(Type.LIKE, qpart.getType())) {
420-
return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source;
421-
}
422-
423-
if ("*".equals(source)) {
424-
return ".*";
425-
}
426-
427-
StringBuilder sb = new StringBuilder();
428-
429-
boolean leadingWildcard = source.startsWith("*");
430-
boolean trailingWildcard = source.endsWith("*");
431-
432-
String valueToUse = source.substring(leadingWildcard ? 1 : 0,
433-
trailingWildcard ? source.length() - 1 : source.length());
434-
435-
if (PUNCTATION_PATTERN.matcher(valueToUse).find()) {
436-
valueToUse = Pattern.quote(valueToUse);
437-
}
438-
439-
if (leadingWildcard) {
440-
sb.append(".*");
441-
}
442-
sb.append(valueToUse);
443-
if (trailingWildcard) {
444-
sb.append(".*");
445-
}
446-
447-
return sb.toString();
406+
return MongoRegexCreator.INSTANCE.toRegularExpression(source, part.getType());
448407
}
449408

450409
private boolean isSpherical(MongoPersistentProperty property) {

‎spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import java.util.List;
2626
import java.util.Set;
2727

28+
import org.springframework.data.domain.Example;
2829
import org.springframework.data.domain.Page;
2930
import org.springframework.data.domain.PageImpl;
3031
import org.springframework.data.domain.Pageable;
@@ -259,6 +260,57 @@ public <S extends T> List<S> insert(Iterable<S> entities) {
259260
return list;
260261
}
261262

263+
/*
264+
* (non-Javadoc)
265+
* @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable)
266+
*/
267+
@Override
268+
public <S extends T> Page<T> findAllByExample(Example<S> example, Pageable pageable) {
269+
270+
Assert.notNull(example, "Sample must not be null!");
271+
272+
Query q = new Query(new Criteria().alike(example)).with(pageable);
273+
274+
long count = mongoOperations.count(q, entityInformation.getJavaType(), entityInformation.getCollectionName());
275+
if (count == 0) {
276+
return new PageImpl<T>(Collections.<T> emptyList());
277+
}
278+
return new PageImpl<T>(mongoOperations.find(q, entityInformation.getJavaType(),
279+
entityInformation.getCollectionName()), pageable, count);
280+
}
281+
282+
/*
283+
* (non-Javadoc)
284+
* @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort)
285+
*/
286+
@Override
287+
public <S extends T> List<T> findAllByExample(Example<S> example, Sort sort) {
288+
289+
Assert.notNull(example, "Sample must not be null!");
290+
291+
Query q = new Query(new Criteria().alike(example));
292+
293+
if (sort != null) {
294+
q.with(sort);
295+
}
296+
297+
return findAll(q);
298+
}
299+
300+
/*
301+
* (non-Javadoc)
302+
* @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example)
303+
*/
304+
@Override
305+
public <S extends T> List<T> findAllByExample(Example<S> example) {
306+
307+
Assert.notNull(example, "Sample must not be null!");
308+
309+
Query q = new Query(new Criteria().alike(example));
310+
311+
return findAll(q);
312+
}
313+
262314
private List<T> findAll(Query query) {
263315

264316
if (query == null) {
@@ -291,4 +343,5 @@ private static <T> List<T> convertIterableToList(Iterable<T> entities) {
291343
private static int tryDetermineRealSizeOrReturn(Iterable<?> iterable, int defaultSize) {
292344
return iterable == null ? 0 : (iterable instanceof Collection) ? ((Collection<?>) iterable).size() : defaultSize;
293345
}
346+
294347
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012 the original author or authors.
2+
* Copyright 2012-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,18 +20,24 @@
2020
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
2121

2222
import java.util.Arrays;
23+
import java.util.Map;
2324

2425
import org.hamcrest.Matcher;
26+
import org.hamcrest.collection.IsMapContaining;
27+
import org.hamcrest.core.Is;
2528
import org.junit.Test;
2629
import org.springframework.data.mongodb.core.query.SerializationUtils;
2730

31+
import com.mongodb.BasicDBList;
2832
import com.mongodb.BasicDBObject;
33+
import com.mongodb.BasicDBObjectBuilder;
2934
import com.mongodb.DBObject;
3035

3136
/**
3237
* Unit tests for {@link SerializationUtils}.
3338
*
3439
* @author Oliver Gierke
40+
* @author Christoph Strobl
3541
*/
3642
public class SerializationUtilsUnitTests {
3743

@@ -60,6 +66,74 @@ public void writesCollection() {
6066
assertThat(serializeToJsonSafely(dbObject), is(expectedOutput));
6167
}
6268

69+
/**
70+
* @see DATAMONGO-1245
71+
*/
72+
@Test
73+
public void flatMapShouldFlatOutNestedStructureCorrectly() {
74+
75+
DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", "conflux")).get();
76+
77+
assertThat(flatMap(dbo), IsMapContaining.<String, Object> hasEntry("_id", 1));
78+
assertThat(flatMap(dbo), IsMapContaining.<String, Object> hasEntry("nested.value", "conflux"));
79+
}
80+
81+
/**
82+
* @see DATAMONGO-1245
83+
*/
84+
@Test
85+
public void flatMapShouldFlatOutNestedStructureWithListCorrectly() {
86+
87+
BasicDBList dbl = new BasicDBList();
88+
dbl.addAll(Arrays.asList("nightwielder", "calamity"));
89+
90+
DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", dbl)).get();
91+
92+
assertThat(flatMap(dbo), IsMapContaining.<String, Object> hasEntry("_id", 1));
93+
assertThat(flatMap(dbo), IsMapContaining.<String, Object> hasEntry("nested.value", dbl));
94+
}
95+
96+
/**
97+
* @see DATAMONGO-1245
98+
*/
99+
@Test
100+
public void flatMapShouldLeaveKeywordsUntouched() {
101+
102+
DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("$regex", "^conflux$"))
103+
.get();
104+
105+
Map<String, Object> map = flatMap(dbo);
106+
107+
assertThat(map, IsMapContaining.<String, Object> hasEntry("_id", 1));
108+
assertThat(map.get("nested"), notNullValue());
109+
assertThat(((Map<String, Object>) map.get("nested")).get("$regex"), Is.<Object> is("^conflux$"));
110+
}
111+
112+
/**
113+
* @see DATAMONGO-1245
114+
*/
115+
@Test
116+
public void flatMapSouldAppendCommandsCorrectly() {
117+
118+
DBObject dbo = new BasicDBObjectBuilder().add("_id", 1)
119+
.add("nested", new BasicDBObjectBuilder().add("$regex", "^conflux$").add("$options", "i").get()).get();
120+
121+
Map<String, Object> map = flatMap(dbo);
122+
123+
assertThat(map, IsMapContaining.<String, Object> hasEntry("_id", 1));
124+
assertThat(map.get("nested"), notNullValue());
125+
assertThat(((Map<String, Object>) map.get("nested")).get("$regex"), Is.<Object> is("^conflux$"));
126+
assertThat(((Map<String, Object>) map.get("nested")).get("$options"), Is.<Object> is("i"));
127+
}
128+
129+
/**
130+
* @see DATAMONGO-1245
131+
*/
132+
@Test
133+
public void flatMapShouldReturnEmptyMapWhenSourceIsNull() {
134+
assertThat(flatMap(null).isEmpty(), is(true));
135+
}
136+
63137
static class Complex {
64138

65139
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java

Lines changed: 449 additions & 0 deletions
Large diffs are not rendered by default.

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.Map;
3030

3131
import org.bson.types.ObjectId;
32+
import org.hamcrest.core.Is;
3233
import org.junit.Before;
3334
import org.junit.Test;
3435
import org.junit.runner.RunWith;
@@ -791,6 +792,8 @@ public void intersectsShouldUseGeoJsonRepresentationCorrectly() {
791792
}
792793

793794
/**
795+
* <<<<<<< HEAD
796+
*
794797
* @see DATAMONGO-1269
795798
*/
796799
@Test
@@ -818,6 +821,40 @@ public void mappingShouldRetainNumericPositionInList() {
818821
assertThat(dbo.containsField("list.1.stringProperty"), is(true));
819822
}
820823

824+
/**
825+
* @see DATAMONGO-1245
826+
*/
827+
@Test
828+
public void exampleShouldBeMappedCorrectly() {
829+
830+
Foo probe = new Foo();
831+
probe.embedded = new EmbeddedClass();
832+
probe.embedded.id = "conflux";
833+
834+
Query query = query(byExample(probe));
835+
836+
DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class));
837+
838+
assertThat(dbo, is(new BasicDBObjectBuilder().add("embedded._id", "conflux").get()));
839+
}
840+
841+
/**
842+
* @see DATAMONGO-1245
843+
*/
844+
@Test
845+
public void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() {
846+
847+
ClassWithGeoTypes probe = new ClassWithGeoTypes();
848+
probe.legacyPoint = new Point(10D, 20D);
849+
850+
Query query = query(byExample(probe));
851+
852+
DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class));
853+
854+
assertThat(dbo.get("legacyPoint.x"), Is.<Object> is(10D));
855+
assertThat(dbo.get("legacyPoint.y"), Is.<Object> is(20D));
856+
}
857+
821858
@Document
822859
public class Foo {
823860
@Id private ObjectId id;
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
/*
2+
* Copyright 2015 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.temp;
17+
18+
import java.net.UnknownHostException;
19+
import java.util.List;
20+
21+
import org.hamcrest.core.Is;
22+
import org.junit.Assert;
23+
import org.junit.Before;
24+
import org.junit.Test;
25+
import org.springframework.data.annotation.Id;
26+
import org.springframework.data.domain.Example;
27+
import org.springframework.data.mongodb.core.MongoTemplate;
28+
import org.springframework.data.mongodb.core.mapping.Document;
29+
import org.springframework.data.mongodb.core.mapping.Field;
30+
import org.springframework.data.mongodb.core.query.Criteria;
31+
import org.springframework.data.mongodb.core.query.Query;
32+
33+
import com.mongodb.MongoClient;
34+
35+
public class QueryByExampleTests {
36+
37+
MongoTemplate template;
38+
39+
@Before
40+
public void setUp() throws UnknownHostException {
41+
42+
template = new MongoTemplate(new MongoClient(), "query-by-example");
43+
template.remove(new Query(), Person.class);
44+
}
45+
46+
/**
47+
* @see DATAMONGO-1245
48+
*/
49+
@Test
50+
public void findByExampleShouldWorkForSimpleProperty() {
51+
52+
init();
53+
54+
Person sample = new Person();
55+
sample.lastname = "stark";
56+
57+
List<Person> result = template.findByExample(sample);
58+
Assert.assertThat(result.size(), Is.is(2));
59+
}
60+
61+
/**
62+
* @see DATAMONGO-1245
63+
*/
64+
@Test
65+
public void findByExampleShouldWorkForMultipleProperties() {
66+
67+
init();
68+
69+
Person sample = new Person();
70+
sample.lastname = "stark";
71+
sample.firstname = "arya";
72+
73+
List<Person> result = template.findByExample(sample);
74+
Assert.assertThat(result.size(), Is.is(1));
75+
}
76+
77+
/**
78+
* @see DATAMONGO-1245
79+
*/
80+
@Test
81+
public void findByExampleShouldWorkForIdProperty() {
82+
83+
init();
84+
85+
Person p4 = new Person();
86+
template.save(p4);
87+
88+
Person sample = new Person();
89+
sample.id = p4.id;
90+
91+
List<Person> result = template.findByExample(sample);
92+
Assert.assertThat(result.size(), Is.is(1));
93+
}
94+
95+
/**
96+
* @see DATAMONGO-1245
97+
*/
98+
@Test
99+
public void findByExampleShouldReturnEmptyListIfNotMatching() {
100+
101+
init();
102+
103+
Person sample = new Person();
104+
sample.firstname = "jon";
105+
sample.firstname = "stark";
106+
107+
List<Person> result = template.findByExample(sample);
108+
Assert.assertThat(result.size(), Is.is(0));
109+
}
110+
111+
/**
112+
* @see DATAMONGO-1245
113+
*/
114+
@Test
115+
public void findByExampleShouldReturnEverythingWhenSampleIsEmpty() {
116+
117+
init();
118+
119+
Person sample = new Person();
120+
121+
List<Person> result = template.findByExample(sample);
122+
Assert.assertThat(result.size(), Is.is(3));
123+
}
124+
125+
/**
126+
* @see DATAMONGO-1245
127+
*/
128+
@Test
129+
public void findByExampleWithCriteria() {
130+
131+
init();
132+
133+
Person sample = new Person();
134+
sample.lastname = "stark";
135+
136+
Query query = new Query(new Criteria().alike(new Example<Person>(sample)).and("firstname").regex("^ary*"));
137+
138+
List<Person> result = template.find(query, Person.class);
139+
Assert.assertThat(result.size(), Is.is(1));
140+
}
141+
142+
public void init() {
143+
144+
Person p1 = new Person();
145+
p1.firstname = "bran";
146+
p1.lastname = "stark";
147+
148+
Person p2 = new Person();
149+
p2.firstname = "jon";
150+
p2.lastname = "snow";
151+
152+
Person p3 = new Person();
153+
p3.firstname = "arya";
154+
p3.lastname = "stark";
155+
156+
template.save(p1);
157+
template.save(p2);
158+
template.save(p3);
159+
}
160+
161+
@Document(collection = "dramatis-personae")
162+
static class Person {
163+
164+
@Id String id;
165+
String firstname;
166+
167+
@Field("last_name") String lastname;
168+
169+
@Override
170+
public String toString() {
171+
return "Person [id=" + id + ", firstname=" + firstname + ", lastname=" + lastname + "]";
172+
}
173+
}
174+
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@
2828
import java.util.stream.Stream;
2929

3030
import org.hamcrest.Matchers;
31+
import org.hamcrest.core.Is;
32+
import org.junit.Assert;
3133
import org.junit.Before;
3234
import org.junit.Ignore;
3335
import org.junit.Test;
3436
import org.junit.runner.RunWith;
3537
import org.springframework.beans.factory.annotation.Autowired;
3638
import org.springframework.dao.DuplicateKeyException;
39+
import org.springframework.data.domain.Example;
3740
import org.springframework.data.domain.Page;
3841
import org.springframework.data.domain.PageRequest;
3942
import org.springframework.data.domain.Range;
@@ -55,6 +58,7 @@
5558
import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension.SampleSecurityContextHolder;
5659
import org.springframework.data.querydsl.QSort;
5760
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
61+
import org.springframework.test.util.ReflectionTestUtils;
5862

5963
/**
6064
* Base class for tests for {@link PersonRepository}.
@@ -175,8 +179,8 @@ public void findsPagedPersons() throws Exception {
175179
@Test
176180
public void executesPagedFinderCorrectly() throws Exception {
177181

178-
Page<Person> page = repository.findByLastnameLike("*a*",
179-
new PageRequest(0, 2, Direction.ASC, "lastname", "firstname"));
182+
Page<Person> page = repository.findByLastnameLike("*a*", new PageRequest(0, 2, Direction.ASC, "lastname",
183+
"firstname"));
180184
assertThat(page.isFirst(), is(true));
181185
assertThat(page.isLast(), is(false));
182186
assertThat(page.getNumberOfElements(), is(2));
@@ -186,8 +190,8 @@ public void executesPagedFinderCorrectly() throws Exception {
186190
@Test
187191
public void executesPagedFinderWithAnnotatedQueryCorrectly() throws Exception {
188192

189-
Page<Person> page = repository.findByLastnameLikeWithPageable(".*a.*",
190-
new PageRequest(0, 2, Direction.ASC, "lastname", "firstname"));
193+
Page<Person> page = repository.findByLastnameLikeWithPageable(".*a.*", new PageRequest(0, 2, Direction.ASC,
194+
"lastname", "firstname"));
191195
assertThat(page.isFirst(), is(true));
192196
assertThat(page.isLast(), is(false));
193197
assertThat(page.getNumberOfElements(), is(2));
@@ -311,8 +315,8 @@ public void findsPeopleByLocationWithinPolygon() {
311315
@Test
312316
public void findsPagedPeopleByPredicate() throws Exception {
313317

314-
Page<Person> page = repository.findAll(person.lastname.contains("a"),
315-
new PageRequest(0, 2, Direction.ASC, "lastname"));
318+
Page<Person> page = repository.findAll(person.lastname.contains("a"), new PageRequest(0, 2, Direction.ASC,
319+
"lastname"));
316320
assertThat(page.isFirst(), is(true));
317321
assertThat(page.isLast(), is(false));
318322
assertThat(page.getNumberOfElements(), is(2));
@@ -398,8 +402,8 @@ public void executesGeoNearQueryForResultsCorrectly() {
398402
dave.setLocation(point);
399403
repository.save(dave);
400404

401-
GeoResults<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
402-
new Distance(2000, Metrics.KILOMETERS));
405+
GeoResults<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
406+
Metrics.KILOMETERS));
403407
assertThat(results.getContent().isEmpty(), is(false));
404408
}
405409

@@ -410,8 +414,8 @@ public void executesGeoPageQueryForResultsCorrectly() {
410414
dave.setLocation(point);
411415
repository.save(dave);
412416

413-
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
414-
new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 20));
417+
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
418+
Metrics.KILOMETERS), new PageRequest(0, 20));
415419
assertThat(results.getContent().isEmpty(), is(false));
416420

417421
// DATAMONGO-607
@@ -621,8 +625,8 @@ public void executesGeoPageQueryForWithPageRequestForPageInBetween() {
621625

622626
repository.save(Arrays.asList(dave, oliver, carter, boyd, leroi));
623627

624-
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
625-
new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2));
628+
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
629+
Metrics.KILOMETERS), new PageRequest(1, 2));
626630

627631
assertThat(results.getContent().isEmpty(), is(false));
628632
assertThat(results.getNumberOfElements(), is(2));
@@ -646,8 +650,8 @@ public void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() {
646650

647651
repository.save(Arrays.asList(dave, oliver, carter));
648652

649-
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
650-
new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2));
653+
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
654+
Metrics.KILOMETERS), new PageRequest(1, 2));
651655
assertThat(results.getContent().isEmpty(), is(false));
652656
assertThat(results.getNumberOfElements(), is(1));
653657
assertThat(results.isFirst(), is(false));
@@ -665,8 +669,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElement() {
665669
dave.setLocation(point);
666670
repository.save(dave);
667671

668-
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
669-
new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 2));
672+
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
673+
Metrics.KILOMETERS), new PageRequest(0, 2));
670674

671675
assertThat(results.getContent().isEmpty(), is(false));
672676
assertThat(results.getNumberOfElements(), is(1));
@@ -684,8 +688,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() {
684688
dave.setLocation(new Point(-73.99171, 40.738868));
685689
repository.save(dave);
686690

687-
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73),
688-
new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2));
691+
GeoPage<Person> results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000,
692+
Metrics.KILOMETERS), new PageRequest(1, 2));
689693

690694
assertThat(results.getContent().isEmpty(), is(true));
691695
assertThat(results.getNumberOfElements(), is(0));
@@ -935,8 +939,8 @@ public void findByCustomQueryLastnameAndStreetInList() {
935939
@Test
936940
public void shouldLimitCollectionQueryToMaxResultsWhenPresent() {
937941

938-
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"),
939-
new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
942+
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
943+
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
940944
List<Person> result = repository.findTop3ByLastnameStartingWith("Dylan");
941945
assertThat(result.size(), is(3));
942946
}
@@ -947,8 +951,8 @@ public void shouldLimitCollectionQueryToMaxResultsWhenPresent() {
947951
@Test
948952
public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() {
949953

950-
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"),
951-
new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
954+
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
955+
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
952956
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(0, 2));
953957
assertThat(result.getContent().size(), is(2));
954958
}
@@ -959,8 +963,8 @@ public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() {
959963
@Test
960964
public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() {
961965

962-
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"),
963-
new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
966+
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
967+
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
964968
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(1, 2));
965969
assertThat(result.getContent().size(), is(1));
966970
}
@@ -971,8 +975,8 @@ public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() {
971975
@Test
972976
public void shouldReturnEmptyWhenPageRequestedPageIsTotallyOutOfScopeForLimit() {
973977

974-
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"),
975-
new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
978+
repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3",
979+
"Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan")));
976980
Page<Person> result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(2, 2));
977981
assertThat(result.getContent().size(), is(0));
978982
}
@@ -1221,4 +1225,41 @@ public void shouldFindByFirstnameForSpELExpressionWithParameterVariableOnly() {
12211225
assertThat(users, hasSize(1));
12221226
assertThat(users.get(0), is(dave));
12231227
}
1228+
1229+
/**
1230+
* @see DATAMONGO-1245
1231+
*/
1232+
@Test
1233+
public void findByExampleShouldResolveStuffCorrectly() {
1234+
1235+
Person sample = new Person();
1236+
sample.setLastname("Matthews");
1237+
1238+
// needed to tweak stuff a bit since some field are automatically set - so we need to undo this
1239+
ReflectionTestUtils.setField(sample, "id", null);
1240+
ReflectionTestUtils.setField(sample, "createdAt", null);
1241+
ReflectionTestUtils.setField(sample, "email", null);
1242+
1243+
Page<Person> result = repository.findAllByExample(new Example<Person>(sample), new PageRequest(0, 10));
1244+
Assert.assertThat(result.getNumberOfElements(), Is.is(2));
1245+
}
1246+
1247+
/**
1248+
* @see DATAMONGO-1245
1249+
*/
1250+
@Test
1251+
public void findAllByExampleShouldResolveStuffCorrectly() {
1252+
1253+
Person sample = new Person();
1254+
sample.setLastname("Matthews");
1255+
1256+
// needed to tweak stuff a bit since some field are automatically set - so we need to undo this
1257+
ReflectionTestUtils.setField(sample, "id", null);
1258+
ReflectionTestUtils.setField(sample, "createdAt", null);
1259+
ReflectionTestUtils.setField(sample, "email", null);
1260+
1261+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
1262+
Assert.assertThat(result.size(), Is.is(2));
1263+
}
1264+
12241265
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@
2727
@Document
2828
public abstract class Contact {
2929

30-
@Id
31-
protected final String id;
30+
@Id protected String id;
3231

3332
public Contact() {
3433
this.id = new ObjectId().toString();

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -334,22 +334,23 @@ public interface PersonRepository extends MongoRepository<Person, String>, Query
334334
*/
335335
@Query("{ firstname : { $in : ?0 }}")
336336
Stream<Person> findByCustomQueryWithStreamingCursorByFirstnames(List<String> firstnames);
337-
337+
338338
/**
339339
* @see DATAMONGO-990
340340
*/
341341
@Query("{ firstname : ?#{[0]}}")
342342
List<Person> findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname);
343-
343+
344344
/**
345345
* @see DATAMONGO-990
346346
*/
347347
@Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }")
348348
List<Person> findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname);
349-
349+
350350
/**
351351
* @see DATAMONGO-990
352352
*/
353353
@Query("{ firstname : :#{#firstname}}")
354354
List<Person> findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname);
355+
355356
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import java.util.Arrays;
1919
import java.util.Iterator;
2020

21+
import org.springframework.data.domain.Example;
2122
import org.springframework.data.domain.Pageable;
2223
import org.springframework.data.domain.Range;
2324
import org.springframework.data.domain.Sort;
@@ -147,4 +148,13 @@ public Object[] getValues() {
147148
public Class<?> getDynamicProjection() {
148149
return null;
149150
}
151+
152+
/*
153+
* (non-Javadoc)
154+
* @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getSampleObject()
155+
*/
156+
@Override
157+
public Example<?> getSampleObject() {
158+
return null;
159+
}
150160
}

‎spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2010-2014 the original author or authors.
2+
* Copyright 2010-2015 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -31,16 +31,26 @@
3131
import org.junit.Test;
3232
import org.junit.runner.RunWith;
3333
import org.springframework.beans.factory.annotation.Autowired;
34+
import org.springframework.data.domain.Example;
35+
import org.springframework.data.domain.Page;
36+
import org.springframework.data.domain.PageRequest;
37+
import org.springframework.data.geo.Point;
3438
import org.springframework.data.mongodb.core.MongoTemplate;
39+
import org.springframework.data.mongodb.core.geo.GeoJsonPoint;
40+
import org.springframework.data.mongodb.core.mapping.Document;
41+
import org.springframework.data.mongodb.repository.Address;
3542
import org.springframework.data.mongodb.repository.Person;
3643
import org.springframework.data.mongodb.repository.Person.Sex;
44+
import org.springframework.data.mongodb.repository.User;
3745
import org.springframework.data.mongodb.repository.query.MongoEntityInformation;
3846
import org.springframework.test.context.ContextConfiguration;
3947
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
48+
import org.springframework.test.util.ReflectionTestUtils;
4049

4150
/**
4251
* @author <a href="mailto:kowsercse@gmail.com">A. B. M. Kowser</a>
4352
* @author Thomas Darimont
53+
* @author Christoph Strobl
4454
*/
4555
@RunWith(SpringJUnit4ClassRunner.class)
4656
@ContextConfiguration("classpath:infrastructure.xml")
@@ -160,6 +170,224 @@ public void shouldInsertMutlipleFromSet() {
160170
assertThatAllReferencePersonsWereStoredCorrectly(idToPerson, saved);
161171
}
162172

173+
/**
174+
* @see DATAMONGO-1245
175+
*/
176+
@Test
177+
public void findByExampleShouldLookUpEntriesCorrectly() {
178+
179+
Person sample = new Person();
180+
sample.setLastname("Matthews");
181+
trimDomainType(sample, "id", "createdAt", "email");
182+
183+
Page<Person> result = repository.findAllByExample(new Example<Person>(sample), new PageRequest(0, 10));
184+
185+
assertThat(result.getContent(), hasItems(dave, oliver));
186+
assertThat(result.getContent(), hasSize(2));
187+
}
188+
189+
/**
190+
* @see DATAMONGO-1245
191+
*/
192+
@Test
193+
public void findAllByExampleShouldLookUpEntriesCorrectly() {
194+
195+
Person sample = new Person();
196+
sample.setLastname("Matthews");
197+
trimDomainType(sample, "id", "createdAt", "email");
198+
199+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
200+
201+
assertThat(result, hasItems(dave, oliver));
202+
assertThat(result, hasSize(2));
203+
}
204+
205+
/**
206+
* @see DATAMONGO-1245
207+
*/
208+
@Test
209+
public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObject() {
210+
211+
dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington"));
212+
repository.save(dave);
213+
214+
oliver.setAddress(new Address("East Capitol St NE & First St SE", "20004", "Washington"));
215+
repository.save(oliver);
216+
217+
Person sample = new Person();
218+
sample.setAddress(dave.getAddress());
219+
trimDomainType(sample, "id", "createdAt", "email");
220+
221+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
222+
223+
assertThat(result, hasItem(dave));
224+
assertThat(result, hasSize(1));
225+
}
226+
227+
/**
228+
* @see DATAMONGO-1245
229+
*/
230+
@Test
231+
public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingPartialNestedObject() {
232+
233+
dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington"));
234+
repository.save(dave);
235+
236+
oliver.setAddress(new Address("East Capitol St NE & First St SE", "20004", "Washington"));
237+
repository.save(oliver);
238+
239+
Person sample = new Person();
240+
sample.setAddress(new Address(null, null, "Washington"));
241+
trimDomainType(sample, "id", "createdAt", "email");
242+
243+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
244+
245+
assertThat(result, hasItems(dave, oliver));
246+
assertThat(result, hasSize(2));
247+
}
248+
249+
/**
250+
* @see DATAMONGO-1245
251+
*/
252+
@Test
253+
public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInStrictMode() {
254+
255+
dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington"));
256+
repository.save(dave);
257+
258+
Person sample = new Person();
259+
sample.setAddress(new Address(null, null, "Washington"));
260+
trimDomainType(sample, "id", "createdAt", "email");
261+
262+
List<Person> result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get());
263+
264+
assertThat(result, empty());
265+
}
266+
267+
/**
268+
* @see DATAMONGO-1245
269+
*/
270+
@Test
271+
public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInStrictMode() {
272+
273+
dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington"));
274+
repository.save(dave);
275+
276+
Person sample = new Person();
277+
sample.setAddress(dave.getAddress());
278+
trimDomainType(sample, "id", "createdAt", "email");
279+
280+
List<Person> result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get());
281+
282+
assertThat(result, hasItem(dave));
283+
assertThat(result, hasSize(1));
284+
}
285+
286+
/**
287+
* @see DATAMONGO-1245
288+
*/
289+
@Test
290+
public void findAllByExampleShouldRespectStringMatchMode() {
291+
292+
Person sample = new Person();
293+
sample.setLastname("Mat");
294+
trimDomainType(sample, "id", "createdAt", "email");
295+
296+
List<Person> result = repository.findAllByExample(Example.newExampleOf(sample).matchStringsStartingWith().get());
297+
298+
assertThat(result, hasItems(dave, oliver));
299+
assertThat(result, hasSize(2));
300+
}
301+
302+
/**
303+
* @see DATAMONGO-1245
304+
*/
305+
@Test
306+
public void findAllByExampleShouldResolveDbRefCorrectly() {
307+
308+
User user = new User();
309+
user.setId("c0nf1ux");
310+
user.setUsername("conflux");
311+
template.save(user);
312+
313+
Person megan = new Person("megan", "tarash");
314+
megan.setCreator(user);
315+
316+
repository.save(megan);
317+
318+
Person sample = new Person();
319+
sample.setCreator(user);
320+
trimDomainType(sample, "id", "createdAt", "email");
321+
322+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
323+
324+
assertThat(result, hasItem(megan));
325+
assertThat(result, hasSize(1));
326+
}
327+
328+
/**
329+
* @see DATAMONGO-1245
330+
*/
331+
@Test
332+
public void findAllByExampleShouldResolveLegacyCoordinatesCorrectly() {
333+
334+
Person megan = new Person("megan", "tarash");
335+
megan.setLocation(new Point(41.85003D, -87.65005D));
336+
337+
repository.save(megan);
338+
339+
Person sample = new Person();
340+
sample.setLocation(megan.getLocation());
341+
trimDomainType(sample, "id", "createdAt", "email");
342+
343+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
344+
345+
assertThat(result, hasItem(megan));
346+
assertThat(result, hasSize(1));
347+
}
348+
349+
/**
350+
* @see DATAMONGO-1245
351+
*/
352+
@Test
353+
public void findAllByExampleShouldResolveGeoJsonCoordinatesCorrectly() {
354+
355+
Person megan = new Person("megan", "tarash");
356+
megan.setLocation(new GeoJsonPoint(41.85003D, -87.65005D));
357+
358+
repository.save(megan);
359+
360+
Person sample = new Person();
361+
sample.setLocation(megan.getLocation());
362+
trimDomainType(sample, "id", "createdAt", "email");
363+
364+
List<Person> result = repository.findAllByExample(new Example<Person>(sample));
365+
366+
assertThat(result, hasItem(megan));
367+
assertThat(result, hasSize(1));
368+
}
369+
370+
/**
371+
* @see DATAMONGO-1245
372+
*/
373+
@Test
374+
public void findAllByExampleShouldProcessInheritanceCorrectly() {
375+
376+
PersonExtended sample = new PersonExtended() {};
377+
sample.setLastname("Matthews");
378+
trimDomainType(sample, "id", "createdAt", "email");
379+
380+
List<Person> result = repository.findAllByExample(new Example<PersonExtended>(sample));
381+
382+
assertThat(result, hasItems(dave, oliver));
383+
assertThat(result, hasSize(2));
384+
}
385+
386+
@Document(collection = "customizedPerson")
387+
static class PersonExtended extends Person {
388+
389+
}
390+
163391
private void assertThatAllReferencePersonsWereStoredCorrectly(Map<String, Person> references, List<Person> saved) {
164392

165393
for (Person person : saved) {
@@ -168,6 +396,13 @@ private void assertThatAllReferencePersonsWereStoredCorrectly(Map<String, Person
168396
}
169397
}
170398

399+
private void trimDomainType(Object source, String... attributes) {
400+
401+
for (String attribute : attributes) {
402+
ReflectionTestUtils.setField(source, attribute, null);
403+
}
404+
}
405+
171406
private static class CustomizedPersonInformation implements MongoEntityInformation<Person, String> {
172407

173408
@Override

0 commit comments

Comments
 (0)
Please sign in to comment.