From 69effc90c27932e05deac9269de68b7f3a30f7e9 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 26 Jun 2015 10:29:22 +0200 Subject: [PATCH 01/14] DATAMONGO-1245 - Add support for QBE. Prepare issue branch. --- pom.xml | 4 ++-- spring-data-mongodb-cross-store/pom.xml | 4 ++-- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb-log4j/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index e62904016c..25bfb6ba96 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT pom Spring Data MongoDB @@ -28,7 +28,7 @@ multi spring-data-mongodb - 1.12.0.BUILD-SNAPSHOT + 1.12.0.DATACMNS-810-SNAPSHOT 2.14.0 2.13.0 diff --git a/spring-data-mongodb-cross-store/pom.xml b/spring-data-mongodb-cross-store/pom.xml index fd36debedd..3466a5249d 100644 --- a/spring-data-mongodb-cross-store/pom.xml +++ b/spring-data-mongodb-cross-store/pom.xml @@ -6,7 +6,7 @@ org.springframework.data spring-data-mongodb-parent - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT ../pom.xml @@ -48,7 +48,7 @@ org.springframework.data spring-data-mongodb - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 28c91bc332..fdfe1ffb59 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-log4j/pom.xml b/spring-data-mongodb-log4j/pom.xml index dfe146ff96..398db82286 100644 --- a/spring-data-mongodb-log4j/pom.xml +++ b/spring-data-mongodb-log4j/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 0fcdb2f39f..9749f026ea 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -11,7 +11,7 @@ org.springframework.data spring-data-mongodb-parent - 1.9.0.BUILD-SNAPSHOT + 1.9.0.DATAMONGO-1245-SNAPSHOT ../pom.xml From 12f5ad81f78ea257d9753bbf8c94cc6a130e9a7a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 24 Jun 2015 15:47:58 +0200 Subject: [PATCH 02/14] DATAMONGO-1245 - Add support for Query By Example. !!! ATTENTION !!! This is just an explorative approach to QBE trying find possibilities and limitations. Since o.s.d.domain.Example needs to be in spring-data-commons one has to run the build with bundlor disabled '-Dbundlor.enabled=false'. !!! ATTENTION !!! 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. --- .../springframework/data/domain/Example.java | 149 ++++++++++ .../data/mongodb/core/MongoTemplate.java | 12 + .../mongodb/core/convert/GeoConverters.java | 4 + .../mongodb/core/convert/QueryMapper.java | 109 ++++++- .../data/mongodb/core/query/Criteria.java | 47 ++- .../core/query/SerializationUtils.java | 68 ++++- .../mongodb/repository/MongoRepository.java | 18 ++ .../query/ConvertingParameterAccessor.java | 6 + .../query/MongoParameterAccessor.java | 9 + .../repository/query/MongoParameters.java | 24 +- .../MongoParametersParameterAccessor.java | 13 +- .../repository/query/MongoQueryCreator.java | 23 +- .../support/SimpleMongoRepository.java | 35 +++ .../core/SerializationUtilsUnitTests.java | 76 ++++- .../core/convert/QueryMapperUnitTests.java | 276 ++++++++++++++++++ .../core/temp/QueryByExampleTests.java | 174 +++++++++++ ...tractPersonRepositoryIntegrationTests.java | 53 ++-- .../data/mongodb/repository/Contact.java | 3 +- .../mongodb/repository/PersonRepository.java | 7 +- .../PersonRepositoryIntegrationTests.java | 48 ++- .../query/StubParameterAccessor.java | 10 + .../support/SimpleMongoRepositoryTests.java | 220 +++++++++++++- 22 files changed, 1330 insertions(+), 54 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java new file mode 100644 index 0000000000..31a7d54740 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * @author Christoph Strobl + * @param + */ +public class Example { + + private final T probe; + private ObjectMatchMode objectMatchMode = ObjectMatchMode.LENIENT; + private StringMatchMode stringMatchMode = StringMatchMode.DEFAULT; + private boolean ignoreCaseEnabled = false; + + public Example(S probe) { + + Assert.notNull(probe, "Probe must not be null!"); + this.probe = probe; + } + + public T getProbe() { + return probe; + } + + public ObjectMatchMode getObjectMatchMode() { + return objectMatchMode; + } + + public StringMatchMode getStringMatchMode() { + return stringMatchMode; + } + + public boolean isIngnoreCaseEnabled() { + return this.ignoreCaseEnabled; + } + + @SuppressWarnings("unchecked") + public Class getProbeType() { + return (Class) ClassUtils.getUserClass(probe.getClass()); + } + + public static Example example(S probe) { + return new Example(probe); + } + + public static class ExampleBuilder { + + private Example example; + + public ExampleBuilder(T probe) { + example = new Example(probe); + } + + public ExampleBuilder objectMatchMode(ObjectMatchMode matchMode) { + + example.objectMatchMode = matchMode == null ? ObjectMatchMode.LENIENT : matchMode; + return this; + } + + public ExampleBuilder stringMatchMode(StringMatchMode matchMode) { + + example.stringMatchMode = matchMode == null ? StringMatchMode.DEFAULT : matchMode; + return this; + } + + public ExampleBuilder stringMatchMode(StringMatchMode matchMode, boolean ignoreCase) { + + example.stringMatchMode = matchMode == null ? StringMatchMode.DEFAULT : matchMode; + example.ignoreCaseEnabled = ignoreCase; + return this; + } + + public Example get() { + return this.example; + } + } + + /** + * Match modes indicates inclusion of complex objects. + * + * @author Christoph Strobl + */ + public static enum ObjectMatchMode { + /** + * Strict matching will use partially filled objects as reference. + */ + STRICT, + /** + * Lenient matching will inspected nested objects and extract path if needed. + */ + LENIENT + } + + /** + * Match modes indicates treatment of {@link String} values. + * + * @author Christoph Strobl + */ + public static enum StringMatchMode { + + /** + * Store specific default. + */ + DEFAULT, + /** + * Matches the exact string + */ + EXACT, + /** + * Matches string starting with pattern + */ + STARTING, + /** + * Matches string ending with pattern + */ + ENDING, + /** + * Matches string containing pattern + */ + CONTAINING, + /** + * Treats strings as regular expression patterns + */ + REGEX + } + + // TODO: add default null handling + // TODO: add default String handling + // TODO: add per field null handling + // TODO: add per field String handling + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 8bc83ff0b1..04a6288c83 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -51,6 +51,7 @@ import org.springframework.data.annotation.Id; import org.springframework.data.authentication.UserCredentials; import org.springframework.data.convert.EntityReader; +import org.springframework.data.domain.Example; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; @@ -638,6 +639,17 @@ public T findById(Object id, Class entityClass, String collectionName) { return doFindOne(collectionName, new BasicDBObject(idKey, id), null, entityClass); } + public List findByExample(S sample) { + return findByExample(new Example(sample)); + } + + @SuppressWarnings("unchecked") + public List findByExample(Example sample) { + + Assert.notNull(sample, "Sample object must not be null!"); + return (List) find(new Query(new Criteria().alike(sample)), sample.getProbeType()); + } + public GeoResults geoNear(NearQuery near, Class entityClass) { return geoNear(near, entityClass, determineCollectionName(entityClass)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java index 7c6e1229ce..bcbdef5e24 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java @@ -117,6 +117,10 @@ public Point convert(DBObject source) { Assert.isTrue(source.keySet().size() == 2, "Source must contain 2 elements"); + if (source.containsField("type")) { + return DbObjectToGeoJsonPointConverter.INSTANCE.convert(source); + } + return new Point((Double) source.get("x"), (Double) source.get("y")); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 490325461b..046b155608 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -20,13 +20,18 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.regex.Pattern; import org.bson.types.ObjectId; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Example.ObjectMatchMode; +import org.springframework.data.domain.Example.StringMatchMode; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PropertyPath; @@ -39,9 +44,11 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -239,9 +246,93 @@ protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity en return new BasicDBObject(keyword.getKey(), newConditions); } + if (keyword.isSample()) { + return getMappedExample(keyword.> getValue(), entity); + } + return new BasicDBObject(keyword.getKey(), convertSimpleOrDBObject(keyword.getValue(), entity)); } + /** + * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from + * {@link Example#getProbe()}. + * + * @param example + * @param entity + * @return + * @since 1.8 + */ + protected DBObject getMappedExample(Example example, MongoPersistentEntity entity) { + + DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe()); + + if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) { + reference.removeField(entity.getIdProperty().getFieldName()); + } + + if (!ObjectUtils.nullSafeEquals(StringMatchMode.DEFAULT, example.getStringMatchMode()) + || example.isIngnoreCaseEnabled()) { + applyStringPattern(reference, example); + } + + return ObjectUtils.nullSafeEquals(ObjectMatchMode.STRICT, example.getObjectMatchMode()) ? reference + : new BasicDBObject(SerializationUtils.flatMap(reference)); + } + + private void applyStringPattern(DBObject source, Example example) { + + if (!(source instanceof BasicDBObject)) { + return; + } + Iterator> iter = ((BasicDBObject) source).entrySet().iterator(); + + while (iter.hasNext()) { + + Map.Entry entry = iter.next(); + if (entry.getValue() instanceof String) { + + // TODO: extract common stuff from MongoQueryCreator + BasicDBObject dbo = new BasicDBObject(); + switch (example.getStringMatchMode()) { + + case REGEX: + dbo.put("$regex", entry.getValue()); + entry.setValue(dbo); + break; + case DEFAULT: + dbo.put("$regex", Pattern.quote((String) entry.getValue())); + entry.setValue(dbo); + break; + case CONTAINING: + dbo.put("$regex", ".*" + entry.getValue() + ".*"); + entry.setValue(dbo); + break; + case STARTING: + dbo.put("$regex", "^" + entry.getValue()); + entry.setValue(dbo); + break; + case ENDING: + dbo.put("$regex", entry.getValue() + "$"); + entry.setValue(dbo); + break; + case EXACT: + dbo.put("$regex", "^" + entry.getValue() + "$"); + entry.setValue(dbo); + break; + default: + } + + // sometimes order matters in MongoDB so make sure to add $options after $regex. + if (example.isIngnoreCaseEnabled()) { + dbo.put("$options", "i"); + } + + } else if (entry.getValue() instanceof BasicDBObject) { + applyStringPattern((BasicDBObject) entry.getValue(), example); + } + } + } + /** * Returns the mapped keyword considered defining a criteria for the given property. * @@ -254,8 +345,8 @@ protected DBObject getMappedKeyword(Field property, Keyword keyword) { boolean needsAssociationConversion = property.isAssociation() && !keyword.isExists(); Object value = keyword.getValue(); - Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) - : getMappedValue(property.with(keyword.getKey()), value); + Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) : getMappedValue( + property.with(keyword.getKey()), value); return new BasicDBObject(keyword.key, convertedValue); } @@ -477,8 +568,8 @@ public Object convertId(Object id) { } try { - return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService.convert(id, ObjectId.class) - : delegateConvertToMongoType(id, null); + return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService + .convert(id, ObjectId.class) : delegateConvertToMongoType(id, null); } catch (ConversionException o_O) { return delegateConvertToMongoType(id, null); } @@ -566,6 +657,16 @@ public boolean isGeometry() { return "$geometry".equalsIgnoreCase(key); } + /** + * Returns wheter the current keyword indicates a sample object. + * + * @return + * @since 1.8 + */ + public boolean isSample() { + return "$sample".equalsIgnoreCase(key); + } + public boolean hasIterableValue() { return value instanceof Iterable; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index aa67852ffb..1bbf306a82 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -26,6 +26,7 @@ import java.util.regex.Pattern; import org.bson.BSON; +import org.springframework.data.domain.Example; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; import org.springframework.data.geo.Shape; @@ -88,6 +89,30 @@ public static Criteria where(String key) { return new Criteria(key); } + /** + * Static factory method to create a {@link Criteria} matching an example object. + * + * @param example must not be {@literal null}. + * @return + * @see Criteria#alike(Example) + * @since 1.8 + */ + public static Criteria byExample(Object example) { + return byExample(new Example(example)); + } + + /** + * Static factory method to create a {@link Criteria} matching an example object. + * + * @param example must not be {@literal null}. + * @return + * @see Criteria#alike(Example) + * @since 1.8 + */ + public static Criteria byExample(Example example) { + return new Criteria().alike(example); + } + /** * Static factory method to create a Criteria using the provided key * @@ -191,8 +216,8 @@ public Criteria gte(Object o) { */ public Criteria in(Object... o) { if (o.length > 1 && o[1] instanceof Collection) { - throw new InvalidMongoDbApiUsageException( - "You can only pass in one argument of type " + o[1].getClass().getName()); + throw new InvalidMongoDbApiUsageException("You can only pass in one argument of type " + + o[1].getClass().getName()); } criteria.put("$in", Arrays.asList(o)); return this; @@ -498,6 +523,20 @@ public Criteria elemMatch(Criteria c) { return this; } + /** + * Creates a criterion using the given object as a pattern. + * + * @param sample + * @return + * @since 1.8 + */ + public Criteria alike(Example sample) { + + criteria.put("$sample", sample); + this.criteriaChain.add(this); + return this; + } + /** * Creates an 'or' criteria using the $or operator for all of the provided criteria *

@@ -543,8 +582,8 @@ public Criteria andOperator(Criteria... criteria) { private Criteria registerCriteriaChainElement(Criteria criteria) { if (lastOperatorWasNot()) { - throw new IllegalArgumentException( - "operator $not is not allowed around criteria chain element: " + criteria.getCriteriaObject()); + throw new IllegalArgumentException("operator $not is not allowed around criteria chain element: " + + criteria.getCriteriaObject()); } else { criteriaChain.add(criteria); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java index b11948220c..efc904b03a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.data.mongodb.core.query; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import org.springframework.core.convert.converter.Converter; +import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.util.JSON; @@ -29,6 +32,7 @@ * Utility methods for JSON serialization. * * @author Oliver Gierke + * @author Christoph Strobl */ public abstract class SerializationUtils { @@ -36,6 +40,68 @@ private SerializationUtils() { } + /** + * Flattens out a given {@link DBObject}. + * + *

+	 * 
+	 * {
+	 *   _id : 1
+	 *   nested : { value : "conflux"}
+	 * }
+	 * 
+	 * will result in 
+	 * 
+	 * {
+	 *   _id : 1
+	 *   nested.value : "conflux"
+	 * }
+	 * 
+	 * 
+ * + * @param source can be {@literal null}. + * @return {@link Collections#emptyMap()} when source is {@literal null} + * @since 1.8 + */ + public static Map flatMap(DBObject source) { + + if (source == null) { + return Collections.emptyMap(); + } + + Map result = new HashMap(); + toFlatMap("", source, result); + return result; + } + + private static void toFlatMap(String currentPath, Object source, Map map) { + + if (source instanceof BasicDBObject) { + + BasicDBObject dbo = (BasicDBObject) source; + Iterator> iter = dbo.entrySet().iterator(); + String pathPrefix = currentPath.isEmpty() ? "" : currentPath + "."; + + while (iter.hasNext()) { + + Map.Entry entry = iter.next(); + + if (entry.getKey().startsWith("$")) { + if (map.containsKey(currentPath)) { + ((BasicDBObject) map.get(currentPath)).put(entry.getKey(), entry.getValue()); + } else { + map.put(currentPath, new BasicDBObject(entry.getKey(), entry.getValue())); + } + } else { + + toFlatMap(pathPrefix + entry.getKey(), entry.getValue(), map); + } + } + } else { + map.put(currentPath, source); + } + } + /** * Serializes the given object into pseudo-JSON meaning it's trying to create a JSON representation as far as possible * but falling back to the given object's {@link Object#toString()} method if it's not serializable. Useful for diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java index c056d61cb8..2dfeae27e5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java @@ -18,6 +18,9 @@ import java.io.Serializable; import java.util.List; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; @@ -71,4 +74,19 @@ public interface MongoRepository extends PagingAndSo * @since 1.7 */ List insert(Iterable entities); + + /** + * @param example + * @return + * @since 1.8 + */ + List findAllByExample(Example example); + + /** + * @param example + * @param pageable + * @return + * @since 1.8 + */ + Page findByExample(Example example, Pageable pageable); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index 15d0cd25e6..d6e8dfbd6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -21,6 +21,7 @@ import java.util.Iterator; import java.util.List; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; @@ -132,6 +133,11 @@ public TextCriteria getFullText() { return delegate.getFullText(); } + @Override + public Example getSampleObject() { + return delegate.getSampleObject(); + } + /** * Converts the given value with the underlying {@link MongoWriter}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 6c67778e30..a64793e645 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -60,4 +61,12 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @since 1.8 */ Object[] getValues(); + + /** + * Get the sample for query by example + * + * @return + * @since 1.8 + */ + Example getSampleObject(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 1e36047b94..3173011451 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -20,6 +20,7 @@ import java.util.List; import org.springframework.core.MethodParameter; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -42,6 +43,7 @@ public class MongoParameters extends Parameters private final int rangeIndex; private final int maxDistanceIndex; private final Integer fullTextIndex; + private final int sampleObjectIndex; private Integer nearIndex; @@ -69,10 +71,12 @@ public MongoParameters(Method method, boolean isGeoNearMethod) { } else if (this.nearIndex == null) { this.nearIndex = -1; } + + this.sampleObjectIndex = parameterTypes.indexOf(Example.class); } private MongoParameters(List parameters, int maxDistanceIndex, Integer nearIndex, - Integer fullTextIndex, int rangeIndex) { + Integer fullTextIndex, int rangeIndex, int sampleObjectIndex) { super(parameters); @@ -80,6 +84,7 @@ private MongoParameters(List parameters, int maxDistanceIndex, I this.fullTextIndex = fullTextIndex; this.maxDistanceIndex = maxDistanceIndex; this.rangeIndex = rangeIndex; + this.sampleObjectIndex = sampleObjectIndex; } private final int getNearIndex(List> parameterTypes) { @@ -182,13 +187,22 @@ public int getRangeIndex() { return rangeIndex; } + /** + * @return + * @since 1.8 + */ + public int getSampleObjectParameterIndex() { + return sampleObjectIndex; + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) */ @Override protected MongoParameters createFrom(List parameters) { - return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex); + return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex, + this.sampleObjectIndex); } private int getTypeIndex(List> parameterTypes, Class type, Class componentType) { @@ -240,7 +254,7 @@ class MongoParameter extends Parameter { @Override public boolean isSpecialParameter() { return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter() - || TextCriteria.class.isAssignableFrom(getType()); + || TextCriteria.class.isAssignableFrom(getType()) || isExample(); } private boolean isNearParameter() { @@ -260,6 +274,10 @@ private boolean hasNearAnnotation() { return parameter.getParameterAnnotation(Near.class) != null; } + private boolean isExample() { + return Example.class.isAssignableFrom(getType()); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index 7f8ac79b4f..9cbe003e3d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.List; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -125,9 +126,15 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) { return ((TextCriteria) fullText); } - throw new IllegalArgumentException( - String.format("Expected full text parameter to be one of String, Term or TextCriteria but found %s.", - ClassUtils.getShortName(fullText.getClass()))); + throw new IllegalArgumentException(String.format( + "Expected full text parameter to be one of String, Term or TextCriteria but found %s.", + ClassUtils.getShortName(fullText.getClass()))); + } + + public Example getSampleObject() { + + int index = method.getParameters().getSampleObjectParameterIndex(); + return index >= 0 ? (Example) getValue(index) : null; } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index a0cb9aa00a..8ef7c11507 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -151,7 +151,20 @@ protected Criteria or(Criteria base, Criteria criteria) { @Override protected Query complete(Criteria criteria, Sort sort) { - Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort); + Criteria toUse = null; + if (accessor.getSampleObject() != null) { + toUse = new Criteria().alike(accessor.getSampleObject()); + } + + if (criteria != null) { + if (toUse == null) { + toUse = criteria; + } else { + toUse.andOperator(criteria); + } + } + + Query query = (toUse == null ? new Query() : new Query(toUse)).with(sort); if (LOG.isDebugEnabled()) { LOG.debug("Created query " + query); @@ -286,8 +299,8 @@ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProper case ALWAYS: if (path.getType() != String.class) { - throw new IllegalArgumentException( - String.format("Part %s must be of type String but was %s", path, path.getType())); + throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", path, + path.getType())); } // fall-through @@ -373,8 +386,8 @@ private T nextAs(Iterator iterator, Class type) { return (T) parameter; } - throw new IllegalArgumentException( - String.format("Expected parameter type of %s but got %s!", type, parameter.getClass())); + throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type, + parameter.getClass())); } private Object[] nextAsArray(PotentiallyConvertingIterator iterator, MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 0b768343d8..f788044493 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Set; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -259,6 +260,39 @@ public List insert(Iterable entities) { return list; } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoRepository#findByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) + */ + @Override + public Page findByExample(Example example, Pageable pageable) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)).with(pageable); + + long count = mongoOperations.count(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + if (count == 0) { + return new PageImpl(Collections. emptyList()); + } + return new PageImpl(mongoOperations.find(q, entityInformation.getJavaType(), + entityInformation.getCollectionName()), pageable, count); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example) + */ + @Override + public List findAllByExample(Example example) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)); + + return findAll(q); + } + private List findAll(Query query) { if (query == null) { @@ -291,4 +325,5 @@ private static List convertIterableToList(Iterable entities) { private static int tryDetermineRealSizeOrReturn(Iterable iterable, int defaultSize) { return iterable == null ? 0 : (iterable instanceof Collection) ? ((Collection) iterable).size() : defaultSize; } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java index 78ef19c037..231070d34f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,18 +20,24 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import java.util.Arrays; +import java.util.Map; import org.hamcrest.Matcher; +import org.hamcrest.collection.IsMapContaining; +import org.hamcrest.core.Is; import org.junit.Test; import org.springframework.data.mongodb.core.query.SerializationUtils; +import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; import com.mongodb.DBObject; /** * Unit tests for {@link SerializationUtils}. * * @author Oliver Gierke + * @author Christoph Strobl */ public class SerializationUtilsUnitTests { @@ -60,6 +66,74 @@ public void writesCollection() { assertThat(serializeToJsonSafely(dbObject), is(expectedOutput)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void flatMapShouldFlatOutNestedStructureCorrectly() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", "conflux")).get(); + + assertThat(flatMap(dbo), IsMapContaining. hasEntry("_id", 1)); + assertThat(flatMap(dbo), IsMapContaining. hasEntry("nested.value", "conflux")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flatMapShouldFlatOutNestedStructureWithListCorrectly() { + + BasicDBList dbl = new BasicDBList(); + dbl.addAll(Arrays.asList("nightwielder", "calamity")); + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", dbl)).get(); + + assertThat(flatMap(dbo), IsMapContaining. hasEntry("_id", 1)); + assertThat(flatMap(dbo), IsMapContaining. hasEntry("nested.value", dbl)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flatMapShouldLeaveKeywordsUntouched() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("$regex", "^conflux$")) + .get(); + + Map map = flatMap(dbo); + + assertThat(map, IsMapContaining. hasEntry("_id", 1)); + assertThat(map.get("nested"), notNullValue()); + assertThat(((Map) map.get("nested")).get("$regex"), Is. is("^conflux$")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flatMapSouldAppendCommandsCorrectly() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1) + .add("nested", new BasicDBObjectBuilder().add("$regex", "^conflux$").add("$options", "i").get()).get(); + + Map map = flatMap(dbo); + + assertThat(map, IsMapContaining. hasEntry("_id", 1)); + assertThat(map.get("nested"), notNullValue()); + assertThat(((Map) map.get("nested")).get("$regex"), Is. is("^conflux$")); + assertThat(((Map) map.get("nested")).get("$options"), Is. is("i")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flatMapShouldReturnEmptyMapWhenSourceIsNull() { + assertThat(flatMap(null).isEmpty(), is(true)); + } + static class Complex { } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 8cf5852b76..82829e0144 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.springframework.data.domain.Example.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; @@ -27,14 +28,20 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; import org.bson.types.ObjectId; +import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Example.ExampleBuilder; +import org.springframework.data.domain.Example.ObjectMatchMode; +import org.springframework.data.domain.Example.StringMatchMode; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.Point; @@ -791,6 +798,8 @@ public void intersectsShouldUseGeoJsonRepresentationCorrectly() { } /** + * <<<<<<< HEAD + * * @see DATAMONGO-1269 */ @Test @@ -818,6 +827,273 @@ public void mappingShouldRetainNumericPositionInList() { assertThat(dbo.containsField("list.1.stringProperty"), is(true)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsSet() { + + FlatDocument probe = new FlatDocument(); + probe.id = "steelheart"; + Example sample = example(probe); + + Query query = query(byExample(sample)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("_id", "steelheart").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenMultipleValuesSet() { + + FlatDocument probe = new FlatDocument(); + probe.id = "steelheart"; + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("_id", "steelheart").add("stringValue", "firefight").add("intValue", 100) + .get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsNotSet() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "firefight").add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenListHasValues() { + + FlatDocument probe = new FlatDocument(); + probe.listOfString = Arrays.asList("Prof", "Tia", "David"); + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("listOfString", Arrays.asList("Prof", "Tia", "David")).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() { + + FlatDocument probe = new FlatDocument(); + probe.customNamedField = "Mitosis"; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "Mitosis").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatchMode() { + + Foo probe = new Foo(); + probe.embedded = new EmbeddedClass(); + probe.embedded.id = "conflux"; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("embedded._id", "conflux").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktMatchMode() { + + Foo probe = new Foo(); + probe.embedded = new EmbeddedClass(); + probe.embedded.id = "conflux"; + + Query query = query(byExample(new ExampleBuilder(probe).objectMatchMode(ObjectMatchMode.STRICT).get())); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class)); + + assertThat(dbo, isBsonObject().containing("embedded._id", "conflux")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarting() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( + StringMatchMode.STARTING).get())); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^firefight")) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( + StringMatchMode.ENDING).get())); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight$")) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMatchModeSet() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( + StringMatchMode.ENDING, true).get())); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat( + dbo, + is(new BasicDBObjectBuilder() + .add("stringValue", new BasicDBObjectBuilder().add("$regex", "firefight$").add("$options", "i").get()) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( + StringMatchMode.DEFAULT, true).get())); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); + + assertThat( + dbo, + is(new BasicDBObjectBuilder() + .add("stringValue", + new BasicDBObjectBuilder().add("$regex", Pattern.quote("firefight")).add("$options", "i").get()) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedWhenContainingDBRef() { + + WithDBRef probe = new WithDBRef(); + probe.someString = "steelheart"; + probe.reference = new Reference(); + probe.reference.id = 200L; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); + com.mongodb.DBRef reference = getTypedValue(dbo, "reference", com.mongodb.DBRef.class); + + assertThat(reference.getId(), Is. is(200L)); + assertThat(reference.getCollectionName(), is("reference")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedWhenDBRefIsNull() { + + WithDBRef probe = new WithDBRef(); + probe.someString = "steelheart"; + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("someString", "steelheart").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() { + + ClassWithGeoTypes probe = new ClassWithGeoTypes(); + probe.legacyPoint = new Point(10D, 20D); + + Query query = query(byExample(probe)); + + DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); + + assertThat(dbo.get("legacyPoint.x"), Is. is(10D)); + assertThat(dbo.get("legacyPoint.y"), Is. is(20D)); + } + + static class FlatDocument { + + @Id String id; + String stringValue; + Integer intValue; + List listOfString; + @Field("custom_field_name") String customNamedField; + } + @Document public class Foo { @Id private ObjectId id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java new file mode 100644 index 0000000000..677ec3bb1c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java @@ -0,0 +1,174 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.temp; + +import java.net.UnknownHostException; +import java.util.List; + +import org.hamcrest.core.Is; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import com.mongodb.MongoClient; + +public class QueryByExampleTests { + + MongoTemplate template; + + @Before + public void setUp() throws UnknownHostException { + + template = new MongoTemplate(new MongoClient(), "query-by-example"); + template.remove(new Query(), Person.class); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldWorkForSimpleProperty() { + + init(); + + Person sample = new Person(); + sample.lastname = "stark"; + + List result = template.findByExample(sample); + Assert.assertThat(result.size(), Is.is(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldWorkForMultipleProperties() { + + init(); + + Person sample = new Person(); + sample.lastname = "stark"; + sample.firstname = "arya"; + + List result = template.findByExample(sample); + Assert.assertThat(result.size(), Is.is(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldWorkForIdProperty() { + + init(); + + Person p4 = new Person(); + template.save(p4); + + Person sample = new Person(); + sample.id = p4.id; + + List result = template.findByExample(sample); + Assert.assertThat(result.size(), Is.is(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldReturnEmptyListIfNotMatching() { + + init(); + + Person sample = new Person(); + sample.firstname = "jon"; + sample.firstname = "stark"; + + List result = template.findByExample(sample); + Assert.assertThat(result.size(), Is.is(0)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldReturnEverythingWhenSampleIsEmpty() { + + init(); + + Person sample = new Person(); + + List result = template.findByExample(sample); + Assert.assertThat(result.size(), Is.is(3)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleWithCriteria() { + + init(); + + Person sample = new Person(); + sample.lastname = "stark"; + + Query query = new Query(new Criteria().alike(new Example(sample)).and("firstname").regex("^ary*")); + + List result = template.find(query, Person.class); + Assert.assertThat(result.size(), Is.is(1)); + } + + public void init() { + + Person p1 = new Person(); + p1.firstname = "bran"; + p1.lastname = "stark"; + + Person p2 = new Person(); + p2.firstname = "jon"; + p2.lastname = "snow"; + + Person p3 = new Person(); + p3.firstname = "arya"; + p3.lastname = "stark"; + + template.save(p1); + template.save(p2); + template.save(p3); + } + + @Document(collection = "dramatis-personae") + static class Person { + + @Id String id; + String firstname; + + @Field("last_name") String lastname; + + @Override + public String toString() { + return "Person [id=" + id + ", firstname=" + firstname + ", lastname=" + lastname + "]"; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 6adc4bbc29..abdf3738a7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -175,8 +175,8 @@ public void findsPagedPersons() throws Exception { @Test public void executesPagedFinderCorrectly() throws Exception { - Page page = repository.findByLastnameLike("*a*", - new PageRequest(0, 2, Direction.ASC, "lastname", "firstname")); + Page page = repository.findByLastnameLike("*a*", new PageRequest(0, 2, Direction.ASC, "lastname", + "firstname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -186,8 +186,8 @@ public void executesPagedFinderCorrectly() throws Exception { @Test public void executesPagedFinderWithAnnotatedQueryCorrectly() throws Exception { - Page page = repository.findByLastnameLikeWithPageable(".*a.*", - new PageRequest(0, 2, Direction.ASC, "lastname", "firstname")); + Page page = repository.findByLastnameLikeWithPageable(".*a.*", new PageRequest(0, 2, Direction.ASC, + "lastname", "firstname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -311,8 +311,8 @@ public void findsPeopleByLocationWithinPolygon() { @Test public void findsPagedPeopleByPredicate() throws Exception { - Page page = repository.findAll(person.lastname.contains("a"), - new PageRequest(0, 2, Direction.ASC, "lastname")); + Page page = repository.findAll(person.lastname.contains("a"), new PageRequest(0, 2, Direction.ASC, + "lastname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -398,8 +398,8 @@ public void executesGeoNearQueryForResultsCorrectly() { dave.setLocation(point); repository.save(dave); - GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS)); + GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS)); assertThat(results.getContent().isEmpty(), is(false)); } @@ -410,8 +410,8 @@ public void executesGeoPageQueryForResultsCorrectly() { dave.setLocation(point); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 20)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS), new PageRequest(0, 20)); assertThat(results.getContent().isEmpty(), is(false)); // DATAMONGO-607 @@ -621,8 +621,8 @@ public void executesGeoPageQueryForWithPageRequestForPageInBetween() { repository.save(Arrays.asList(dave, oliver, carter, boyd, leroi)); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(2)); @@ -646,8 +646,8 @@ public void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() { repository.save(Arrays.asList(dave, oliver, carter)); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(1)); assertThat(results.isFirst(), is(false)); @@ -665,8 +665,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElement() { dave.setLocation(point); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS), new PageRequest(0, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(1)); @@ -684,8 +684,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() { dave.setLocation(new Point(-73.99171, 40.738868)); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, + Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(true)); assertThat(results.getNumberOfElements(), is(0)); @@ -935,8 +935,8 @@ public void findByCustomQueryLastnameAndStreetInList() { @Test public void shouldLimitCollectionQueryToMaxResultsWhenPresent() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), - new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", + "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); List result = repository.findTop3ByLastnameStartingWith("Dylan"); assertThat(result.size(), is(3)); } @@ -947,8 +947,8 @@ public void shouldLimitCollectionQueryToMaxResultsWhenPresent() { @Test public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), - new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", + "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(0, 2)); assertThat(result.getContent().size(), is(2)); } @@ -959,8 +959,8 @@ public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() { @Test public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), - new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", + "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(1, 2)); assertThat(result.getContent().size(), is(1)); } @@ -971,8 +971,8 @@ public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() { @Test public void shouldReturnEmptyWhenPageRequestedPageIsTotallyOutOfScopeForLimit() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), - new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", + "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(2, 2)); assertThat(result.getContent().size(), is(0)); } @@ -1221,4 +1221,5 @@ public void shouldFindByFirstnameForSpELExpressionWithParameterVariableOnly() { assertThat(users, hasSize(1)); assertThat(users.get(0), is(dave)); } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java index 1d15730afc..1a7383601d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java @@ -27,8 +27,7 @@ @Document public abstract class Contact { - @Id - protected final String id; + @Id protected String id; public Contact() { this.id = new ObjectId().toString(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index eae2c02e10..0796ba2012 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -334,22 +334,23 @@ public interface PersonRepository extends MongoRepository, Query */ @Query("{ firstname : { $in : ?0 }}") Stream findByCustomQueryWithStreamingCursorByFirstnames(List firstnames); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : ?#{[0]}}") List findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }") List findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : :#{#firstname}}") List findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname); + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java index 2c5a72a03b..13271d1607 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java @@ -15,7 +15,16 @@ */ package org.springframework.data.mongodb.repository; +import java.util.List; + +import org.hamcrest.core.Is; +import org.junit.Assert; +import org.junit.Test; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.util.ReflectionTestUtils; /** * Integration test for {@link PersonRepository}. @@ -24,4 +33,41 @@ * @author Thomas Darimont */ @ContextConfiguration -public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {} +public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldResolveStuffCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + + // needed to tweak stuff a bit since some field are automatically set - so we need to undo this + ReflectionTestUtils.setField(sample, "id", null); + ReflectionTestUtils.setField(sample, "createdAt", null); + ReflectionTestUtils.setField(sample, "email", null); + + Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); + Assert.assertThat(result.getNumberOfElements(), Is.is(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldResolveStuffCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + + // needed to tweak stuff a bit since some field are automatically set - so we need to undo this + ReflectionTestUtils.setField(sample, "id", null); + ReflectionTestUtils.setField(sample, "createdAt", null); + ReflectionTestUtils.setField(sample, "email", null); + + List result = repository.findAllByExample(new Example(sample)); + Assert.assertThat(result.size(), Is.is(2)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index c13b27eb09..1be130373d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Iterator; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; @@ -147,4 +148,13 @@ public Object[] getValues() { public Class getDynamicProjection() { return null; } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getSampleObject() + */ + @Override + public Example getSampleObject() { + return null; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 5e9177ae39..306ac9d1c0 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,16 +31,27 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Example.ObjectMatchMode; +import org.springframework.data.domain.Example.StringMatchMode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person.Sex; +import org.springframework.data.mongodb.repository.User; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; /** * @author A. B. M. Kowser * @author Thomas Darimont + * @author Christoph Strobl */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -160,6 +171,206 @@ public void shouldInsertMutlipleFromSet() { assertThatAllReferencePersonsWereStoredCorrectly(idToPerson, saved); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldLookUpEntriesCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); + + assertThat(result.getContent(), hasItems(dave, oliver)); + assertThat(result.getContent(), hasSize(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldLookUpEntriesCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItems(dave, oliver)); + assertThat(result, hasSize(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObject() { + + dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington")); + repository.save(dave); + + oliver.setAddress(new Address("East Capitol St NE & First St SE", "20004", "Washington")); + repository.save(oliver); + + Person sample = new Person(); + sample.setAddress(dave.getAddress()); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItem(dave)); + assertThat(result, hasSize(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingPartialNestedObject() { + + dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington")); + repository.save(dave); + + oliver.setAddress(new Address("East Capitol St NE & First St SE", "20004", "Washington")); + repository.save(oliver); + + Person sample = new Person(); + sample.setAddress(new Address(null, null, "Washington")); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItems(dave, oliver)); + assertThat(result, hasSize(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInStrictMode() { + + dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington")); + repository.save(dave); + + Person sample = new Person(); + sample.setAddress(new Address(null, null, "Washington")); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example.ExampleBuilder(sample).objectMatchMode( + ObjectMatchMode.STRICT).get()); + + assertThat(result, empty()); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInStrictMode() { + + dave.setAddress(new Address("1600 Pennsylvania Ave NW", "20500", "Washington")); + repository.save(dave); + + Person sample = new Person(); + sample.setAddress(dave.getAddress()); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example.ExampleBuilder(sample).objectMatchMode( + ObjectMatchMode.STRICT).get()); + + assertThat(result, hasItem(dave)); + assertThat(result, hasSize(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldRespectStringMatchMode() { + + Person sample = new Person(); + sample.setLastname("Mat"); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example.ExampleBuilder(sample).stringMatchMode( + StringMatchMode.STARTING).get()); + + assertThat(result, hasItems(dave, oliver)); + assertThat(result, hasSize(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldResolveDbRefCorrectly() { + + User user = new User(); + user.setId("c0nf1ux"); + user.setUsername("conflux"); + template.save(user); + + Person megan = new Person("megan", "tarash"); + megan.setCreator(user); + + repository.save(megan); + + Person sample = new Person(); + sample.setCreator(user); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItem(megan)); + assertThat(result, hasSize(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldResolveLegacyCoordinatesCorrectly() { + + Person megan = new Person("megan", "tarash"); + megan.setLocation(new Point(41.85003D, -87.65005D)); + + repository.save(megan); + + Person sample = new Person(); + sample.setLocation(megan.getLocation()); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItem(megan)); + assertThat(result, hasSize(1)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldResolveGeoJsonCoordinatesCorrectly() { + + Person megan = new Person("megan", "tarash"); + megan.setLocation(new GeoJsonPoint(41.85003D, -87.65005D)); + + repository.save(megan); + + Person sample = new Person(); + sample.setLocation(megan.getLocation()); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItem(megan)); + assertThat(result, hasSize(1)); + } + private void assertThatAllReferencePersonsWereStoredCorrectly(Map references, List saved) { for (Person person : saved) { @@ -168,6 +379,13 @@ private void assertThatAllReferencePersonsWereStoredCorrectly(Map { @Override From 09976102e3b92ac1c7ae135ceaa0dc6f5f8bb9b3 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 29 Jun 2015 10:50:23 +0200 Subject: [PATCH 03/14] DATAMONGO-1245 - QBE enhancements for specific property handling. Rename methods on Example and introduce PropertySpecifiers. Move mapping to separate class. --- .../springframework/data/domain/Example.java | 116 +++-- .../data/domain/PropertySpecifier.java | 111 +++++ .../data/domain/PropertyValueTransformer.java | 23 + .../core/convert/MongoExampleMapper.java | 234 ++++++++++ .../mongodb/core/convert/QueryMapper.java | 90 +--- .../convert/MongoExampleMapperUnitTests.java | 435 ++++++++++++++++++ .../core/convert/QueryMapperUnitTests.java | 241 +--------- .../support/SimpleMongoRepositoryTests.java | 38 +- 8 files changed, 927 insertions(+), 361 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java index 31a7d54740..2d07cc5d77 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java @@ -15,6 +15,9 @@ */ package org.springframework.data.domain; +import java.util.LinkedHashMap; +import java.util.Map; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -25,9 +28,13 @@ public class Example { private final T probe; - private ObjectMatchMode objectMatchMode = ObjectMatchMode.LENIENT; - private StringMatchMode stringMatchMode = StringMatchMode.DEFAULT; - private boolean ignoreCaseEnabled = false; + + private NullHandling nullHandling = NullHandling.IGNORE_NULL; + private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; + + private boolean ignoreCase = false; + + private Map propertySpecifiers = new LinkedHashMap(); public Example(S probe) { @@ -39,16 +46,28 @@ public T getProbe() { return probe; } - public ObjectMatchMode getObjectMatchMode() { - return objectMatchMode; + public NullHandling getNullHandling() { + return nullHandling; } - public StringMatchMode getStringMatchMode() { - return stringMatchMode; + public StringMatcher getDefaultStringMatcher() { + return defaultStringMatcher; } public boolean isIngnoreCaseEnabled() { - return this.ignoreCaseEnabled; + return this.ignoreCase; + } + + public boolean hasPropertySpecifier(String path) { + return propertySpecifiers.containsKey(path); + } + + public PropertySpecifier getPropertySpecifier(String propertyPath) { + return this.propertySpecifiers.get(propertyPath); + } + + public boolean hasPropertySpecifiers() { + return !this.propertySpecifiers.isEmpty(); } @SuppressWarnings("unchecked") @@ -56,34 +75,76 @@ public Class getProbeType() { return (Class) ClassUtils.getUserClass(probe.getClass()); } - public static Example example(S probe) { + public static Example exampleOf(S probe) { return new Example(probe); } - public static class ExampleBuilder { + public static Example exampleOf(S probe, String... ignoredProperties) { + return new Builder(probe).ignore(ignoredProperties).get(); + } + + public static Builder newExampleOf(S probe) { + return new Builder(probe); + } + + public static class Builder { private Example example; - public ExampleBuilder(T probe) { + Builder(T probe) { example = new Example(probe); } - public ExampleBuilder objectMatchMode(ObjectMatchMode matchMode) { + public Builder with(NullHandling nullHandling) { + return nullHandling(nullHandling); + } + + public Builder with(StringMatcher stringMatcher) { + return stringMatcher(stringMatcher); + } + + public Builder with(PropertySpecifier specifier) { + return specify(specifier); + } + + public Builder nullHandling(NullHandling nullHandling) { - example.objectMatchMode = matchMode == null ? ObjectMatchMode.LENIENT : matchMode; + example.nullHandling = nullHandling == null ? NullHandling.IGNORE_NULL : nullHandling; return this; } - public ExampleBuilder stringMatchMode(StringMatchMode matchMode) { + public Builder stringMatcher(StringMatcher stringMatcher) { - example.stringMatchMode = matchMode == null ? StringMatchMode.DEFAULT : matchMode; + example.defaultStringMatcher = stringMatcher == null ? StringMatcher.DEFAULT : stringMatcher; return this; } - public ExampleBuilder stringMatchMode(StringMatchMode matchMode, boolean ignoreCase) { + public Builder stringMatcher(StringMatcher stringMatching, boolean ignoreCase) { - example.stringMatchMode = matchMode == null ? StringMatchMode.DEFAULT : matchMode; - example.ignoreCaseEnabled = ignoreCase; + example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; + example.ignoreCase = ignoreCase; + return this; + } + + public Builder ignoreCase() { + example.ignoreCase = true; + return this; + } + + public Builder specify(PropertySpecifier... specifiers) { + + for (PropertySpecifier specifier : specifiers) { + example.propertySpecifiers.put(specifier.getPath(), specifier); + } + return this; + } + + public Builder ignore(String... ignoredProperties) { + + for (String ignoredProperty : ignoredProperties) { + specify(PropertySpecifier.newPropertySpecifier(ignoredProperty) + .valueTransformer(new ExcludingValueTransformer()).get()); + } return this; } @@ -97,15 +158,15 @@ public Example get() { * * @author Christoph Strobl */ - public static enum ObjectMatchMode { + public static enum NullHandling { /** * Strict matching will use partially filled objects as reference. */ - STRICT, + INCLUDE_NULL, /** * Lenient matching will inspected nested objects and extract path if needed. */ - LENIENT + IGNORE_NULL } /** @@ -113,7 +174,7 @@ public static enum ObjectMatchMode { * * @author Christoph Strobl */ - public static enum StringMatchMode { + public static enum StringMatcher { /** * Store specific default. @@ -141,9 +202,12 @@ public static enum StringMatchMode { REGEX } - // TODO: add default null handling - // TODO: add default String handling - // TODO: add per field null handling - // TODO: add per field String handling + public static class ExcludingValueTransformer implements PropertyValueTransformer { + + @Override + public Object tranform(Object source) { + return null; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java new file mode 100644 index 0000000000..4664ccbc79 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java @@ -0,0 +1,111 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +import org.springframework.data.domain.Example.StringMatcher; +import org.springframework.util.Assert; + +/** + * @author Christoph Strobl + */ +public class PropertySpecifier { + + private PropertyValueTransformer valueTransformer; + private StringMatcher stringMatcher = StringMatcher.DEFAULT; + private boolean ignoreCase = false; + private final String path; + + PropertySpecifier(String path) { + + Assert.hasText(path, "Path must not be null/empty!"); + this.path = path; + } + + public StringMatcher getStringMatcher() { + return this.stringMatcher == null ? StringMatcher.DEFAULT : stringMatcher; + } + + public boolean isIgnoreCaseEnabled() { + return ignoreCase; + } + + public PropertyValueTransformer getPropertyValueTransformer() { + return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; + } + + public String getPath() { + return path; + } + + public Object transformValue(Object source) { + return getPropertyValueTransformer().tranform(source); + } + + public static PropertySpecifier ignoreCase(String propertyPath) { + return new Builder(propertyPath).ignoreCase().get(); + } + + public static Builder newPropertySpecifier(String propertyPath) { + return new Builder(propertyPath); + } + + public static class Builder { + + private PropertySpecifier specifier; + + Builder(String path) { + specifier = new PropertySpecifier(path); + } + + public Builder with(StringMatcher stringMatcher) { + return stringMatcher(stringMatcher); + } + + public Builder with(PropertyValueTransformer valueTransformer) { + return valueTransformer(valueTransformer); + } + + public Builder stringMatcher(StringMatcher stringMatcher) { + specifier.stringMatcher = stringMatcher; + return this; + } + + public Builder ignoreCase() { + specifier.ignoreCase = true; + return this; + } + + public Builder valueTransformer(PropertyValueTransformer valueTransformer) { + specifier.valueTransformer = valueTransformer; + return this; + } + + public PropertySpecifier get() { + return this.specifier; + } + } + + static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { + + INSTANCE; + + @Override + public Object tranform(Object source) { + return source; + } + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java new file mode 100644 index 0000000000..fafcb9da67 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.domain; + +/** + * @author Christoph Strobl + */ +public interface PropertyValueTransformer { + Object tranform(Object souce); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java new file mode 100644 index 0000000000..e4753cc8e5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -0,0 +1,234 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.convert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Stack; +import java.util.regex.Pattern; + +import org.springframework.data.domain.Example; +import org.springframework.data.domain.Example.NullHandling; +import org.springframework.data.domain.Example.StringMatcher; +import org.springframework.data.domain.PropertySpecifier; +import org.springframework.data.mapping.PropertyHandler; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + * @since 1.8 + */ +public class MongoExampleMapper { + + private final MappingContext, MongoPersistentProperty> mappingContext; + private final MongoConverter converter; + + public MongoExampleMapper(MongoConverter converter) { + + this.converter = converter; + this.mappingContext = converter.getMappingContext(); + } + + /** + * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from + * {@link Example#getProbe()}. + * + * @param example + * @return + * @since 1.8 + */ + public DBObject getMappedExample(Example example) { + return getMappedExample(example, mappingContext.getPersistentEntity(example.getProbeType())); + } + + /** + * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from + * {@link Example#getProbe()}. + * + * @param example + * @param entity + * @return + * @since 1.8 + */ + public DBObject getMappedExample(Example example, MongoPersistentEntity entity) { + + DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe()); + + if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) { + reference.removeField(entity.getIdProperty().getFieldName()); + } + + applyPropertySpecs("", reference, example); + + return ObjectUtils.nullSafeEquals(NullHandling.INCLUDE_NULL, example.getNullHandling()) ? reference + : new BasicDBObject(SerializationUtils.flatMap(reference)); + } + + private String getMappedPropertyPath(String path, Example example) { + + MongoPersistentEntity entity = mappingContext.getPersistentEntity(example.getProbeType()); + + Iterator parts = Arrays.asList(path.split("\\.")).iterator(); + + final Stack stack = new Stack(); + + List resultParts = new ArrayList(); + + while (parts.hasNext()) { + + final String part = parts.next(); + MongoPersistentProperty prop = entity.getPersistentProperty(part); + + if (prop == null) { + + entity.doWithProperties(new PropertyHandler() { + + @Override + public void doWithPersistentProperty(MongoPersistentProperty property) { + + if (property.getFieldName().equals(part)) { + stack.push(property); + } + } + }); + + if (stack.isEmpty()) { + throw new RuntimeException("foobar"); + } + prop = stack.pop(); + } + + resultParts.add(prop.getName()); + + if (prop.isEntity() && mappingContext.hasPersistentEntityFor(prop.getActualType())) { + entity = mappingContext.getPersistentEntity(prop.getActualType()); + } else { + break; + } + } + + return StringUtils.collectionToDelimitedString(resultParts, "."); + + } + + private void applyPropertySpecs(String path, DBObject source, Example example) { + + if (!(source instanceof BasicDBObject)) { + return; + } + + Iterator> iter = ((BasicDBObject) source).entrySet().iterator(); + + while (iter.hasNext()) { + + Map.Entry entry = iter.next(); + + if (entry.getKey().equals("_id") && entry.getValue() == null) { + iter.remove(); + continue; + } + + String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); + String mappedPropertyPath = propertyPath; + + PropertySpecifier specifier = null; + StringMatcher stringMatcher = example.getDefaultStringMatcher(); + Object value = entry.getValue(); + + if (example.hasPropertySpecifiers()) { + + mappedPropertyPath = example.hasPropertySpecifier(propertyPath) ? propertyPath : getMappedPropertyPath( + propertyPath, example); + specifier = example.getPropertySpecifier(mappedPropertyPath); + + if (specifier != null && !specifier.getStringMatcher().equals(StringMatcher.DEFAULT)) { + stringMatcher = specifier.getStringMatcher(); + } + } + + if (specifier != null) { + + value = specifier.transformValue(value); + if (value == null) { + iter.remove(); + continue; + } + + entry.setValue(value); + } + + if (entry.getValue() instanceof String) { + applyStringMatcher(example, stringMatcher, entry); + } else if (entry.getValue() instanceof BasicDBObject) { + applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), example); + } + } + } + + private void applyStringMatcher(Example example, StringMatcher stringMatcher, Map.Entry entry) { + + // TODO: extract common stuff from MongoQueryCreator + BasicDBObject dbo = new BasicDBObject(); + switch (stringMatcher) { + + case REGEX: + dbo.put("$regex", entry.getValue()); + entry.setValue(dbo); + break; + case DEFAULT: + + if (example.isIngnoreCaseEnabled()) { + dbo.put("$regex", Pattern.quote((String) entry.getValue())); + entry.setValue(dbo); + } + break; + case CONTAINING: + dbo.put("$regex", ".*" + entry.getValue() + ".*"); + entry.setValue(dbo); + break; + case STARTING: + dbo.put("$regex", "^" + entry.getValue()); + entry.setValue(dbo); + break; + case ENDING: + dbo.put("$regex", entry.getValue() + "$"); + entry.setValue(dbo); + break; + case EXACT: + dbo.put("$regex", "^" + entry.getValue() + "$"); + entry.setValue(dbo); + break; + default: + } + + // sometimes order matters in MongoDB so make sure to add $options after $regex. + if (example.isIngnoreCaseEnabled()) { + dbo.put("$options", "i"); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 046b155608..34177102b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -20,18 +20,14 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import java.util.Set; -import java.util.regex.Pattern; import org.bson.types.ObjectId; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.ObjectMatchMode; -import org.springframework.data.domain.Example.StringMatchMode; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PropertyPath; @@ -44,11 +40,9 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; @@ -77,6 +71,7 @@ private enum MetaMapping { private final ConversionService conversionService; private final MongoConverter converter; private final MappingContext, MongoPersistentProperty> mappingContext; + private final MongoExampleMapper exampleMapper; /** * Creates a new {@link QueryMapper} with the given {@link MongoConverter}. @@ -90,6 +85,7 @@ public QueryMapper(MongoConverter converter) { this.conversionService = converter.getConversionService(); this.converter = converter; this.mappingContext = converter.getMappingContext(); + this.exampleMapper = new MongoExampleMapper(converter); } /** @@ -247,92 +243,12 @@ protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity en } if (keyword.isSample()) { - return getMappedExample(keyword.> getValue(), entity); + return exampleMapper.getMappedExample(keyword.> getValue(), entity); } return new BasicDBObject(keyword.getKey(), convertSimpleOrDBObject(keyword.getValue(), entity)); } - /** - * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from - * {@link Example#getProbe()}. - * - * @param example - * @param entity - * @return - * @since 1.8 - */ - protected DBObject getMappedExample(Example example, MongoPersistentEntity entity) { - - DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe()); - - if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) { - reference.removeField(entity.getIdProperty().getFieldName()); - } - - if (!ObjectUtils.nullSafeEquals(StringMatchMode.DEFAULT, example.getStringMatchMode()) - || example.isIngnoreCaseEnabled()) { - applyStringPattern(reference, example); - } - - return ObjectUtils.nullSafeEquals(ObjectMatchMode.STRICT, example.getObjectMatchMode()) ? reference - : new BasicDBObject(SerializationUtils.flatMap(reference)); - } - - private void applyStringPattern(DBObject source, Example example) { - - if (!(source instanceof BasicDBObject)) { - return; - } - Iterator> iter = ((BasicDBObject) source).entrySet().iterator(); - - while (iter.hasNext()) { - - Map.Entry entry = iter.next(); - if (entry.getValue() instanceof String) { - - // TODO: extract common stuff from MongoQueryCreator - BasicDBObject dbo = new BasicDBObject(); - switch (example.getStringMatchMode()) { - - case REGEX: - dbo.put("$regex", entry.getValue()); - entry.setValue(dbo); - break; - case DEFAULT: - dbo.put("$regex", Pattern.quote((String) entry.getValue())); - entry.setValue(dbo); - break; - case CONTAINING: - dbo.put("$regex", ".*" + entry.getValue() + ".*"); - entry.setValue(dbo); - break; - case STARTING: - dbo.put("$regex", "^" + entry.getValue()); - entry.setValue(dbo); - break; - case ENDING: - dbo.put("$regex", entry.getValue() + "$"); - entry.setValue(dbo); - break; - case EXACT: - dbo.put("$regex", "^" + entry.getValue() + "$"); - entry.setValue(dbo); - break; - default: - } - - // sometimes order matters in MongoDB so make sure to add $options after $regex. - if (example.isIngnoreCaseEnabled()) { - dbo.put("$options", "i"); - } - - } else if (entry.getValue() instanceof BasicDBObject) { - applyStringPattern((BasicDBObject) entry.getValue(), example); - } - } - } - /** * Returns the mapped keyword considered defining a criteria for the given property. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java new file mode 100644 index 0000000000..c83c6ab99d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -0,0 +1,435 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.convert; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.springframework.data.domain.Example.*; +import static org.springframework.data.domain.Example.NullHandling.*; +import static org.springframework.data.domain.Example.StringMatcher.*; +import static org.springframework.data.domain.PropertySpecifier.*; +import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; + +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; + +import org.hamcrest.core.Is; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Example; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes; +import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.WithDBRef; +import org.springframework.data.mongodb.core.mapping.DBRef; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; + +import com.mongodb.BasicDBObject; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class MongoExampleMapperUnitTests { + + MongoExampleMapper mapper; + MongoMappingContext context; + MappingMongoConverter converter; + + @Mock MongoDbFactory factory; + + @Before + public void setUp() { + + this.context = new MongoMappingContext(); + + this.converter = new MappingMongoConverter(new DefaultDbRefResolver(factory), context); + this.converter.afterPropertiesSet(); + + this.mapper = new MongoExampleMapper(converter); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsSet() { + + FlatDocument probe = new FlatDocument(); + probe.id = "steelheart"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("_id", "steelheart").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenMultipleValuesSet() { + + FlatDocument probe = new FlatDocument(); + probe.id = "steelheart"; + probe.stringValue = "firefight"; + probe.intValue = 100; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("_id", "steelheart").add("stringValue", "firefight").add("intValue", 100) + .get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsNotSet() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "firefight").add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenListHasValues() { + + FlatDocument probe = new FlatDocument(); + probe.listOfString = Arrays.asList("Prof", "Tia", "David"); + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("listOfString", Arrays.asList("Prof", "Tia", "David")).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() { + + FlatDocument probe = new FlatDocument(); + probe.customNamedField = "Mitosis"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "Mitosis").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatchMode() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.stringValue = "conflux"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "conflux").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktMatchMode() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.stringValue = "conflux"; + + Example example = newExampleOf(probe).nullHandling(INCLUDE_NULL).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, isBsonObject().containing("flatDoc.stringValue", "conflux")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarting() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = newExampleOf(probe).stringMatcher(STARTING).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^firefight")) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = newExampleOf(probe).stringMatcher(ENDING).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight$")) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMatchModeSet() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = newExampleOf(probe).stringMatcher(ENDING, true).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat( + dbo, + is(new BasicDBObjectBuilder() + .add("stringValue", new BasicDBObjectBuilder().add("$regex", "firefight$").add("$options", "i").get()) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = newExampleOf(probe).stringMatcher(DEFAULT, true).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat( + dbo, + is(new BasicDBObjectBuilder() + .add("stringValue", + new BasicDBObjectBuilder().add("$regex", Pattern.quote("firefight")).add("$options", "i").get()) + .add("intValue", 100).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedWhenContainingDBRef() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "steelheart"; + probe.referenceDocument = new ReferenceDocument(); + probe.referenceDocument.id = "200"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WithDBRef.class)); + com.mongodb.DBRef reference = getTypedValue(dbo, "referenceDocument", com.mongodb.DBRef.class); + + assertThat(reference.getId(), Is. is("200")); + assertThat(reference.getCollectionName(), is("refDoc")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedWhenDBRefIsNull() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "steelheart"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "steelheart").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() { + + ClassWithGeoTypes probe = new ClassWithGeoTypes(); + probe.legacyPoint = new Point(10D, 20D); + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WithDBRef.class)); + + assertThat(dbo.get("legacyPoint.x"), Is. is(10D)); + assertThat(dbo.get("legacyPoint.y"), Is. is(20D)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldExcludeFieldWithCustomNameCorrectly() { + + FlatDocument probe = new FlatDocument(); + probe.customNamedField = "foo"; + probe.intValue = 10; + probe.stringValue = "string"; + + Example example = newExampleOf(probe).ignore("customNamedField").get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "string").add("intValue", 10).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldExcludeFieldCorrectly() { + + FlatDocument probe = new FlatDocument(); + probe.customNamedField = "foo"; + probe.intValue = 10; + probe.stringValue = "string"; + + Example example = newExampleOf(probe).ignore("stringValue").get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "foo").add("intValue", 10).get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldExcludeNestedFieldCorrectly() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.customNamedField = "foo"; + probe.flatDoc.intValue = 10; + probe.flatDoc.stringValue = "string"; + + Example example = newExampleOf(probe).ignore("flatDoc.stringValue").get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.custom_field_name", "foo").add("flatDoc.intValue", 10) + .get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldExcludeNestedFieldWithCustomNameCorrectly() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.customNamedField = "foo"; + probe.flatDoc.intValue = 10; + probe.flatDoc.stringValue = "string"; + + Example example = newExampleOf(probe).ignore("flatDoc.customNamedField").get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "string").add("flatDoc.intValue", 10) + .get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldFavorFieldSpecificationStringMatcherOverDefaultStringMatcher() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.customNamedField = "steelheart"; + + Example example = newExampleOf(probe).specify( + newPropertySpecifier("stringValue").stringMatcher(CONTAINING).get()).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat( + dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", ".*firefight.*")) + .add("custom_field_name", "steelheart").get())); + } + + static class FlatDocument { + + @Id String id; + String stringValue; + @Field("custom_field_name") String customNamedField; + Integer intValue; + List listOfString; + @DBRef ReferenceDocument referenceDocument; + } + + static class HierachicalDocument extends FlatDocument { + + String anotherStringValue; + } + + static class WrapperDocument { + + @Id String id; + FlatDocument flatDoc; + } + + @Document(collection = "refDoc") + static class ReferenceDocument { + + @Id String id; + String value; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java index 82829e0144..80d9b98b1d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/QueryMapperUnitTests.java @@ -17,7 +17,6 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.springframework.data.domain.Example.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; @@ -28,7 +27,6 @@ import java.util.Arrays; import java.util.List; import java.util.Map; -import java.util.regex.Pattern; import org.bson.types.ObjectId; import org.hamcrest.core.Is; @@ -38,10 +36,6 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Id; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.ExampleBuilder; -import org.springframework.data.domain.Example.ObjectMatchMode; -import org.springframework.data.domain.Example.StringMatchMode; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.Point; @@ -831,93 +825,7 @@ public void mappingShouldRetainNumericPositionInList() { * @see DATAMONGO-1245 */ @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsSet() { - - FlatDocument probe = new FlatDocument(); - probe.id = "steelheart"; - Example sample = example(probe); - - Query query = query(byExample(sample)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, is(new BasicDBObjectBuilder().add("_id", "steelheart").get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenMultipleValuesSet() { - - FlatDocument probe = new FlatDocument(); - probe.id = "steelheart"; - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, - is(new BasicDBObjectBuilder().add("_id", "steelheart").add("stringValue", "firefight").add("intValue", 100) - .get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsNotSet() { - - FlatDocument probe = new FlatDocument(); - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "firefight").add("intValue", 100).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenListHasValues() { - - FlatDocument probe = new FlatDocument(); - probe.listOfString = Arrays.asList("Prof", "Tia", "David"); - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, is(new BasicDBObjectBuilder().add("listOfString", Arrays.asList("Prof", "Tia", "David")).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() { - - FlatDocument probe = new FlatDocument(); - probe.customNamedField = "Mitosis"; - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "Mitosis").get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatchMode() { + public void exampleShouldBeMappedCorrectly() { Foo probe = new Foo(); probe.embedded = new EmbeddedClass(); @@ -930,144 +838,6 @@ public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatch assertThat(dbo, is(new BasicDBObjectBuilder().add("embedded._id", "conflux").get())); } - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktMatchMode() { - - Foo probe = new Foo(); - probe.embedded = new EmbeddedClass(); - probe.embedded.id = "conflux"; - - Query query = query(byExample(new ExampleBuilder(probe).objectMatchMode(ObjectMatchMode.STRICT).get())); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(Foo.class)); - - assertThat(dbo, isBsonObject().containing("embedded._id", "conflux")); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarting() { - - FlatDocument probe = new FlatDocument(); - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( - StringMatchMode.STARTING).get())); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^firefight")) - .add("intValue", 100).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding() { - - FlatDocument probe = new FlatDocument(); - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( - StringMatchMode.ENDING).get())); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight$")) - .add("intValue", 100).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMatchModeSet() { - - FlatDocument probe = new FlatDocument(); - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( - StringMatchMode.ENDING, true).get())); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat( - dbo, - is(new BasicDBObjectBuilder() - .add("stringValue", new BasicDBObjectBuilder().add("$regex", "firefight$").add("$options", "i").get()) - .add("intValue", 100).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { - - FlatDocument probe = new FlatDocument(); - probe.stringValue = "firefight"; - probe.intValue = 100; - - Query query = query(byExample(new Example.ExampleBuilder(probe).stringMatchMode( - StringMatchMode.DEFAULT, true).get())); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(FlatDocument.class)); - - assertThat( - dbo, - is(new BasicDBObjectBuilder() - .add("stringValue", - new BasicDBObjectBuilder().add("$regex", Pattern.quote("firefight")).add("$options", "i").get()) - .add("intValue", 100).get())); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedWhenContainingDBRef() { - - WithDBRef probe = new WithDBRef(); - probe.someString = "steelheart"; - probe.reference = new Reference(); - probe.reference.id = 200L; - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); - com.mongodb.DBRef reference = getTypedValue(dbo, "reference", com.mongodb.DBRef.class); - - assertThat(reference.getId(), Is. is(200L)); - assertThat(reference.getCollectionName(), is("reference")); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void exampleShouldBeMappedWhenDBRefIsNull() { - - WithDBRef probe = new WithDBRef(); - probe.someString = "steelheart"; - - Query query = query(byExample(probe)); - - DBObject dbo = mapper.getMappedObject(query.getQueryObject(), context.getPersistentEntity(WithDBRef.class)); - - assertThat(dbo, is(new BasicDBObjectBuilder().add("someString", "steelheart").get())); - } - /** * @see DATAMONGO-1245 */ @@ -1085,15 +855,6 @@ public void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() { assertThat(dbo.get("legacyPoint.y"), Is. is(20D)); } - static class FlatDocument { - - @Id String id; - String stringValue; - Integer intValue; - List listOfString; - @Field("custom_field_name") String customNamedField; - } - @Document public class Foo { @Id private ObjectId id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 306ac9d1c0..1c7f528204 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -32,13 +32,14 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.ObjectMatchMode; -import org.springframework.data.domain.Example.StringMatchMode; +import org.springframework.data.domain.Example.NullHandling; +import org.springframework.data.domain.Example.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; +import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.Address; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.Person.Sex; @@ -260,8 +261,8 @@ public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInSt sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example.ExampleBuilder(sample).objectMatchMode( - ObjectMatchMode.STRICT).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample) + .nullHandling(NullHandling.INCLUDE_NULL).get()); assertThat(result, empty()); } @@ -279,8 +280,8 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInS sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example.ExampleBuilder(sample).objectMatchMode( - ObjectMatchMode.STRICT).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample) + .nullHandling(NullHandling.INCLUDE_NULL).get()); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); @@ -296,8 +297,8 @@ public void findAllByExampleShouldRespectStringMatchMode() { sample.setLastname("Mat"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example.ExampleBuilder(sample).stringMatchMode( - StringMatchMode.STARTING).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample) + .stringMatcher(StringMatcher.STARTING).get()); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); @@ -371,6 +372,27 @@ public void findAllByExampleShouldResolveGeoJsonCoordinatesCorrectly() { assertThat(result, hasSize(1)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldProcessInheritanceCorrectly() { + + PersonExtended sample = new PersonExtended() {}; + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + List result = repository.findAllByExample(new Example(sample)); + + assertThat(result, hasItems(dave, oliver)); + assertThat(result, hasSize(2)); + } + + @Document(collection = "customizedPerson") + static class PersonExtended extends Person { + + } + private void assertThatAllReferencePersonsWereStoredCorrectly(Map references, List saved) { for (Person person : saved) { From 4c04ddf38a84c856b797ef3521088159557ec804 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 3 Jul 2015 08:49:07 +0200 Subject: [PATCH 04/14] DATAMONGO-1245 - QBE now uses common MongoRegexCreator. Introduced MongoRegexCreator. Added some javadoc. Still need to do reference documentation. --- .../springframework/data/domain/Example.java | 228 +++++++++++++++--- .../data/domain/PropertySpecifier.java | 129 +++++++++- .../data/domain/PropertyValueTransformer.java | 6 +- .../core/convert/MongoExampleMapper.java | 63 ++--- .../mongodb/core/query/MongoRegexCreator.java | 101 ++++++++ .../repository/query/MongoQueryCreator.java | 58 +---- .../convert/MongoExampleMapperUnitTests.java | 20 +- ...tractPersonRepositoryIntegrationTests.java | 40 +++ .../PersonRepositoryIntegrationTests.java | 48 +--- .../support/SimpleMongoRepositoryTests.java | 6 +- 10 files changed, 504 insertions(+), 195 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java index 2d07cc5d77..9599ef6420 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java @@ -18,10 +18,14 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** + * Support for query by example (QBE). + * * @author Christoph Strobl * @param */ @@ -29,64 +33,127 @@ public class Example { private final T probe; - private NullHandling nullHandling = NullHandling.IGNORE_NULL; + private NullHandler nullHandler = NullHandler.IGNORE; private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; + private PropertySpecifiers propertySpecifiers = new PropertySpecifiers(); private boolean ignoreCase = false; - private Map propertySpecifiers = new LinkedHashMap(); - + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe The example to use. Must not be {@literal null}. + */ public Example(S probe) { Assert.notNull(probe, "Probe must not be null!"); this.probe = probe; } + /** + * Get the example used. + * + * @return never {@literal null}. + */ public T getProbe() { return probe; } - public NullHandling getNullHandling() { - return nullHandling; + /** + * Get defined null handling. + * + * @return never {@literal null} + */ + public NullHandler getNullHandler() { + return nullHandler; } + /** + * Get defined {@link StringMatcher}. + * + * @return never {@literal null}. + */ public StringMatcher getDefaultStringMatcher() { return defaultStringMatcher; } + /** + * @return {@literal true} if {@link String} should be matched with ignore case option. + */ public boolean isIngnoreCaseEnabled() { return this.ignoreCase; } + /** + * @param path Dot-Path to property. + * @return {@literal true} in case {@link PropertySpecifier} defined for given path. + */ public boolean hasPropertySpecifier(String path) { - return propertySpecifiers.containsKey(path); + return propertySpecifiers.hasSpecifierForPath(path); } - public PropertySpecifier getPropertySpecifier(String propertyPath) { - return this.propertySpecifiers.get(propertyPath); + /** + * @param path Dot-Path to property. + * @return {@literal null} when no {@link PropertySpecifier} defined for path. + */ + public PropertySpecifier getPropertySpecifier(String path) { + return propertySpecifiers.getForPath(path); } + /** + * @return true if at least one {@link PropertySpecifier} defined. + */ public boolean hasPropertySpecifiers() { - return !this.propertySpecifiers.isEmpty(); + return this.propertySpecifiers.hasValues(); } + /** + * Get the actual type for the example used. + * + * @return + */ @SuppressWarnings("unchecked") public Class getProbeType() { return (Class) ClassUtils.getUserClass(probe.getClass()); } + /** + * Create a new {@link Example} including all non-null properties by default. + * + * @param probe must not be {@literal null}. + * @return + */ public static Example exampleOf(S probe) { return new Example(probe); } + /** + * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. + * + * @param probe must not be {@literal null}. + * @return + */ public static Example exampleOf(S probe, String... ignoredProperties) { return new Builder(probe).ignore(ignoredProperties).get(); } + /** + * Create new {@link Builder} for specifying {@link Example}. + * + * @param probe must not be {@literal null}. + * @return + * @see Builder + */ public static Builder newExampleOf(S probe) { return new Builder(probe); } + /** + * Builder for specifying desired behavior of {@link Example}. + * + * @author Christoph Strobl + * @param + */ public static class Builder { private Example example; @@ -95,30 +162,68 @@ public static class Builder { example = new Example(probe); } - public Builder with(NullHandling nullHandling) { + /** + * Sets {@link NullHandler} used for {@link Example}. + * + * @param nullHandling + * @return + * @see Builder#nullHandling(NullHandler) + */ + public Builder with(NullHandler nullHandling) { return nullHandling(nullHandling); } + /** + * Sets default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher + * @return + * @see Builder#stringMatcher(StringMatcher) + */ public Builder with(StringMatcher stringMatcher) { return stringMatcher(stringMatcher); } - public Builder with(PropertySpecifier specifier) { - return specify(specifier); + /** + * Adds {@link PropertySpecifier} used for {@link Example}. + * + * @param specifier + * @return + * @see Builder#specify(PropertySpecifier...) + */ + public Builder with(PropertySpecifier... specifiers) { + return specify(specifiers); } - public Builder nullHandling(NullHandling nullHandling) { + /** + * Sets {@link NullHandler} used for {@link Example}. + * + * @param nullHandling Defaulted to {@link NullHandler#INCLUDE} in case of {@literal null}. + * @return + */ + public Builder nullHandling(NullHandler nullHandling) { - example.nullHandling = nullHandling == null ? NullHandling.IGNORE_NULL : nullHandling; + example.nullHandler = nullHandling == null ? NullHandler.IGNORE : nullHandling; return this; } + /** + * Sets the default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. + * @return + */ public Builder stringMatcher(StringMatcher stringMatcher) { - - example.defaultStringMatcher = stringMatcher == null ? StringMatcher.DEFAULT : stringMatcher; - return this; + return stringMatcher(stringMatcher, example.ignoreCase); } + /** + * Sets the default {@link StringMatcher} used for {@link Example}. + * + * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. + * @param ignoreCase + * @return + */ public Builder stringMatcher(StringMatcher stringMatching, boolean ignoreCase) { example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; @@ -126,19 +231,34 @@ public Builder stringMatcher(StringMatcher stringMatching, boolean ignoreCase return this; } + /** + * @return + */ public Builder ignoreCase() { example.ignoreCase = true; return this; } + /** + * Define specific property handling. + * + * @param specifiers + * @return + */ public Builder specify(PropertySpecifier... specifiers) { for (PropertySpecifier specifier : specifiers) { - example.propertySpecifiers.put(specifier.getPath(), specifier); + example.propertySpecifiers.add(specifier); } return this; } + /** + * Ignore given properties. + * + * @param ignoredProperties + * @return + */ public Builder ignore(String... ignoredProperties) { for (String ignoredProperty : ignoredProperties) { @@ -148,29 +268,26 @@ public Builder ignore(String... ignoredProperties) { return this; } + /** + * @return {@link Example} as defined. + */ public Example get() { return this.example; } } /** - * Match modes indicates inclusion of complex objects. + * Null handling for creating criterion out of an {@link Example}. * * @author Christoph Strobl */ - public static enum NullHandling { - /** - * Strict matching will use partially filled objects as reference. - */ - INCLUDE_NULL, - /** - * Lenient matching will inspected nested objects and extract path if needed. - */ - IGNORE_NULL + public static enum NullHandler { + + INCLUDE, IGNORE } /** - * Match modes indicates treatment of {@link String} values. + * Match modes for treatment of {@link String} values. * * @author Christoph Strobl */ @@ -179,35 +296,74 @@ public static enum StringMatcher { /** * Store specific default. */ - DEFAULT, + DEFAULT(null), /** * Matches the exact string */ - EXACT, + EXACT(Type.SIMPLE_PROPERTY), /** * Matches string starting with pattern */ - STARTING, + STARTING(Type.STARTING_WITH), /** * Matches string ending with pattern */ - ENDING, + ENDING(Type.ENDING_WITH), /** * Matches string containing pattern */ - CONTAINING, + CONTAINING(Type.CONTAINING), /** * Treats strings as regular expression patterns */ - REGEX + REGEX(Type.REGEX); + + private Type type; + + private StringMatcher(Type type) { + this.type = type; + } + + /** + * Get the according {@link Part.Type}. + * + * @return {@literal null} for {@link StringMatcher#DEFAULT}. + */ + public Type getPartType() { + return type; + } + } public static class ExcludingValueTransformer implements PropertyValueTransformer { @Override - public Object tranform(Object source) { + public Object convert(Object source) { return null; } } + static class PropertySpecifiers { + + private Map propertySpecifiers = new LinkedHashMap(); + + public void add(PropertySpecifier specifier) { + + Assert.notNull(specifier, "PropertySpecifier must not be null!"); + propertySpecifiers.put(specifier.getPath(), specifier); + } + + public boolean hasSpecifierForPath(String path) { + return propertySpecifiers.containsKey(path); + } + + public PropertySpecifier getForPath(String path) { + return propertySpecifiers.get(path); + } + + public boolean hasValues() { + return !propertySpecifiers.isEmpty(); + } + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java index 4664ccbc79..da5981be09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java @@ -19,49 +19,106 @@ import org.springframework.util.Assert; /** + * Define specific property handling for a Dot-Path. + * * @author Christoph Strobl */ public class PropertySpecifier { - private PropertyValueTransformer valueTransformer; - private StringMatcher stringMatcher = StringMatcher.DEFAULT; - private boolean ignoreCase = false; private final String path; + private StringMatcher stringMatcher; + private Boolean ignoreCase; + + private PropertyValueTransformer valueTransformer; + + /** + * Creates new {@link PropertySpecifier} for given path. + * + * @param path Dot-Path to the property. Must not be {@literal null}. + */ PropertySpecifier(String path) { Assert.hasText(path, "Path must not be null/empty!"); this.path = path; } + /** + * Get the properties Dot-Path. + * + * @return never {@literal null}. + */ + public String getPath() { + return path; + } + + /** + * Get the {@link StringMatcher}. + * + * @return can be {@literal null}. + */ public StringMatcher getStringMatcher() { - return this.stringMatcher == null ? StringMatcher.DEFAULT : stringMatcher; + return stringMatcher; } - public boolean isIgnoreCaseEnabled() { + /** + * @return {literal true} in case {@link StringMatcher} defined. + */ + public boolean hasStringMatcher() { + return this.stringMatcher != null; + } + + /** + * @return {@literal null} if not set. + */ + public Boolean getIgnoreCase() { return ignoreCase; } + /** + * Get the property transformer to be applied. + * + * @return never {@literal null}. + */ public PropertyValueTransformer getPropertyValueTransformer() { return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; } - public String getPath() { - return path; - } - + /** + * Transforms a given source using the {@link PropertyValueTransformer}. + * + * @param source + * @return + */ public Object transformValue(Object source) { - return getPropertyValueTransformer().tranform(source); + return getPropertyValueTransformer().convert(source); } + /** + * Creates new case ignoring {@link PropertySpecifier} for given path. + * + * @param propertyPath must not be {@literal null}. + * @return + */ public static PropertySpecifier ignoreCase(String propertyPath) { return new Builder(propertyPath).ignoreCase().get(); } + /** + * Create new {@link Builder} for specifying {@link PropertySpecifier}. + * + * @param propertyPath must not be {@literal null}. + * @return + */ public static Builder newPropertySpecifier(String propertyPath) { return new Builder(propertyPath); } + /** + * Builder for specifying desired behavior of {@link PropertySpecifier}. + * + * @author Christoph Strobl + */ public static class Builder { private PropertySpecifier specifier; @@ -70,40 +127,88 @@ public static class Builder { specifier = new PropertySpecifier(path); } + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @return + * @see Builder#stringMatcher(StringMatcher) + */ public Builder with(StringMatcher stringMatcher) { return stringMatcher(stringMatcher); } + /** + * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. + * + * @param valueTransformer + * @return + * @see Builder#valueTransformer(PropertyValueTransformer) + */ public Builder with(PropertyValueTransformer valueTransformer) { return valueTransformer(valueTransformer); } + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @return + */ public Builder stringMatcher(StringMatcher stringMatcher) { + return stringMatcher(stringMatcher, specifier.ignoreCase); + } + + /** + * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. + * + * @param stringMatcher + * @param ignoreCase + * @return + */ + public Builder stringMatcher(StringMatcher stringMatcher, Boolean ignoreCase) { + specifier.stringMatcher = stringMatcher; + specifier.ignoreCase = ignoreCase; return this; } + /** + * @return + */ public Builder ignoreCase() { - specifier.ignoreCase = true; + specifier.ignoreCase = Boolean.TRUE; return this; } + /** + * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. + * + * @param valueTransformer + * @return + */ public Builder valueTransformer(PropertyValueTransformer valueTransformer) { specifier.valueTransformer = valueTransformer; return this; } + /** + * @return {@link PropertySpecifier} as defined. + */ public PropertySpecifier get() { return this.specifier; } } + /** + * @author Christoph Strobl + */ static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { INSTANCE; @Override - public Object tranform(Object source) { + public Object convert(Object source) { return source; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java index fafcb9da67..30d779a1bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java @@ -15,9 +15,11 @@ */ package org.springframework.data.domain; +import org.springframework.core.convert.converter.Converter; + /** * @author Christoph Strobl */ -public interface PropertyValueTransformer { - Object tranform(Object souce); +public interface PropertyValueTransformer extends Converter { + // TODO: should we use the converter interface directly or not at all? } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index e4753cc8e5..ce9fbcabe8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -24,13 +24,14 @@ import java.util.regex.Pattern; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.NullHandling; +import org.springframework.data.domain.Example.NullHandler; import org.springframework.data.domain.Example.StringMatcher; import org.springframework.data.domain.PropertySpecifier; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.MongoRegexCreator; import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -84,7 +85,7 @@ public DBObject getMappedExample(Example example, MongoPersistentEntity en applyPropertySpecs("", reference, example); - return ObjectUtils.nullSafeEquals(NullHandling.INCLUDE_NULL, example.getNullHandling()) ? reference + return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, example.getNullHandler()) ? reference : new BasicDBObject(SerializationUtils.flatMap(reference)); } @@ -158,6 +159,7 @@ private void applyPropertySpecs(String path, DBObject source, Example example PropertySpecifier specifier = null; StringMatcher stringMatcher = example.getDefaultStringMatcher(); Object value = entry.getValue(); + boolean ignoreCase = example.isIngnoreCaseEnabled(); if (example.hasPropertySpecifiers()) { @@ -165,11 +167,18 @@ private void applyPropertySpecs(String path, DBObject source, Example example propertyPath, example); specifier = example.getPropertySpecifier(mappedPropertyPath); - if (specifier != null && !specifier.getStringMatcher().equals(StringMatcher.DEFAULT)) { - stringMatcher = specifier.getStringMatcher(); + if (specifier != null) { + if (specifier.hasStringMatcher()) { + stringMatcher = specifier.getStringMatcher(); + } + if (specifier.getIgnoreCase() != null) { + ignoreCase = specifier.getIgnoreCase(); + } + } } + // TODO: should a PropertySpecifier outrule the later on string matching? if (specifier != null) { value = specifier.transformValue(value); @@ -182,53 +191,33 @@ private void applyPropertySpecs(String path, DBObject source, Example example } if (entry.getValue() instanceof String) { - applyStringMatcher(example, stringMatcher, entry); + applyStringMatcher(entry, stringMatcher, ignoreCase); } else if (entry.getValue() instanceof BasicDBObject) { applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), example); } } } - private void applyStringMatcher(Example example, StringMatcher stringMatcher, Map.Entry entry) { + private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, boolean ignoreCase) { - // TODO: extract common stuff from MongoQueryCreator BasicDBObject dbo = new BasicDBObject(); - switch (stringMatcher) { - case REGEX: - dbo.put("$regex", entry.getValue()); - entry.setValue(dbo); - break; - case DEFAULT: + if (ObjectUtils.nullSafeEquals(StringMatcher.DEFAULT, stringMatcher)) { - if (example.isIngnoreCaseEnabled()) { - dbo.put("$regex", Pattern.quote((String) entry.getValue())); - entry.setValue(dbo); - } - break; - case CONTAINING: - dbo.put("$regex", ".*" + entry.getValue() + ".*"); - entry.setValue(dbo); - break; - case STARTING: - dbo.put("$regex", "^" + entry.getValue()); + if (ignoreCase) { + dbo.put("$regex", Pattern.quote((String) entry.getValue())); entry.setValue(dbo); - break; - case ENDING: - dbo.put("$regex", entry.getValue() + "$"); - entry.setValue(dbo); - break; - case EXACT: - dbo.put("$regex", "^" + entry.getValue() + "$"); - entry.setValue(dbo); - break; - default: + } + } else { + + String expression = MongoRegexCreator.INSTANCE + .toRegularExpression((String) entry.getValue(), stringMatcher.getPartType()); + dbo.put("$regex", expression); + entry.setValue(dbo); } - // sometimes order matters in MongoDB so make sure to add $options after $regex. - if (example.isIngnoreCaseEnabled()) { + if (ignoreCase) { dbo.put("$options", "i"); } } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java new file mode 100644 index 0000000000..4f7b4f042a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.query; + +import java.util.regex.Pattern; + +import org.springframework.data.repository.query.parser.Part.Type; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + * @since 1.8 + */ +public enum MongoRegexCreator { + + INSTANCE; + + private static final Pattern PUNCTATION_PATTERN = Pattern.compile("\\p{Punct}"); + + /** + * Creates a regular expression String to be used with {@code $regex}. + * + * @param source the plain String + * @param type + * @return {@literal source} when {@literal source} or {@literal type} is {@literal null}. + */ + public String toRegularExpression(String source, Type type) { + + if (type == null || source == null) { + return source; + } + + String regex = prepareAndEscapeStringBeforeApplyingLikeRegex(source, type); + + switch (type) { + case STARTING_WITH: + regex = "^" + regex; + break; + case ENDING_WITH: + regex = regex + "$"; + break; + case CONTAINING: + case NOT_CONTAINING: + regex = ".*" + regex + ".*"; + break; + case SIMPLE_PROPERTY: + case NEGATING_SIMPLE_PROPERTY: + regex = "^" + regex + "$"; + default: + } + + return regex; + } + + private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Type type) { + + if (!ObjectUtils.nullSafeEquals(Type.LIKE, type)) { + return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source; + } + + if (source.equals("*")) { + return ".*"; + } + + StringBuilder sb = new StringBuilder(); + + boolean leadingWildcard = source.startsWith("*"); + boolean trailingWildcard = source.endsWith("*"); + + String valueToUse = source.substring(leadingWildcard ? 1 : 0, + trailingWildcard ? source.length() - 1 : source.length()); + + if (PUNCTATION_PATTERN.matcher(valueToUse).find()) { + valueToUse = Pattern.quote(valueToUse); + } + + if (leadingWildcard) { + sb.append(".*"); + } + sb.append(valueToUse); + if (trailingWildcard) { + sb.append(".*"); + } + + return sb.toString(); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 8ef7c11507..9c09a149f0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -38,6 +38,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.data.mongodb.core.query.MongoRegexCreator; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor.PotentiallyConvertingIterator; import org.springframework.data.repository.query.parser.AbstractQueryCreator; @@ -46,7 +47,6 @@ import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Custom query creator to create Mongo criterias. @@ -403,61 +403,7 @@ private Object[] nextAsArray(PotentiallyConvertingIterator iterator, MongoPersis } private String toLikeRegex(String source, Part part) { - - Type type = part.getType(); - String regex = prepareAndEscapeStringBeforeApplyingLikeRegex(source, part); - - switch (type) { - case STARTING_WITH: - regex = "^" + regex; - break; - case ENDING_WITH: - regex = regex + "$"; - break; - case CONTAINING: - case NOT_CONTAINING: - regex = ".*" + regex + ".*"; - break; - case SIMPLE_PROPERTY: - case NEGATING_SIMPLE_PROPERTY: - regex = "^" + regex + "$"; - default: - } - - return regex; - } - - private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Part qpart) { - - if (!ObjectUtils.nullSafeEquals(Type.LIKE, qpart.getType())) { - return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source; - } - - if ("*".equals(source)) { - return ".*"; - } - - StringBuilder sb = new StringBuilder(); - - boolean leadingWildcard = source.startsWith("*"); - boolean trailingWildcard = source.endsWith("*"); - - String valueToUse = source.substring(leadingWildcard ? 1 : 0, - trailingWildcard ? source.length() - 1 : source.length()); - - if (PUNCTATION_PATTERN.matcher(valueToUse).find()) { - valueToUse = Pattern.quote(valueToUse); - } - - if (leadingWildcard) { - sb.append(".*"); - } - sb.append(valueToUse); - if (trailingWildcard) { - sb.append(".*"); - } - - return sb.toString(); + return MongoRegexCreator.INSTANCE.toRegularExpression(source, part.getType()); } private boolean isSpherical(MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index c83c6ab99d..1db709912b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -18,7 +18,7 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.domain.Example.*; -import static org.springframework.data.domain.Example.NullHandling.*; +import static org.springframework.data.domain.Example.NullHandler.*; import static org.springframework.data.domain.Example.StringMatcher.*; import static org.springframework.data.domain.PropertySpecifier.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; @@ -172,7 +172,7 @@ public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktM probe.flatDoc = new FlatDocument(); probe.flatDoc.stringValue = "conflux"; - Example example = newExampleOf(probe).nullHandling(INCLUDE_NULL).get(); + Example example = newExampleOf(probe).nullHandling(INCLUDE).get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -404,6 +404,22 @@ public void mappingShouldFavorFieldSpecificationStringMatcherOverDefaultStringMa .add("custom_field_name", "steelheart").get())); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldIncludePropertiesFromHierarchicalDocument() { + + HierachicalDocument probe = new HierachicalDocument(); + probe.stringValue = "firefight"; + probe.customNamedField = "steelheart"; + probe.anotherStringValue = "calamity"; + + DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, isBsonObject().containing("anotherStringValue", "calamity")); + } + static class FlatDocument { @Id String id; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index abdf3738a7..95a0c7792f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -28,12 +28,15 @@ import java.util.stream.Stream; import org.hamcrest.Matchers; +import org.hamcrest.core.Is; +import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; +import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; @@ -55,6 +58,7 @@ import org.springframework.data.mongodb.repository.SampleEvaluationContextExtension.SampleSecurityContextHolder; import org.springframework.data.querydsl.QSort; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.test.util.ReflectionTestUtils; /** * Base class for tests for {@link PersonRepository}. @@ -1222,4 +1226,40 @@ public void shouldFindByFirstnameForSpELExpressionWithParameterVariableOnly() { assertThat(users.get(0), is(dave)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldResolveStuffCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + + // needed to tweak stuff a bit since some field are automatically set - so we need to undo this + ReflectionTestUtils.setField(sample, "id", null); + ReflectionTestUtils.setField(sample, "createdAt", null); + ReflectionTestUtils.setField(sample, "email", null); + + Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); + Assert.assertThat(result.getNumberOfElements(), Is.is(2)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findAllByExampleShouldResolveStuffCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + + // needed to tweak stuff a bit since some field are automatically set - so we need to undo this + ReflectionTestUtils.setField(sample, "id", null); + ReflectionTestUtils.setField(sample, "createdAt", null); + ReflectionTestUtils.setField(sample, "email", null); + + List result = repository.findAllByExample(new Example(sample)); + Assert.assertThat(result.size(), Is.is(2)); + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java index 13271d1607..2c5a72a03b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryIntegrationTests.java @@ -15,16 +15,7 @@ */ package org.springframework.data.mongodb.repository; -import java.util.List; - -import org.hamcrest.core.Is; -import org.junit.Assert; -import org.junit.Test; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; import org.springframework.test.context.ContextConfiguration; -import org.springframework.test.util.ReflectionTestUtils; /** * Integration test for {@link PersonRepository}. @@ -33,41 +24,4 @@ * @author Thomas Darimont */ @ContextConfiguration -public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests { - - /** - * @see DATAMONGO-1245 - */ - @Test - public void findByExampleShouldResolveStuffCorrectly() { - - Person sample = new Person(); - sample.setLastname("Matthews"); - - // needed to tweak stuff a bit since some field are automatically set - so we need to undo this - ReflectionTestUtils.setField(sample, "id", null); - ReflectionTestUtils.setField(sample, "createdAt", null); - ReflectionTestUtils.setField(sample, "email", null); - - Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); - Assert.assertThat(result.getNumberOfElements(), Is.is(2)); - } - - /** - * @see DATAMONGO-1245 - */ - @Test - public void findAllByExampleShouldResolveStuffCorrectly() { - - Person sample = new Person(); - sample.setLastname("Matthews"); - - // needed to tweak stuff a bit since some field are automatically set - so we need to undo this - ReflectionTestUtils.setField(sample, "id", null); - ReflectionTestUtils.setField(sample, "createdAt", null); - ReflectionTestUtils.setField(sample, "email", null); - - List result = repository.findAllByExample(new Example(sample)); - Assert.assertThat(result.size(), Is.is(2)); - } -} +public class PersonRepositoryIntegrationTests extends AbstractPersonRepositoryIntegrationTests {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 1c7f528204..3e17843d30 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -32,7 +32,7 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.NullHandling; +import org.springframework.data.domain.Example.NullHandler; import org.springframework.data.domain.Example.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -262,7 +262,7 @@ public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInSt trimDomainType(sample, "id", "createdAt", "email"); List result = repository.findAllByExample(Example.newExampleOf(sample) - .nullHandling(NullHandling.INCLUDE_NULL).get()); + .nullHandling(NullHandler.INCLUDE).get()); assertThat(result, empty()); } @@ -281,7 +281,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInS trimDomainType(sample, "id", "createdAt", "email"); List result = repository.findAllByExample(Example.newExampleOf(sample) - .nullHandling(NullHandling.INCLUDE_NULL).get()); + .nullHandling(NullHandler.INCLUDE).get()); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); From 69e81a3ad0d05aa83b73a47bf02f233c2ab970e0 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 20 Jan 2016 11:05:47 +0100 Subject: [PATCH 05/14] DATAMONGO-1245 - Switch to types used in DATACMNS-810 --- .../springframework/data/domain/Example.java | 369 ------------------ .../data/domain/PropertySpecifier.java | 216 ---------- .../data/domain/PropertyValueTransformer.java | 25 -- .../data/mongodb/core/MongoTemplate.java | 110 +++--- .../core/convert/MongoExampleMapper.java | 26 +- .../convert/MongoExampleMapperUnitTests.java | 16 +- .../support/SimpleMongoRepositoryTests.java | 11 +- 7 files changed, 77 insertions(+), 696 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java deleted file mode 100644 index 9599ef6420..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/Example.java +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.domain; - -import java.util.LinkedHashMap; -import java.util.Map; - -import org.springframework.data.repository.query.parser.Part; -import org.springframework.data.repository.query.parser.Part.Type; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; - -/** - * Support for query by example (QBE). - * - * @author Christoph Strobl - * @param - */ -public class Example { - - private final T probe; - - private NullHandler nullHandler = NullHandler.IGNORE; - private StringMatcher defaultStringMatcher = StringMatcher.DEFAULT; - private PropertySpecifiers propertySpecifiers = new PropertySpecifiers(); - - private boolean ignoreCase = false; - - /** - * Create a new {@link Example} including all non-null properties by default. - * - * @param probe The example to use. Must not be {@literal null}. - */ - public Example(S probe) { - - Assert.notNull(probe, "Probe must not be null!"); - this.probe = probe; - } - - /** - * Get the example used. - * - * @return never {@literal null}. - */ - public T getProbe() { - return probe; - } - - /** - * Get defined null handling. - * - * @return never {@literal null} - */ - public NullHandler getNullHandler() { - return nullHandler; - } - - /** - * Get defined {@link StringMatcher}. - * - * @return never {@literal null}. - */ - public StringMatcher getDefaultStringMatcher() { - return defaultStringMatcher; - } - - /** - * @return {@literal true} if {@link String} should be matched with ignore case option. - */ - public boolean isIngnoreCaseEnabled() { - return this.ignoreCase; - } - - /** - * @param path Dot-Path to property. - * @return {@literal true} in case {@link PropertySpecifier} defined for given path. - */ - public boolean hasPropertySpecifier(String path) { - return propertySpecifiers.hasSpecifierForPath(path); - } - - /** - * @param path Dot-Path to property. - * @return {@literal null} when no {@link PropertySpecifier} defined for path. - */ - public PropertySpecifier getPropertySpecifier(String path) { - return propertySpecifiers.getForPath(path); - } - - /** - * @return true if at least one {@link PropertySpecifier} defined. - */ - public boolean hasPropertySpecifiers() { - return this.propertySpecifiers.hasValues(); - } - - /** - * Get the actual type for the example used. - * - * @return - */ - @SuppressWarnings("unchecked") - public Class getProbeType() { - return (Class) ClassUtils.getUserClass(probe.getClass()); - } - - /** - * Create a new {@link Example} including all non-null properties by default. - * - * @param probe must not be {@literal null}. - * @return - */ - public static Example exampleOf(S probe) { - return new Example(probe); - } - - /** - * Create a new {@link Example} including all non-null properties, excluding explicitly named properties to ignore. - * - * @param probe must not be {@literal null}. - * @return - */ - public static Example exampleOf(S probe, String... ignoredProperties) { - return new Builder(probe).ignore(ignoredProperties).get(); - } - - /** - * Create new {@link Builder} for specifying {@link Example}. - * - * @param probe must not be {@literal null}. - * @return - * @see Builder - */ - public static Builder newExampleOf(S probe) { - return new Builder(probe); - } - - /** - * Builder for specifying desired behavior of {@link Example}. - * - * @author Christoph Strobl - * @param - */ - public static class Builder { - - private Example example; - - Builder(T probe) { - example = new Example(probe); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling - * @return - * @see Builder#nullHandling(NullHandler) - */ - public Builder with(NullHandler nullHandling) { - return nullHandling(nullHandling); - } - - /** - * Sets default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher - * @return - * @see Builder#stringMatcher(StringMatcher) - */ - public Builder with(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher); - } - - /** - * Adds {@link PropertySpecifier} used for {@link Example}. - * - * @param specifier - * @return - * @see Builder#specify(PropertySpecifier...) - */ - public Builder with(PropertySpecifier... specifiers) { - return specify(specifiers); - } - - /** - * Sets {@link NullHandler} used for {@link Example}. - * - * @param nullHandling Defaulted to {@link NullHandler#INCLUDE} in case of {@literal null}. - * @return - */ - public Builder nullHandling(NullHandler nullHandling) { - - example.nullHandler = nullHandling == null ? NullHandler.IGNORE : nullHandling; - return this; - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @return - */ - public Builder stringMatcher(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher, example.ignoreCase); - } - - /** - * Sets the default {@link StringMatcher} used for {@link Example}. - * - * @param stringMatcher Defaulted to {@link StringMatcher#DEFAULT} in case of {@literal null}. - * @param ignoreCase - * @return - */ - public Builder stringMatcher(StringMatcher stringMatching, boolean ignoreCase) { - - example.defaultStringMatcher = stringMatching == null ? StringMatcher.DEFAULT : stringMatching; - example.ignoreCase = ignoreCase; - return this; - } - - /** - * @return - */ - public Builder ignoreCase() { - example.ignoreCase = true; - return this; - } - - /** - * Define specific property handling. - * - * @param specifiers - * @return - */ - public Builder specify(PropertySpecifier... specifiers) { - - for (PropertySpecifier specifier : specifiers) { - example.propertySpecifiers.add(specifier); - } - return this; - } - - /** - * Ignore given properties. - * - * @param ignoredProperties - * @return - */ - public Builder ignore(String... ignoredProperties) { - - for (String ignoredProperty : ignoredProperties) { - specify(PropertySpecifier.newPropertySpecifier(ignoredProperty) - .valueTransformer(new ExcludingValueTransformer()).get()); - } - return this; - } - - /** - * @return {@link Example} as defined. - */ - public Example get() { - return this.example; - } - } - - /** - * Null handling for creating criterion out of an {@link Example}. - * - * @author Christoph Strobl - */ - public static enum NullHandler { - - INCLUDE, IGNORE - } - - /** - * Match modes for treatment of {@link String} values. - * - * @author Christoph Strobl - */ - public static enum StringMatcher { - - /** - * Store specific default. - */ - DEFAULT(null), - /** - * Matches the exact string - */ - EXACT(Type.SIMPLE_PROPERTY), - /** - * Matches string starting with pattern - */ - STARTING(Type.STARTING_WITH), - /** - * Matches string ending with pattern - */ - ENDING(Type.ENDING_WITH), - /** - * Matches string containing pattern - */ - CONTAINING(Type.CONTAINING), - /** - * Treats strings as regular expression patterns - */ - REGEX(Type.REGEX); - - private Type type; - - private StringMatcher(Type type) { - this.type = type; - } - - /** - * Get the according {@link Part.Type}. - * - * @return {@literal null} for {@link StringMatcher#DEFAULT}. - */ - public Type getPartType() { - return type; - } - - } - - public static class ExcludingValueTransformer implements PropertyValueTransformer { - - @Override - public Object convert(Object source) { - return null; - } - } - - static class PropertySpecifiers { - - private Map propertySpecifiers = new LinkedHashMap(); - - public void add(PropertySpecifier specifier) { - - Assert.notNull(specifier, "PropertySpecifier must not be null!"); - propertySpecifiers.put(specifier.getPath(), specifier); - } - - public boolean hasSpecifierForPath(String path) { - return propertySpecifiers.containsKey(path); - } - - public PropertySpecifier getForPath(String path) { - return propertySpecifiers.get(path); - } - - public boolean hasValues() { - return !propertySpecifiers.isEmpty(); - } - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java deleted file mode 100644 index da5981be09..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertySpecifier.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.domain; - -import org.springframework.data.domain.Example.StringMatcher; -import org.springframework.util.Assert; - -/** - * Define specific property handling for a Dot-Path. - * - * @author Christoph Strobl - */ -public class PropertySpecifier { - - private final String path; - - private StringMatcher stringMatcher; - private Boolean ignoreCase; - - private PropertyValueTransformer valueTransformer; - - /** - * Creates new {@link PropertySpecifier} for given path. - * - * @param path Dot-Path to the property. Must not be {@literal null}. - */ - PropertySpecifier(String path) { - - Assert.hasText(path, "Path must not be null/empty!"); - this.path = path; - } - - /** - * Get the properties Dot-Path. - * - * @return never {@literal null}. - */ - public String getPath() { - return path; - } - - /** - * Get the {@link StringMatcher}. - * - * @return can be {@literal null}. - */ - public StringMatcher getStringMatcher() { - return stringMatcher; - } - - /** - * @return {literal true} in case {@link StringMatcher} defined. - */ - public boolean hasStringMatcher() { - return this.stringMatcher != null; - } - - /** - * @return {@literal null} if not set. - */ - public Boolean getIgnoreCase() { - return ignoreCase; - } - - /** - * Get the property transformer to be applied. - * - * @return never {@literal null}. - */ - public PropertyValueTransformer getPropertyValueTransformer() { - return valueTransformer == null ? NoOpPropertyValueTransformer.INSTANCE : valueTransformer; - } - - /** - * Transforms a given source using the {@link PropertyValueTransformer}. - * - * @param source - * @return - */ - public Object transformValue(Object source) { - return getPropertyValueTransformer().convert(source); - } - - /** - * Creates new case ignoring {@link PropertySpecifier} for given path. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static PropertySpecifier ignoreCase(String propertyPath) { - return new Builder(propertyPath).ignoreCase().get(); - } - - /** - * Create new {@link Builder} for specifying {@link PropertySpecifier}. - * - * @param propertyPath must not be {@literal null}. - * @return - */ - public static Builder newPropertySpecifier(String propertyPath) { - return new Builder(propertyPath); - } - - /** - * Builder for specifying desired behavior of {@link PropertySpecifier}. - * - * @author Christoph Strobl - */ - public static class Builder { - - private PropertySpecifier specifier; - - Builder(String path) { - specifier = new PropertySpecifier(path); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - * @see Builder#stringMatcher(StringMatcher) - */ - public Builder with(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher); - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - * @see Builder#valueTransformer(PropertyValueTransformer) - */ - public Builder with(PropertyValueTransformer valueTransformer) { - return valueTransformer(valueTransformer); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @return - */ - public Builder stringMatcher(StringMatcher stringMatcher) { - return stringMatcher(stringMatcher, specifier.ignoreCase); - } - - /** - * Sets the {@link StringMatcher} used for {@link PropertySpecifier}. - * - * @param stringMatcher - * @param ignoreCase - * @return - */ - public Builder stringMatcher(StringMatcher stringMatcher, Boolean ignoreCase) { - - specifier.stringMatcher = stringMatcher; - specifier.ignoreCase = ignoreCase; - return this; - } - - /** - * @return - */ - public Builder ignoreCase() { - specifier.ignoreCase = Boolean.TRUE; - return this; - } - - /** - * Sets the {@link PropertyValueTransformer} used for {@link PropertySpecifier}. - * - * @param valueTransformer - * @return - */ - public Builder valueTransformer(PropertyValueTransformer valueTransformer) { - specifier.valueTransformer = valueTransformer; - return this; - } - - /** - * @return {@link PropertySpecifier} as defined. - */ - public PropertySpecifier get() { - return this.specifier; - } - } - - /** - * @author Christoph Strobl - */ - static enum NoOpPropertyValueTransformer implements PropertyValueTransformer { - - INSTANCE; - - @Override - public Object convert(Object source) { - return source; - } - - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java deleted file mode 100644 index 30d779a1bd..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/domain/PropertyValueTransformer.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2015 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.domain; - -import org.springframework.core.convert.converter.Converter; - -/** - * @author Christoph Strobl - */ -public interface PropertyValueTransformer extends Converter { - // TODO: should we use the converter interface directly or not at all? -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 04a6288c83..f01a2beb42 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -340,8 +340,8 @@ public CloseableIterator doInCollection(DBCollection collection) throws Mongo DBCursor cursor = collection.find(mappedQuery, mappedFields); QueryCursorPreparer cursorPreparer = new QueryCursorPreparer(query, entityType); - ReadDbObjectCallback readCallback = new ReadDbObjectCallback(mongoConverter, entityType, - collection.getName()); + ReadDbObjectCallback readCallback = new ReadDbObjectCallback(mongoConverter, entityType, collection + .getName()); return new CloseableIterableCursorAdapter(cursorPreparer.prepare(cursor), exceptionTranslator, readCallback); } @@ -374,8 +374,8 @@ public CommandResult doInDB(DB db) throws MongoException, DataAccessException { */ @Deprecated public CommandResult executeCommand(final DBObject command, final int options) { - return executeCommand(command, - (options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred() : ReadPreference.primary()); + return executeCommand(command, (options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred() + : ReadPreference.primary()); } /* @@ -423,8 +423,7 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan * @param preparer allows for customization of the {@link DBCursor} used when iterating over the result set, (apply * limits, skips and so on). */ - protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch, - CursorPreparer preparer) { + protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch, CursorPreparer preparer) { Assert.notNull(query); @@ -647,7 +646,7 @@ public List findByExample(S sample) { public List findByExample(Example sample) { Assert.notNull(sample, "Sample object must not be null!"); - return (List) find(new Query(new Criteria().alike(sample)), sample.getProbeType()); + return (List) find(new Query(new Criteria().alike(sample)), sample.getSampleType()); } public GeoResults geoNear(NearQuery near, Class entityClass) { @@ -680,8 +679,8 @@ public GeoResults geoNear(NearQuery near, Class entityClass, String co List results = (List) commandResult.get("results"); results = results == null ? Collections.emptyList() : results; - DbObjectCallback> callback = new GeoNearResultDbObjectCallback( - new ReadDbObjectCallback(mongoConverter, entityClass, collectionName), near.getMetric()); + DbObjectCallback> callback = new GeoNearResultDbObjectCallback(new ReadDbObjectCallback( + mongoConverter, entityClass, collectionName), near.getMetric()); List> result = new ArrayList>(results.size()); int index = 0; @@ -757,9 +756,8 @@ public long count(final Query query, String collectionName) { public long count(Query query, Class entityClass, String collectionName) { Assert.hasText(collectionName); - final DBObject dbObject = query == null ? null - : queryMapper.getMappedObject(query.getQueryObject(), - entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); + final DBObject dbObject = query == null ? null : queryMapper.getMappedObject(query.getQueryObject(), + entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); return execute(collectionName, new CollectionCallback() { public Long doInCollection(DBCollection collection) throws MongoException, DataAccessException { @@ -1036,8 +1034,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc) - : collection.insert(dbDoc, writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc) : collection.insert(dbDoc, + writeConcernToUse); handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.INSERT); return dbDoc.get(ID_FIELD); } @@ -1057,8 +1055,8 @@ public Void doInCollection(DBCollection collection) throws MongoException, DataA MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT_LIST, collectionName, null, null, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList) - : collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList) : collection.insert( + dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); handleAnyWriteResultErrors(writeResult, null, MongoActionOperation.INSERT_LIST); return null; } @@ -1086,8 +1084,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc) - : collection.save(dbDoc, writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc) : collection.save(dbDoc, + writeConcernToUse); handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.SAVE); return dbDoc.get(ID_FIELD); } @@ -1140,10 +1138,10 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException increaseVersionForUpdateIfNecessary(entity, update); - DBObject queryObj = query == null ? new BasicDBObject() - : queryMapper.getMappedObject(query.getQueryObject(), entity); - DBObject updateObj = update == null ? new BasicDBObject() - : updateMapper.getMappedObject(update.getUpdateObject(), entity); + DBObject queryObj = query == null ? new BasicDBObject() : queryMapper.getMappedObject(query.getQueryObject(), + entity); + DBObject updateObj = update == null ? new BasicDBObject() : updateMapper.getMappedObject( + update.getUpdateObject(), entity); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Calling update using query: %s and update: %s in collection: %s", @@ -1284,9 +1282,9 @@ private void assertUpdateableIdIfNotSet(Object entity) { Object idValue = persistentEntity.getPropertyAccessor(entity).getProperty(idProperty); if (idValue == null && !MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(idProperty.getType())) { - throw new InvalidDataAccessApiUsageException( - String.format("Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(), - entity.getClass().getName())); + throw new InvalidDataAccessApiUsageException(String.format( + "Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(), entity.getClass() + .getName())); } } @@ -1325,12 +1323,12 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Remove using query: {} in collection: {}.", - new Object[] { serializeToJsonSafely(dboq), collection.getName() }); + LOGGER.debug("Remove using query: {} in collection: {}.", new Object[] { serializeToJsonSafely(dboq), + collection.getName() }); } - WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) - : collection.remove(dboq, writeConcernToUse); + WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq, + writeConcernToUse); handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE); @@ -1346,8 +1344,8 @@ public List findAll(Class entityClass) { } public List findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(null), null, - new ReadDbObjectCallback(mongoConverter, entityClass, collectionName), collectionName); + return executeFindMultiInternal(new FindCallback(null), null, new ReadDbObjectCallback(mongoConverter, + entityClass, collectionName), collectionName); } public MapReduceResults mapReduce(String inputCollectionName, String mapFunction, String reduceFunction, @@ -1363,8 +1361,8 @@ public MapReduceResults mapReduce(String inputCollectionName, String mapF public MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, String reduceFunction, Class entityClass) { - return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, new MapReduceOptions().outputTypeInline(), - entityClass); + return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, + new MapReduceOptions().outputTypeInline(), entityClass); } public MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, @@ -1375,9 +1373,8 @@ public MapReduceResults mapReduce(Query query, String inputCollectionName DBCollection inputCollection = getCollection(inputCollectionName); MapReduceCommand command = new MapReduceCommand(inputCollection, mapFunc, reduceFunc, - mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), - query == null || query.getQueryObject() == null ? null - : queryMapper.getMappedObject(query.getQueryObject(), null)); + mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), query == null + || query.getQueryObject() == null ? null : queryMapper.getMappedObject(query.getQueryObject(), null)); copyMapReduceOptionsToCommand(query, mapReduceOptions, command); @@ -1713,8 +1710,8 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), - new ReadDbObjectCallback(this.mongoConverter, entityClass, collectionName), collectionName); + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), new ReadDbObjectCallback( + this.mongoConverter, entityClass, collectionName), collectionName); } /** @@ -1728,8 +1725,8 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields * @return the List of converted objects. */ protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass) { - return doFind(collectionName, query, fields, entityClass, null, - new ReadDbObjectCallback(this.mongoConverter, entityClass, collectionName)); + return doFind(collectionName, query, fields, entityClass, null, new ReadDbObjectCallback(this.mongoConverter, + entityClass, collectionName)); } /** @@ -1747,8 +1744,8 @@ protected List doFind(String collectionName, DBObject query, DBObject fie */ protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, CursorPreparer preparer) { - return doFind(collectionName, query, fields, entityClass, preparer, - new ReadDbObjectCallback(mongoConverter, entityClass, collectionName)); + return doFind(collectionName, query, fields, entityClass, preparer, new ReadDbObjectCallback(mongoConverter, + entityClass, collectionName)); } protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, @@ -1824,13 +1821,9 @@ protected T doFindAndModify(String collectionName, DBObject query, DBObject DBObject mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); if (LOGGER.isDebugEnabled()) { - LOGGER - .debug( - String.format( - "findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " - + "in collection: %s", - serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(mappedUpdate), - collectionName)); + LOGGER.debug(String.format("findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " + + "in collection: %s", serializeToJsonSafely(mappedQuery), fields, sort, entityClass, + serializeToJsonSafely(mappedUpdate), collectionName)); } return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options), @@ -1900,8 +1893,8 @@ private T executeFindOneInternal(CollectionCallback collectionCall DbObjectCallback objectCallback, String collectionName) { try { - T result = objectCallback - .doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), collectionName))); + T result = objectCallback.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), + collectionName))); return result; } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -1926,8 +1919,8 @@ private T executeFindOneInternal(CollectionCallback collectionCall * @param collectionName the collection to be queried * @return */ - private List executeFindMultiInternal(CollectionCallback collectionCallback, CursorPreparer preparer, - DbObjectCallback objectCallback, String collectionName) { + private List executeFindMultiInternal(CollectionCallback collectionCallback, + CursorPreparer preparer, DbObjectCallback objectCallback, String collectionName) { try { @@ -2017,8 +2010,8 @@ String determineCollectionName(Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); if (entity == null) { - throw new InvalidDataAccessApiUsageException( - "No Persistent Entity information found for the class " + entityClass.getName()); + throw new InvalidDataAccessApiUsageException("No Persistent Entity information found for the class " + + entityClass.getName()); } return entity.getCollection(); } @@ -2082,8 +2075,8 @@ private void handleCommandError(CommandResult result, DBObject source) { String error = result.getErrorMessage(); error = error == null ? "NO MESSAGE" : error; - throw new InvalidDataAccessApiUsageException( - "Command execution failed: Error [" + error + "], Command = " + source, ex); + throw new InvalidDataAccessApiUsageException("Command execution failed: Error [" + error + "], Command = " + + source, ex); } } @@ -2279,8 +2272,7 @@ public T doWith(DBObject object) { class UnwrapAndReadDbObjectCallback extends ReadDbObjectCallback { - public UnwrapAndReadDbObjectCallback(EntityReader reader, Class type, - String collectionName) { + public UnwrapAndReadDbObjectCallback(EntityReader reader, Class type, String collectionName) { super(reader, type, collectionName); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index ce9fbcabe8..3332e4f568 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -63,7 +63,7 @@ public MongoExampleMapper(MongoConverter converter) { * @since 1.8 */ public DBObject getMappedExample(Example example) { - return getMappedExample(example, mappingContext.getPersistentEntity(example.getProbeType())); + return getMappedExample(example, mappingContext.getPersistentEntity(example.getSampleType())); } /** @@ -77,21 +77,21 @@ public DBObject getMappedExample(Example example) { */ public DBObject getMappedExample(Example example, MongoPersistentEntity entity) { - DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe()); + DBObject reference = (DBObject) converter.convertToMongoType(example.getSampleObject()); - if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) { + if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getSampleObject()).getIdentifier() == null) { reference.removeField(entity.getIdProperty().getFieldName()); } applyPropertySpecs("", reference, example); - return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, example.getNullHandler()) ? reference - : new BasicDBObject(SerializationUtils.flatMap(reference)); + return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, example.getNullHandler()) ? reference : new BasicDBObject( + SerializationUtils.flatMap(reference)); } private String getMappedPropertyPath(String path, Example example) { - MongoPersistentEntity entity = mappingContext.getPersistentEntity(example.getProbeType()); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(example.getSampleType()); Iterator parts = Arrays.asList(path.split("\\.")).iterator(); @@ -118,7 +118,7 @@ public void doWithPersistentProperty(MongoPersistentProperty property) { }); if (stack.isEmpty()) { - throw new RuntimeException("foobar"); + return ""; } prop = stack.pop(); } @@ -154,7 +154,12 @@ private void applyPropertySpecs(String path, DBObject source, Example example } String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); - String mappedPropertyPath = propertyPath; + + String mappedPropertyPath = getMappedPropertyPath(propertyPath, example); + if (example.isIgnoredPath(propertyPath) || example.isIgnoredPath(mappedPropertyPath)) { + iter.remove(); + continue; + } PropertySpecifier specifier = null; StringMatcher stringMatcher = example.getDefaultStringMatcher(); @@ -165,6 +170,7 @@ private void applyPropertySpecs(String path, DBObject source, Example example mappedPropertyPath = example.hasPropertySpecifier(propertyPath) ? propertyPath : getMappedPropertyPath( propertyPath, example); + specifier = example.getPropertySpecifier(mappedPropertyPath); if (specifier != null) { @@ -210,8 +216,8 @@ private void applyStringMatcher(Map.Entry entry, StringMatcher s } } else { - String expression = MongoRegexCreator.INSTANCE - .toRegularExpression((String) entry.getValue(), stringMatcher.getPartType()); + String expression = MongoRegexCreator.INSTANCE.toRegularExpression((String) entry.getValue(), + stringMatcher.getPartType()); dbo.put("$regex", expression); entry.setValue(dbo); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index 1db709912b..956e6bc40b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -18,8 +18,6 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.domain.Example.*; -import static org.springframework.data.domain.Example.NullHandler.*; -import static org.springframework.data.domain.Example.StringMatcher.*; import static org.springframework.data.domain.PropertySpecifier.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; @@ -172,7 +170,7 @@ public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktM probe.flatDoc = new FlatDocument(); probe.flatDoc.stringValue = "conflux"; - Example example = newExampleOf(probe).nullHandling(INCLUDE).get(); + Example example = newExampleOf(probe).includeNullValues().get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -189,7 +187,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarti probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).stringMatcher(STARTING).get(); + Example example = newExampleOf(probe).matchStringsStartingWith().get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -208,7 +206,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).stringMatcher(ENDING).get(); + Example example = newExampleOf(probe).matchStringsEndingWith().get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -227,7 +225,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMat probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).stringMatcher(ENDING, true).get(); + Example example = newExampleOf(probe).matchStringsEndingWith().matchStringsWithIgnoreCase().get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -248,7 +246,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).stringMatcher(DEFAULT, true).get(); + Example example = newExampleOf(probe).matchStringsWithIgnoreCase().get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -393,8 +391,8 @@ public void mappingShouldFavorFieldSpecificationStringMatcherOverDefaultStringMa probe.stringValue = "firefight"; probe.customNamedField = "steelheart"; - Example example = newExampleOf(probe).specify( - newPropertySpecifier("stringValue").stringMatcher(CONTAINING).get()).get(); + Example example = newExampleOf(probe).specify(newPropertySpecifier("stringValue").matchStringContaining().get()) + .get(); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 3e17843d30..b8fabfa9ed 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -32,8 +32,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.NullHandler; -import org.springframework.data.domain.Example.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.geo.Point; @@ -261,8 +259,7 @@ public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInSt sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample) - .nullHandling(NullHandler.INCLUDE).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get()); assertThat(result, empty()); } @@ -280,8 +277,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInS sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample) - .nullHandling(NullHandler.INCLUDE).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get()); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); @@ -297,8 +293,7 @@ public void findAllByExampleShouldRespectStringMatchMode() { sample.setLastname("Mat"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample) - .stringMatcher(StringMatcher.STARTING).get()); + List result = repository.findAllByExample(Example.newExampleOf(sample).matchStringsStartingWith().get()); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); From bc0a58be071313eeedd4fe1aaf47252e33deca1f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 28 Jan 2016 13:46:24 +0100 Subject: [PATCH 06/14] DATAMONGO-1245 - Align MongoRepsoitory QBE with DATAJPA-218. --- .../mongodb/repository/MongoRepository.java | 27 ++++++++++++++----- .../support/SimpleMongoRepository.java | 22 +++++++++++++-- ...tractPersonRepositoryIntegrationTests.java | 2 +- .../support/SimpleMongoRepositoryTests.java | 2 +- 4 files changed, 43 insertions(+), 10 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java index 2dfeae27e5..35ec5f3765 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,17 +76,32 @@ public interface MongoRepository extends PagingAndSo List insert(Iterable entities); /** - * @param example + * Returns all instances of the type specified by the given {@link Example}. + * + * @param example must not be {@literal null}. * @return * @since 1.8 */ List findAllByExample(Example example); /** - * @param example - * @param pageable - * @return + * Returns all instances of the type specified by the given {@link Example}. + * + * @param example must not be {@literal null}. + * @param sort can be {@literal null}. + * @return all entities sorted by the given options + * @since 1.8 + */ + List findAllByExample(Example example, Sort sort); + + /** + * Returns a {@link Page} of entities meeting the paging restriction specified by the given {@link Example} limited to + * criteria provided in the {@code Pageable} object. + * + * @param example must not be {@literal null}. + * @param pageable can be {@literal null}. + * @return a {@link Page} of entities * @since 1.8 */ - Page findByExample(Example example, Pageable pageable); + Page findAllByExample(Example example, Pageable pageable); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index f788044493..a87c184f51 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -262,10 +262,10 @@ public List insert(Iterable entities) { /* * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.MongoRepository#findByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) + * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) */ @Override - public Page findByExample(Example example, Pageable pageable) { + public Page findAllByExample(Example example, Pageable pageable) { Assert.notNull(example, "Sample must not be null!"); @@ -279,6 +279,24 @@ public Page findByExample(Example example, Pageable pageable entityInformation.getCollectionName()), pageable, count); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) + */ + @Override + public List findAllByExample(Example example, Sort sort) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)); + + if (sort != null) { + q.with(sort); + } + + return findAll(q); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 95a0c7792f..0f716fb6c5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1240,7 +1240,7 @@ public void findByExampleShouldResolveStuffCorrectly() { ReflectionTestUtils.setField(sample, "createdAt", null); ReflectionTestUtils.setField(sample, "email", null); - Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); + Page result = repository.findAllByExample(new Example(sample), new PageRequest(0, 10)); Assert.assertThat(result.getNumberOfElements(), Is.is(2)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index b8fabfa9ed..6b75c51edc 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -180,7 +180,7 @@ public void findByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - Page result = repository.findByExample(new Example(sample), new PageRequest(0, 10)); + Page result = repository.findAllByExample(new Example(sample), new PageRequest(0, 10)); assertThat(result.getContent(), hasItems(dave, oliver)); assertThat(result.getContent(), hasSize(2)); From ff60457673f07faa17f4276c6b5fe99e4103ba49 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 23 Feb 2016 17:15:50 +0100 Subject: [PATCH 07/14] DATAMONGO-1245 - Initial documentation for Query by Example. --- src/main/asciidoc/index.adoc | 1 + .../asciidoc/reference/query-by-example.adoc | 167 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 src/main/asciidoc/reference/query-by-example.adoc diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 4ae939dba8..05053485a9 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -27,6 +27,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] include::reference/introduction.adoc[] include::reference/mongodb.adoc[] include::reference/mongo-repositories.adoc[] +include::reference/query-by-example.adoc[] include::{spring-data-commons-docs}/auditing.adoc[] include::reference/mongo-auditing.adoc[] include::reference/mapping.adoc[] diff --git a/src/main/asciidoc/reference/query-by-example.adoc b/src/main/asciidoc/reference/query-by-example.adoc new file mode 100644 index 0000000000..abb15061cd --- /dev/null +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -0,0 +1,167 @@ +[[query.by.example]] += Query by Example + +== Introduction + +This chapter will give you an introduction to Query by Example and will explain how to use example specifications. + +Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. + +== Usage + +An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties. You can use Query by Example in the `MongoTemplate` and within `Repositories`. + +Query by Example is suited for several use-cases but also comes with limitations: + +**When to use** + +* Querying your data store with a set of static or dynamic constraints +* Frequent refactoring of the entities without worrying about breaking existing queries +* Works independently from the data store API + +**Limitations** + +* Query predicates are combined using the `AND` keyword +* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` +* Limited to starts/contains/ends/regex matching for strings and exact matching for other property types + + +Before getting started with Query by Example you need to have your entities set up. + +.Sample Person entity +==== +[source,java] +---- +public class Person { + + @Id + private String id; + private String firstname; + private String lastname; + private Address address; + + // … getters and setters omitted +} +---- +==== + +We have a quite simple entity here that is mapped to the data store. You can use this entity to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. + +.The first Example specification +==== +[source,xml] +---- +Person person = new Person(); <1> + +person.setFirstname("Dave"); <2> + +Example example = Example.exampleOf(person); <3> +---- +<1> Create a new instance of the entity +<2> Set the properties to query +<3> Create an `Example` +==== + + +NOTE: Property names of the sample object must correlate with the property names of the queried entity. + +.Query by Example using the MongoTemplate +==== +[source,xml] +---- +@Autowired +MongoTemplate template; + +public List findPeople(Person sampleObject) { + return template.findByExample(Example.exampleOf(person)); +} +---- +==== + +The `findByExample` method accepts either the sample object or an `Example` object to query the data store. Spring Data uses the `Example` to create and execute a query. + + +.Query by Example using the Repositories +==== +[source, java] +---- +public interface MongoRepository { + + List findAllByExample(Example example); + + List findAllByExample(Example example, Sort sort); + + Page findAllByExample(Example example, Pageable pageable); + + // … more functionality omitted. +} +---- +==== + +[[query.by.example.builder]] +== Example builder + +Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the example builder. + +.Query by Example builder +==== +[source, java] +---- +Example.newExampleOf(person) + .withStringMatcher(StringMatcher.ENDING) + .includeNullValues() + .withPropertySpecifier( + newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()) + .withPropertySpecifier( + newPropertySpecifier("lastname").matchStringsWithIgnoreCase().get()) + .withPropertySpecifier( + newPropertySpecifier("address.city").matchStringStartingWith().get()) + .get(); +---- +==== + +Property specifier accepts property names and property paths separated by dots that are contained within the sample object. A `PropertySpecifier` allows setting string matching options, case-sensitivity, + +[cols="1,2", options="header"] +.Supported string matching options of `StringMatcher` +|=== +| Matching +| Logical result + +| `DEFAULT` (case-sensitive) +| `{"firstname" : firstname}` + +| `DEFAULT` (case-insensitive) +| `{"firstname" : { $regex: firstname, $options: 'i'}}` + +| `EXACT` (case-sensitive) +| `{"firstname" : { $regex: /^firstname$/}}` + +| `EXACT` (case-insensitive) +| `{"firstname" : { $regex: /^firstname$/, $options: 'i'}}` + +| `STARTING` (case-sensitive) +| `{"firstname" : { $regex: /^firstname/}}` + +| `STARTING` (case-insensitive) +| `{"firstname" : { $regex: /^firstname/, $options: 'i'}}` + +| `ENDING` (case-sensitive) +| `{"firstname" : { $regex: /firstname$/}}` + +| `ENDING` (case-insensitive) +| `{"firstname" : { $regex: /firstname$/, $options: 'i'}}` + +| `CONTAINING` (case-sensitive) +| `{"firstname" : { $regex: /.\*firstname.*/}}` + +| `CONTAINING` (case-insensitive) +| `{"firstname" : { $regex: /.\*firstname.*/, $options: 'i'}}` + +| `REGEX` (case-sensitive) +| `{"firstname" : { $regex: /firstname/}}` + +| `REGEX` (case-insensitive) +| `{"firstname" : { $regex: /firstname/, $options: 'i'}}` + +|=== From b77e21b199c9e4b3b99c91de82de24f5afefadf2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Feb 2016 08:33:28 +0100 Subject: [PATCH 08/14] DATAMONGO-1245 - Update documentation for Query by Example. --- .../asciidoc/reference/query-by-example.adoc | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main/asciidoc/reference/query-by-example.adoc b/src/main/asciidoc/reference/query-by-example.adoc index abb15061cd..63f3cebf78 100644 --- a/src/main/asciidoc/reference/query-by-example.adoc +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -3,32 +3,32 @@ == Introduction -This chapter will give you an introduction to Query by Example and will explain how to use example specifications. +This chapter will give you an introduction to Query by Example and explain how to use example specifications. Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. == Usage -An `Example` takes a data object (usually the entity object or a subtype of it) and a specification how to match properties. You can use Query by Example in the `MongoTemplate` and within `Repositories`. +An `Example` takes a data object (usually the domain object object or a subtype of it) and a specification how to match properties. You can use Query by Example with the `MongoTemplate` and with Repositories. Query by Example is suited for several use-cases but also comes with limitations: **When to use** * Querying your data store with a set of static or dynamic constraints -* Frequent refactoring of the entities without worrying about breaking existing queries +* Frequent refactoring of the domain objects without worrying about breaking existing queries * Works independently from the data store API **Limitations** * Query predicates are combined using the `AND` keyword * No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` -* Limited to starts/contains/ends/regex matching for strings and exact matching for other property types +* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types -Before getting started with Query by Example you need to have your entities set up. +Before getting started with Query by Example you need to have your interface to the data store set up. -.Sample Person entity +.Sample Person object ==== [source,java] ---- @@ -45,9 +45,9 @@ public class Person { ---- ==== -We have a quite simple entity here that is mapped to the data store. You can use this entity to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. +This is a simple domain object. You can use it to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. -.The first Example specification +.Simple Example specification ==== [source,xml] ---- @@ -57,15 +57,15 @@ person.setFirstname("Dave"); <2> Example example = Example.exampleOf(person); <3> ---- -<1> Create a new instance of the entity +<1> Create a new instance of the domain object <2> Set the properties to query <3> Create an `Example` ==== -NOTE: Property names of the sample object must correlate with the property names of the queried entity. +NOTE: Property names of the sample object must correlate with the property names of the queried domain object. -.Query by Example using the MongoTemplate +.Query by Example using MongoTemplate ==== [source,xml] ---- @@ -78,20 +78,20 @@ public List findPeople(Person sampleObject) { ---- ==== -The `findByExample` method accepts either the sample object or an `Example` object to query the data store. Spring Data uses the `Example` to create and execute a query. +`MongoTemplate.findByExample` accepts either the sample object or an `Example` object to create and execute a query. -.Query by Example using the Repositories +.Query by Example using a Repository ==== [source, java] ---- public interface MongoRepository { - List findAllByExample(Example example); + List findAllByExample(Example example); - List findAllByExample(Example example, Sort sort); + List findAllByExample(Example example, Sort sort); - Page findAllByExample(Example example, Pageable pageable); + Page findAllByExample(Example example, Pageable pageable); // … more functionality omitted. } @@ -120,10 +120,10 @@ Example.newExampleOf(person) ---- ==== -Property specifier accepts property names and property paths separated by dots that are contained within the sample object. A `PropertySpecifier` allows setting string matching options, case-sensitivity, +Property specifier accepts property names (e.g. "firstname" and "lastname"). You can navigate by chaining properties together with dots ("address.city"). You can tune it with matching options and case sensitivity. [cols="1,2", options="header"] -.Supported string matching options of `StringMatcher` +.`StringMatcher` options |=== | Matching | Logical result From 9e18a5477d1a1873ae4dd9b8b2cbeb724de0fce3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 24 Feb 2016 09:03:34 +0100 Subject: [PATCH 09/14] DATAMONGO-1245 - Remove escaping for native regex string matching. --- .../mongodb/core/query/MongoRegexCreator.java | 7 ++- .../convert/MongoExampleMapperUnitTests.java | 46 +++++++++++++++++-- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java index 4f7b4f042a..a1d85dff99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ /** * @author Christoph Strobl + * @author Mark Paluch * @since 1.8 */ public enum MongoRegexCreator { @@ -67,6 +68,10 @@ public String toRegularExpression(String source, Type type) { private String prepareAndEscapeStringBeforeApplyingLikeRegex(String source, Type type) { + if (ObjectUtils.nullSafeEquals(Type.REGEX, type)) { + return source; + } + if (!ObjectUtils.nullSafeEquals(Type.LIKE, type)) { return PUNCTATION_PATTERN.matcher(source).find() ? Pattern.quote(source) : source; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index 956e6bc40b..9692078af9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -49,6 +49,7 @@ /** * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class MongoExampleMapperUnitTests { @@ -149,7 +150,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() * @see DATAMONGO-1245 */ @Test - public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatchMode() { + public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenientMatchMode() { WrapperDocument probe = new WrapperDocument(); probe.flatDoc = new FlatDocument(); @@ -164,7 +165,7 @@ public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenienMatch * @see DATAMONGO-1245 */ @Test - public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStriktMatchMode() { + public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStrictMatchMode() { WrapperDocument probe = new WrapperDocument(); probe.flatDoc = new FlatDocument(); @@ -196,6 +197,26 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarti .add("intValue", 100).get())); } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeContainingDotsWhenStringMatchModeIsStarting() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "fire.ight"; + probe.intValue = 100; + + Example example = newExampleOf(probe).matchStringsStartingWith().get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^" + Pattern.quote("fire.ight"))) + .add("intValue", 100).get())); + } + /** * @see DATAMONGO-1245 */ @@ -215,6 +236,25 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding .add("intValue", 100).get())); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeRegex() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.customNamedField = "^(cat|dog).*shelter\\d?"; + + Example example = newExampleOf(probe).matchStrings(StringMatcher.REGEX).get(); + + DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, + is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight")) + .add("custom_field_name", new BasicDBObject("$regex", "^(cat|dog).*shelter\\d?")).get())); + } + /** * @see DATAMONGO-1245 */ From 6270e86505e1983fb0044964081cd6b400f83fe4 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 25 Feb 2016 09:32:33 +0100 Subject: [PATCH 10/14] DATAMONGO-1245 - Adopt QBE API refactoring. Adopt changes from query by example API refactoring. Related ticket: DATACMNS-810. --- .../data/mongodb/core/MongoOperations.java | 32 ++++++ .../data/mongodb/core/MongoTemplate.java | 95 ++++++++++-------- .../core/convert/MongoExampleMapper.java | 74 +++++++------- .../mongodb/repository/MongoRepository.java | 51 ++++------ .../support/SimpleMongoRepository.java | 55 +++++++++-- .../convert/MongoExampleMapperUnitTests.java | 99 +++++++++---------- ...tractPersonRepositoryIntegrationTests.java | 7 +- .../support/SimpleMongoRepositoryTests.java | 79 ++++++++++++--- 8 files changed, 302 insertions(+), 190 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index dab3b0de9d..be72a7be89 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Set; +import org.springframework.data.domain.Example; import org.springframework.data.geo.GeoResults; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -56,6 +57,7 @@ * @author Chuong Ngo * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public interface MongoOperations { @@ -628,6 +630,36 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin */ T findById(Object id, Class entityClass, String collectionName); + /** + * Map the results of a Query-by-Example query onto the given {@code probe} class. The collection is determined from the {@code probe} type. + *

+ * The objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless + * configured otherwise, an instance of MappingMongoConverter will be used. + *

+ * The query is constructed from the {@code probe}. + * + * @param probe the probe to be used for the Example, must not be {@literal null}. + * @param + * @param + * @return the List of converted objects + */ + List findByExample(S probe); + + /** + * Map the results of a Query-by-Example query onto the given {@link Example#getProbeType()} class. The collection is determined from the {@link Example#getProbeType()}. + *

+ * The objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless + * configured otherwise, an instance of MappingMongoConverter will be used. + *

+ * The query is constructed from the {@link Example}. + * + * @param example the example, must not be {@literal null}. + * @param + * @param + * @return the List of converted objects + */ + List findByExample(Example example); + /** * Triggers findAndModify * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index f01a2beb42..7af96585db 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -126,7 +126,7 @@ /** * Primary implementation of {@link MongoOperations}. - * + * * @author Thomas Risberg * @author Graeme Rocher * @author Mark Pollack @@ -139,6 +139,7 @@ * @author Chuong Ngo * @author Christoph Strobl * @author Doménique Tilleuil + * @author Mark Paluch */ @SuppressWarnings("deprecation") public class MongoTemplate implements MongoOperations, ApplicationContextAware { @@ -175,7 +176,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { /** * Constructor used for a basic template configuration - * + * * @param mongo must not be {@literal null}. * @param databaseName must not be {@literal null} or empty. */ @@ -186,7 +187,7 @@ public MongoTemplate(Mongo mongo, String databaseName) { /** * Constructor used for a template configuration with user credentials in the form of * {@link org.springframework.data.authentication.UserCredentials} - * + * * @param mongo must not be {@literal null}. * @param databaseName must not be {@literal null} or empty. * @param userCredentials @@ -197,7 +198,7 @@ public MongoTemplate(Mongo mongo, String databaseName, UserCredentials userCrede /** * Constructor used for a basic template configuration. - * + * * @param mongoDbFactory must not be {@literal null}. */ public MongoTemplate(MongoDbFactory mongoDbFactory) { @@ -206,7 +207,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory) { /** * Constructor used for a basic template configuration. - * + * * @param mongoDbFactory must not be {@literal null}. * @param mongoConverter */ @@ -235,7 +236,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverte /** * Configures the {@link WriteResultChecking} to be used with the template. Setting {@literal null} will reset the * default of {@value #DEFAULT_WRITE_RESULT_CHECKING}. - * + * * @param resultChecking */ public void setWriteResultChecking(WriteResultChecking resultChecking) { @@ -246,7 +247,7 @@ public void setWriteResultChecking(WriteResultChecking resultChecking) { * Configures the {@link WriteConcern} to be used with the template. If none is configured the {@link WriteConcern} * configured on the {@link MongoDbFactory} will apply. If you configured a {@link Mongo} instance no * {@link WriteConcern} will be used. - * + * * @param writeConcern */ public void setWriteConcern(WriteConcern writeConcern) { @@ -255,7 +256,7 @@ public void setWriteConcern(WriteConcern writeConcern) { /** * Configures the {@link WriteConcernResolver} to be used with the template. - * + * * @param writeConcernResolver */ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { @@ -265,7 +266,7 @@ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { /** * Used by @{link {@link #prepareCollection(DBCollection)} to set the {@link ReadPreference} before any operations are * performed. - * + * * @param readPreference */ public void setReadPreference(ReadPreference readPreference) { @@ -292,7 +293,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext} * can be found we manually add the internally created one as {@link ApplicationListener} to make sure indexes get * created appropriately for entity types persisted through this {@link MongoTemplate} instance. - * + * * @param context must not be {@literal null}. */ private void prepareIndexCreator(ApplicationContext context) { @@ -313,14 +314,14 @@ private void prepareIndexCreator(ApplicationContext context) { /** * Returns the default {@link org.springframework.data.mongodb.core.core.convert.MongoConverter}. - * + * * @return */ public MongoConverter getConverter() { return this.mongoConverter; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#executeAsStream(org.springframework.data.mongodb.core.query.Query, java.lang.Class) */ @@ -415,7 +416,7 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan /** * Execute a MongoDB query and iterate over the query results on a per-document basis with a * {@link DocumentCallbackHandler} using the provided CursorPreparer. - * + * * @param query the query class that specifies the criteria used to find a record and also an optional fields * specification, must not be {@literal null}. * @param collectionName name of the collection to retrieve the objects from @@ -638,15 +639,23 @@ public T findById(Object id, Class entityClass, String collectionName) { return doFindOne(collectionName, new BasicDBObject(idKey, id), null, entityClass); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.MongoOperations#findByExample(java.lang.Object) + */ public List findByExample(S sample) { return findByExample(new Example(sample)); } + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.MongoOperations#findByExample(org.springframework.data.domain.Example) + */ @SuppressWarnings("unchecked") public List findByExample(Example sample) { Assert.notNull(sample, "Sample object must not be null!"); - return (List) find(new Query(new Criteria().alike(sample)), sample.getSampleType()); + return (List) find(new Query(new Criteria().alike(sample)), sample.getProbeType()); } public GeoResults geoNear(NearQuery near, Class entityClass) { @@ -691,7 +700,7 @@ public GeoResults geoNear(NearQuery near, Class entityClass, String co /* * As MongoDB currently (2.4.4) doesn't support the skipping of elements in near queries * we skip the elements ourselves to avoid at least the document 2 object mapping overhead. - * + * * @see https://jira.mongodb.org/browse/SERVER-3925 */ if (index >= elementsToSkip) { @@ -795,7 +804,7 @@ protected void ensureNotIterable(Object o) { /** * Prepare the collection before any processing is done using it. This allows a convenient way to apply settings like * slaveOk() etc. Can be overridden in sub-classes. - * + * * @param collection */ protected void prepareCollection(DBCollection collection) { @@ -809,7 +818,7 @@ protected void prepareCollection(DBCollection collection) { * settings in sub-classes.
* In case of using MongoDB Java driver version 3 the returned {@link WriteConcern} will be defaulted to * {@link WriteConcern#ACKNOWLEDGED} when {@link WriteResultChecking} is set to {@link WriteResultChecking#EXCEPTION}. - * + * * @param writeConcern any WriteConcern already configured or null * @return The prepared WriteConcern or null */ @@ -1210,7 +1219,7 @@ public WriteResult remove(Object object, String collection) { /** * Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s * property value as its {@link Entry#getValue()}. - * + * * @param object * @return */ @@ -1237,7 +1246,7 @@ private Entry extractIdPropertyAndValue(Object object) { /** * Returns a {@link Query} for the given entity by its id. - * + * * @param object must not be {@literal null}. * @return */ @@ -1249,7 +1258,7 @@ private Query getIdQueryFor(Object object) { /** * Returns a {@link Query} for the given entities by their ids. - * + * * @param objects must not be {@literal null} or {@literal empty}. * @return */ @@ -1519,7 +1528,7 @@ public List findAllAndRemove(Query query, Class entityClass, String co * Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)} * and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is * constructed out of the find result. - * + * * @param collectionName * @param query * @param entityClass @@ -1559,7 +1568,7 @@ protected AggregationResults aggregate(Aggregation aggregation, String co /** * Returns the potentially mapped results of the given {@commandResult} contained some. - * + * * @param outputType * @param commandResult * @return @@ -1671,7 +1680,7 @@ protected void maybeEmitEvent(MongoMappingEvent event) { /** * Create the specified collection using the provided options - * + * * @param collectionName * @param collectionOptions * @return the collection that was created @@ -1692,7 +1701,7 @@ public DBCollection doInDB(DB db) throws MongoException, DataAccessException { /** * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. * The query document is specified as a standard {@link DBObject} and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. @@ -1717,7 +1726,7 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields /** * Map the results of an ad-hoc query on the default MongoDB collection to a List using the template's converter. The * query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned @@ -1733,7 +1742,7 @@ protected List doFind(String collectionName, DBObject query, DBObject fie * Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified type. The object is * converted from the MongoDB native representation using an instance of {@see MongoConverter}. The query document is * specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. @@ -1786,7 +1795,7 @@ protected DBObject convertToDbObject(CollectionOptions collectionOptions) { * The first document that matches the query is returned and also removed from the collection in the database. *

* The query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param entityClass the parameterized type of the returned list. @@ -1832,7 +1841,7 @@ protected T doFindAndModify(String collectionName, DBObject query, DBObject /** * Populates the id property of the saved object, if it's not set already. - * + * * @param savedObject * @param id */ @@ -1882,7 +1891,7 @@ private DBCollection getAndPrepareCollection(DB db, String collectionName) { *

  • Execute the given {@link ConnectionCallback} for a {@link DBObject}.
  • *
  • Apply the given {@link DbObjectCallback} to each of the {@link DBObject}s to obtain the result.
  • *
      - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBObject} with * @param objectCallback the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type @@ -1911,7 +1920,7 @@ private T executeFindOneInternal(CollectionCallback collectionCall *
    1. Iterate over the {@link DBCursor} and applies the given {@link DbObjectCallback} to each of the * {@link DBObject}s collecting the actual result {@link List}.
    2. *
        - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBCursor} with * @param preparer the {@link CursorPreparer} to potentially modify the {@link DBCursor} before ireating over it @@ -2018,7 +2027,7 @@ String determineCollectionName(Class entityClass) { /** * Handles {@link WriteResult} errors based on the configured {@link WriteResultChecking}. - * + * * @param writeResult * @param query * @param operation @@ -2062,7 +2071,7 @@ protected void handleAnyWriteResultErrors(WriteResult writeResult, DBObject quer /** * Inspects the given {@link CommandResult} for erros and potentially throws an * {@link InvalidDataAccessApiUsageException} for that error. - * + * * @param result must not be {@literal null}. * @param source must not be {@literal null}. */ @@ -2100,7 +2109,7 @@ private DBObject getMappedSortObject(Query query, Class type) { /** * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original * exception if the conversation failed. Thus allows safe re-throwing of the return value. - * + * * @param ex the exception to translate * @param exceptionTranslator the {@link PersistenceExceptionTranslator} to be used for translation * @return @@ -2116,7 +2125,7 @@ private static RuntimeException potentiallyConvertRuntimeException(RuntimeExcept /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -2150,7 +2159,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -2181,7 +2190,7 @@ public DBCursor doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Thomas Risberg */ private static class FindAndRemoveCallback implements CollectionCallback { @@ -2226,7 +2235,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple internal callback to allow operations on a {@link DBObject}. - * + * * @author Oliver Gierke * @author Thomas Darimont */ @@ -2239,7 +2248,7 @@ static interface DbObjectCallback { /** * Simple {@link DbObjectCallback} that will transform {@link DBObject} into the given target type using the given * {@link MongoReader}. - * + * * @author Oliver Gierke * @author Christoph Strobl */ @@ -2358,7 +2367,7 @@ public DBCursor prepare(DBCursor cursor) { /** * {@link DbObjectCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to * a delegate and creates a {@link GeoResult} from the result. - * + * * @author Oliver Gierke */ static class GeoNearResultDbObjectCallback implements DbObjectCallback> { @@ -2369,7 +2378,7 @@ static class GeoNearResultDbObjectCallback implements DbObjectCallback delegate, Metric metric) { @@ -2391,7 +2400,7 @@ public GeoResult doWith(DBObject object) { /** * A {@link CloseableIterator} that is backed by a MongoDB {@link Cursor}. - * + * * @since 1.7 * @author Thomas Darimont */ @@ -2403,7 +2412,7 @@ static class CloseableIterableCursorAdapter implements CloseableIterator { /** * Creates a new {@link CloseableIterableCursorAdapter} backed by the given {@link Cursor}. - * + * * @param cursor * @param exceptionTranslator * @param objectReadCallback diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 3332e4f568..6052e32df2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ import java.util.regex.Pattern; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Example.NullHandler; -import org.springframework.data.domain.Example.StringMatcher; -import org.springframework.data.domain.PropertySpecifier; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; +import org.springframework.data.domain.ExampleSpecAccessor; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -39,8 +39,11 @@ import com.mongodb.BasicDBObject; import com.mongodb.DBObject; +import static org.springframework.data.domain.ExampleSpec.*; + /** * @author Christoph Strobl + * @author Mark Paluch * @since 1.8 */ public class MongoExampleMapper { @@ -57,19 +60,19 @@ public MongoExampleMapper(MongoConverter converter) { /** * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from * {@link Example#getProbe()}. - * + * * @param example * @return * @since 1.8 */ public DBObject getMappedExample(Example example) { - return getMappedExample(example, mappingContext.getPersistentEntity(example.getSampleType())); + return getMappedExample(example, mappingContext.getPersistentEntity(example.getProbeType())); } /** * Returns the given {@link Example} as {@link DBObject} holding matching values extracted from * {@link Example#getProbe()}. - * + * * @param example * @param entity * @return @@ -77,21 +80,23 @@ public DBObject getMappedExample(Example example) { */ public DBObject getMappedExample(Example example, MongoPersistentEntity entity) { - DBObject reference = (DBObject) converter.convertToMongoType(example.getSampleObject()); + DBObject reference = (DBObject) converter.convertToMongoType(example.getProbe()); - if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getSampleObject()).getIdentifier() == null) { + if (entity.hasIdProperty() && entity.getIdentifierAccessor(example.getProbe()).getIdentifier() == null) { reference.removeField(entity.getIdProperty().getFieldName()); } - applyPropertySpecs("", reference, example); + ExampleSpecAccessor exampleSpecAccessor = new ExampleSpecAccessor(example.getExampleSpec()); + + applyPropertySpecs("", reference, example.getProbeType(), exampleSpecAccessor); - return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, example.getNullHandler()) ? reference : new BasicDBObject( - SerializationUtils.flatMap(reference)); + return ObjectUtils.nullSafeEquals( NullHandler.INCLUDE, exampleSpecAccessor.getNullHandler()) ? reference + : new BasicDBObject(SerializationUtils.flatMap(reference)); } - private String getMappedPropertyPath(String path, Example example) { + private String getMappedPropertyPath(String path, Class probeType, ExampleSpecAccessor exampleSpecAccessor) { - MongoPersistentEntity entity = mappingContext.getPersistentEntity(example.getSampleType()); + MongoPersistentEntity entity = mappingContext.getPersistentEntity(probeType); Iterator parts = Arrays.asList(path.split("\\.")).iterator(); @@ -136,7 +141,8 @@ public void doWithPersistentProperty(MongoPersistentProperty property) { } - private void applyPropertySpecs(String path, DBObject source, Example example) { + private void applyPropertySpecs(String path, DBObject source, Class probeType, + ExampleSpecAccessor exampleSpecAccessor) { if (!(source instanceof BasicDBObject)) { return; @@ -155,39 +161,30 @@ private void applyPropertySpecs(String path, DBObject source, Example example String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); - String mappedPropertyPath = getMappedPropertyPath(propertyPath, example); - if (example.isIgnoredPath(propertyPath) || example.isIgnoredPath(mappedPropertyPath)) { + String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType, exampleSpecAccessor); + if (exampleSpecAccessor.isIgnoredPath(propertyPath) || exampleSpecAccessor.isIgnoredPath(mappedPropertyPath)) { iter.remove(); continue; } - PropertySpecifier specifier = null; - StringMatcher stringMatcher = example.getDefaultStringMatcher(); + StringMatcher stringMatcher = exampleSpecAccessor.getDefaultStringMatcher(); Object value = entry.getValue(); - boolean ignoreCase = example.isIngnoreCaseEnabled(); + boolean ignoreCase = exampleSpecAccessor.isIgnoreCaseEnabled(); - if (example.hasPropertySpecifiers()) { + if (exampleSpecAccessor.hasPropertySpecifiers()) { - mappedPropertyPath = example.hasPropertySpecifier(propertyPath) ? propertyPath : getMappedPropertyPath( - propertyPath, example); + mappedPropertyPath = exampleSpecAccessor.hasPropertySpecifier(propertyPath) ? propertyPath + : getMappedPropertyPath(propertyPath, probeType, exampleSpecAccessor); - specifier = example.getPropertySpecifier(mappedPropertyPath); - - if (specifier != null) { - if (specifier.hasStringMatcher()) { - stringMatcher = specifier.getStringMatcher(); - } - if (specifier.getIgnoreCase() != null) { - ignoreCase = specifier.getIgnoreCase(); - } - - } + stringMatcher = exampleSpecAccessor.getStringMatcherForPath(mappedPropertyPath); + ignoreCase = exampleSpecAccessor.isIgnoreCaseForPath(mappedPropertyPath); } // TODO: should a PropertySpecifier outrule the later on string matching? - if (specifier != null) { + if (exampleSpecAccessor.hasPropertySpecifier(mappedPropertyPath)) { - value = specifier.transformValue(value); + PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath); + value = valueTransformer.convert(value); if (value == null) { iter.remove(); continue; @@ -199,12 +196,13 @@ private void applyPropertySpecs(String path, DBObject source, Example example if (entry.getValue() instanceof String) { applyStringMatcher(entry, stringMatcher, ignoreCase); } else if (entry.getValue() instanceof BasicDBObject) { - applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), example); + applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), probeType, exampleSpecAccessor); } } } - private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, boolean ignoreCase) { + private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, + boolean ignoreCase) { BasicDBObject dbo = new BasicDBObject(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java index 35ec5f3765..3daf98d20a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java @@ -19,45 +19,49 @@ import java.util.List; import org.springframework.data.domain.Example; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.repository.NoRepositoryBean; import org.springframework.data.repository.PagingAndSortingRepository; +import org.springframework.data.repository.query.QueryByExampleExecutor; /** * Mongo specific {@link org.springframework.data.repository.Repository} interface. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ @NoRepositoryBean -public interface MongoRepository extends PagingAndSortingRepository { +public interface MongoRepository + extends PagingAndSortingRepository, QueryByExampleExecutor { /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ + @Override List save(Iterable entites); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ + @Override List findAll(); /* * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ + @Override List findAll(Sort sort); /** * Inserts the given a given entity. Assumes the instance to be new to be able to apply insertion optimizations. Use * the returned instance for further operations as the save operation might have changed the entity instance * completely. Prefer using {@link #save(Object)} instead to avoid the usage of store-specific API. - * + * * @param entity must not be {@literal null}. * @return the saved entity * @since 1.7 @@ -68,40 +72,25 @@ public interface MongoRepository extends PagingAndSo * Inserts the given entities. Assumes the given entities to have not been persisted yet and thus will optimize the * insert over a call to {@link #save(Iterable)}. Prefer using {@link #save(Iterable)} to avoid the usage of store * specific API. - * + * * @param entities must not be {@literal null}. * @return the saved entities * @since 1.7 */ List insert(Iterable entities); - /** - * Returns all instances of the type specified by the given {@link Example}. - * - * @param example must not be {@literal null}. - * @return - * @since 1.8 + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ - List findAllByExample(Example example); + @Override + List findAll(Example example); - /** - * Returns all instances of the type specified by the given {@link Example}. - * - * @param example must not be {@literal null}. - * @param sort can be {@literal null}. - * @return all entities sorted by the given options - * @since 1.8 + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ - List findAllByExample(Example example, Sort sort); + @Override + List findAll(Example example, Sort sort); - /** - * Returns a {@link Page} of entities meeting the paging restriction specified by the given {@link Example} limited to - * criteria provided in the {@code Pageable} object. - * - * @param example must not be {@literal null}. - * @param pageable can be {@literal null}. - * @return a {@link Page} of entities - * @since 1.8 - */ - Page findAllByExample(Example example, Pageable pageable); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index a87c184f51..68a401d3ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2014 the original author or authors. + * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,10 +40,11 @@ /** * Repository base implementation for Mongo. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class SimpleMongoRepository implements MongoRepository { @@ -52,9 +53,9 @@ public class SimpleMongoRepository implements MongoR /** * Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}. - * + * * @param metadata must not be {@literal null}. - * @param template must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. */ public SimpleMongoRepository(MongoEntityInformation metadata, MongoOperations mongoOperations) { @@ -116,7 +117,9 @@ public List save(Iterable entities) { * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) */ public T findOne(ID id) { + Assert.notNull(id, "The given id must not be null!"); + return mongoOperations.findById(id, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -135,6 +138,7 @@ private Criteria getIdCriteria(Object id) { public boolean exists(ID id) { Assert.notNull(id, "The given id must not be null!"); + return mongoOperations.exists(getIdQuery(id), entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -152,7 +156,9 @@ public long count() { * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) */ public void delete(ID id) { + Assert.notNull(id, "The given id must not be null!"); + mongoOperations.remove(getIdQuery(id), entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -161,7 +167,9 @@ public void delete(ID id) { * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) */ public void delete(T entity) { + Assert.notNull(entity, "The given entity must not be null!"); + delete(entityInformation.getId(entity)); } @@ -194,7 +202,7 @@ public List findAll() { return findAll(new Query()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ @@ -228,7 +236,7 @@ public List findAll(Sort sort) { return findAll(new Query().with(sort)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoRepository#insert(java.lang.Object) */ @@ -241,7 +249,7 @@ public S insert(S entity) { return entity; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoRepository#insert(java.lang.Iterable) */ @@ -265,7 +273,7 @@ public List insert(Iterable entities) { * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) */ @Override - public Page findAllByExample(Example example, Pageable pageable) { + public Page findAll(Example example, Pageable pageable) { Assert.notNull(example, "Sample must not be null!"); @@ -284,7 +292,7 @@ public Page findAllByExample(Example example, Pageable pagea * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ @Override - public List findAllByExample(Example example, Sort sort) { + public List findAll(Example example, Sort sort) { Assert.notNull(example, "Sample must not be null!"); @@ -302,7 +310,7 @@ public List findAllByExample(Example example, Sort sort) { * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example) */ @Override - public List findAllByExample(Example example) { + public List findAll(Example example) { Assert.notNull(example, "Sample must not be null!"); @@ -311,6 +319,33 @@ public List findAllByExample(Example example) { return findAll(q); } + @Override + public T findOne(Example example) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)); + return mongoOperations.findOne(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + } + + @Override + public long count(Example example) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)); + return mongoOperations.count(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + } + + @Override + public boolean exists(Example example) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)); + return mongoOperations.exists(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + } + private List findAll(Query query) { if (query == null) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index 9692078af9..c4ca1ef7d1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -18,7 +18,6 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; import static org.springframework.data.domain.Example.*; -import static org.springframework.data.domain.PropertySpecifier.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; import static org.springframework.data.mongodb.test.util.IsBsonObject.*; @@ -34,6 +33,9 @@ import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.GenericPropertyMatcher; +import org.springframework.data.domain.ExampleSpec.StringMatcher; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.convert.QueryMapperUnitTests.ClassWithGeoTypes; @@ -80,7 +82,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsSet() { FlatDocument probe = new FlatDocument(); probe.id = "steelheart"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("_id", "steelheart").get())); } @@ -96,11 +98,10 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenMultipleValuesSet() { probe.stringValue = "firefight"; probe.intValue = 100; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); - assertThat(dbo, - is(new BasicDBObjectBuilder().add("_id", "steelheart").add("stringValue", "firefight").add("intValue", 100) - .get())); + assertThat(dbo, is(new BasicDBObjectBuilder().add("_id", "steelheart").add("stringValue", "firefight") + .add("intValue", 100).get())); } /** @@ -113,7 +114,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIdIsNotSet() { probe.stringValue = "firefight"; probe.intValue = 100; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "firefight").add("intValue", 100).get())); } @@ -127,7 +128,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenListHasValues() { FlatDocument probe = new FlatDocument(); probe.listOfString = Arrays.asList("Prof", "Tia", "David"); - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("listOfString", Arrays.asList("Prof", "Tia", "David")).get())); } @@ -141,7 +142,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() FlatDocument probe = new FlatDocument(); probe.customNamedField = "Mitosis"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "Mitosis").get())); } @@ -156,7 +157,7 @@ public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenientMatc probe.flatDoc = new FlatDocument(); probe.flatDoc.stringValue = "conflux"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WrapperDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WrapperDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "conflux").get())); } @@ -171,7 +172,7 @@ public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStrictM probe.flatDoc = new FlatDocument(); probe.flatDoc.stringValue = "conflux"; - Example example = newExampleOf(probe).includeNullValues().get(); + Example example = ExampleSpec.of(WrapperDocument.class).withIncludeNullValues().createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -188,16 +189,15 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarti probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).matchStringsStartingWith().get(); + Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.STARTING) + .createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^firefight")) - .add("intValue", 100).get())); + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^firefight")) + .add("intValue", 100).get())); } - /** * @see DATAMONGO-1245 */ @@ -208,13 +208,12 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeContainingDotsWhenStringMat probe.stringValue = "fire.ight"; probe.intValue = 100; - Example example = newExampleOf(probe).matchStringsStartingWith().get(); + Example example = ExampleSpec.of(FlatDocument.class).withStringMatcherStarting().createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "^" + Pattern.quote("fire.ight"))) - .add("intValue", 100).get())); + assertThat(dbo, is(new BasicDBObjectBuilder() + .add("stringValue", new BasicDBObject("$regex", "^" + Pattern.quote("fire.ight"))).add("intValue", 100).get())); } /** @@ -227,13 +226,12 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).matchStringsEndingWith().get(); + Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.ENDING).createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight$")) - .add("intValue", 100).get())); + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight$")) + .add("intValue", 100).get())); } /** @@ -246,13 +244,12 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeRegex() probe.stringValue = "firefight"; probe.customNamedField = "^(cat|dog).*shelter\\d?"; - Example example = newExampleOf(probe).matchStrings(StringMatcher.REGEX).get(); + Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.REGEX).createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat(dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight")) - .add("custom_field_name", new BasicDBObject("$regex", "^(cat|dog).*shelter\\d?")).get())); + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", "firefight")) + .add("custom_field_name", new BasicDBObject("$regex", "^(cat|dog).*shelter\\d?")).get())); } /** @@ -265,12 +262,12 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMat probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).matchStringsEndingWith().matchStringsWithIgnoreCase().get(); + Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.ENDING).withIgnoreCase() + .createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat( - dbo, + assertThat(dbo, is(new BasicDBObjectBuilder() .add("stringValue", new BasicDBObjectBuilder().add("$regex", "firefight$").add("$options", "i").get()) .add("intValue", 100).get())); @@ -286,12 +283,11 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { probe.stringValue = "firefight"; probe.intValue = 100; - Example example = newExampleOf(probe).matchStringsWithIgnoreCase().get(); + Example example = ExampleSpec.of(FlatDocument.class).withIgnoreCase().createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat( - dbo, + assertThat(dbo, is(new BasicDBObjectBuilder() .add("stringValue", new BasicDBObjectBuilder().add("$regex", Pattern.quote("firefight")).add("$options", "i").get()) @@ -309,7 +305,7 @@ public void exampleShouldBeMappedWhenContainingDBRef() { probe.referenceDocument = new ReferenceDocument(); probe.referenceDocument.id = "200"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WithDBRef.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WithDBRef.class)); com.mongodb.DBRef reference = getTypedValue(dbo, "referenceDocument", com.mongodb.DBRef.class); assertThat(reference.getId(), Is. is("200")); @@ -325,7 +321,7 @@ public void exampleShouldBeMappedWhenDBRefIsNull() { FlatDocument probe = new FlatDocument(); probe.stringValue = "steelheart"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", "steelheart").get())); } @@ -339,7 +335,7 @@ public void exampleShouldBeMappedCorrectlyWhenContainingLegacyPoint() { ClassWithGeoTypes probe = new ClassWithGeoTypes(); probe.legacyPoint = new Point(10D, 20D); - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(WithDBRef.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WithDBRef.class)); assertThat(dbo.get("legacyPoint.x"), Is. is(10D)); assertThat(dbo.get("legacyPoint.y"), Is. is(20D)); @@ -356,7 +352,7 @@ public void mappingShouldExcludeFieldWithCustomNameCorrectly() { probe.intValue = 10; probe.stringValue = "string"; - Example example = newExampleOf(probe).ignore("customNamedField").get(); + Example example = ExampleSpec.of(FlatDocument.class).withIgnorePaths("customNamedField").createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -374,7 +370,7 @@ public void mappingShouldExcludeFieldCorrectly() { probe.intValue = 10; probe.stringValue = "string"; - Example example = newExampleOf(probe).ignore("stringValue").get(); + Example example = ExampleSpec.of(FlatDocument.class).withIgnorePaths("stringValue").createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -393,12 +389,12 @@ public void mappingShouldExcludeNestedFieldCorrectly() { probe.flatDoc.intValue = 10; probe.flatDoc.stringValue = "string"; - Example example = newExampleOf(probe).ignore("flatDoc.stringValue").get(); + Example example = ExampleSpec.of(WrapperDocument.class).withIgnorePaths("flatDoc.stringValue").createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); - assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.custom_field_name", "foo").add("flatDoc.intValue", 10) - .get())); + assertThat(dbo, + is(new BasicDBObjectBuilder().add("flatDoc.custom_field_name", "foo").add("flatDoc.intValue", 10).get())); } /** @@ -413,12 +409,13 @@ public void mappingShouldExcludeNestedFieldWithCustomNameCorrectly() { probe.flatDoc.intValue = 10; probe.flatDoc.stringValue = "string"; - Example example = newExampleOf(probe).ignore("flatDoc.customNamedField").get(); + Example example = ExampleSpec.of(WrapperDocument.class).withIgnorePaths("flatDoc.customNamedField") + .createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); - assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "string").add("flatDoc.intValue", 10) - .get())); + assertThat(dbo, + is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "string").add("flatDoc.intValue", 10).get())); } /** @@ -431,15 +428,13 @@ public void mappingShouldFavorFieldSpecificationStringMatcherOverDefaultStringMa probe.stringValue = "firefight"; probe.customNamedField = "steelheart"; - Example example = newExampleOf(probe).specify(newPropertySpecifier("stringValue").matchStringContaining().get()) - .get(); + Example example = ExampleSpec.of(FlatDocument.class) + .withMatcher("stringValue", new GenericPropertyMatcher().contains()).createExample(probe); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); - assertThat( - dbo, - is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", ".*firefight.*")) - .add("custom_field_name", "steelheart").get())); + assertThat(dbo, is(new BasicDBObjectBuilder().add("stringValue", new BasicDBObject("$regex", ".*firefight.*")) + .add("custom_field_name", "steelheart").get())); } /** @@ -453,7 +448,7 @@ public void mappingShouldIncludePropertiesFromHierarchicalDocument() { probe.customNamedField = "steelheart"; probe.anotherStringValue = "calamity"; - DBObject dbo = mapper.getMappedExample(exampleOf(probe), context.getPersistentEntity(FlatDocument.class)); + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); assertThat(dbo, isBsonObject().containing("anotherStringValue", "calamity")); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 0f716fb6c5..3bae8c4ce1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -66,6 +66,7 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) public abstract class AbstractPersonRepositoryIntegrationTests { @@ -1240,7 +1241,7 @@ public void findByExampleShouldResolveStuffCorrectly() { ReflectionTestUtils.setField(sample, "createdAt", null); ReflectionTestUtils.setField(sample, "email", null); - Page result = repository.findAllByExample(new Example(sample), new PageRequest(0, 10)); + Page result = repository.findAll(new Example(sample), new PageRequest(0, 10)); Assert.assertThat(result.getNumberOfElements(), Is.is(2)); } @@ -1258,7 +1259,7 @@ public void findAllByExampleShouldResolveStuffCorrectly() { ReflectionTestUtils.setField(sample, "createdAt", null); ReflectionTestUtils.setField(sample, "email", null); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); Assert.assertThat(result.size(), Is.is(2)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 6b75c51edc..7982bd3d0a 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2015 the original author or authors. + * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,8 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.geo.Point; @@ -51,6 +53,7 @@ * @author A. B. M. Kowser * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -130,7 +133,7 @@ public void shouldInsertSingle() { * @see DATAMONGO-1054 */ @Test - public void shouldInsertMutlipleFromList() { + public void shouldInsertMultipleFromList() { String randomId = UUID.randomUUID().toString(); Map idToPerson = new HashMap(); @@ -180,7 +183,7 @@ public void findByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - Page result = repository.findAllByExample(new Example(sample), new PageRequest(0, 10)); + Page result = repository.findAll(new Example(sample), new PageRequest(0, 10)); assertThat(result.getContent(), hasItems(dave, oliver)); assertThat(result.getContent(), hasSize(2)); @@ -196,7 +199,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); @@ -218,7 +221,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObject() sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); @@ -240,7 +243,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingPartialNestedOb sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); @@ -259,7 +262,8 @@ public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInSt sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get()); + Example example = ExampleSpec.of(Person.class).withIncludeNullValues().createExample(sample); + List result = repository.findAll(example); assertThat(result, empty()); } @@ -277,7 +281,8 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInS sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample).includeNullValues().get()); + Example example = ExampleSpec.of(Person.class).withIncludeNullValues().createExample(sample); + List result = repository.findAll(example); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); @@ -293,7 +298,8 @@ public void findAllByExampleShouldRespectStringMatchMode() { sample.setLastname("Mat"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(Example.newExampleOf(sample).matchStringsStartingWith().get()); + Example example = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.STARTING).createExample(sample); + List result = repository.findAll(example); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); @@ -319,7 +325,7 @@ public void findAllByExampleShouldResolveDbRefCorrectly() { sample.setCreator(user); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -340,7 +346,7 @@ public void findAllByExampleShouldResolveLegacyCoordinatesCorrectly() { sample.setLocation(megan.getLocation()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -361,7 +367,7 @@ public void findAllByExampleShouldResolveGeoJsonCoordinatesCorrectly() { sample.setLocation(megan.getLocation()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -377,12 +383,59 @@ public void findAllByExampleShouldProcessInheritanceCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAllByExample(new Example(sample)); + List result = repository.findAll(new Example(sample)); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void findOneByExampleShouldLookUpEntriesCorrectly() { + + Person sample = new Person(); + sample.setFirstname("Dave"); + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + Person result = repository.findOne(new Example(sample)); + + assertThat(result, is(equalTo(dave))); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void existsByExampleShouldLookUpEntriesCorrectly() { + + Person sample = new Person(); + sample.setFirstname("Dave"); + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + boolean result = repository.exists(new Example(sample)); + + assertThat(result, is(true)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void countByExampleShouldLookUpEntriesCorrectly() { + + Person sample = new Person(); + sample.setLastname("Matthews"); + trimDomainType(sample, "id", "createdAt", "email"); + + long result = repository.count(new Example(sample)); + + assertThat(result, is(equalTo(2L))); + } + @Document(collection = "customizedPerson") static class PersonExtended extends Person { From 1692268f8774dd810a558a723cabafa78a500883 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 26 Feb 2016 15:04:09 +0100 Subject: [PATCH 11/14] DATAMONGO-1245 - Documentation for Query-by-Example. --- src/main/asciidoc/index.adoc | 1 + .../asciidoc/reference/query-by-example.adoc | 115 +++--------------- 2 files changed, 19 insertions(+), 97 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 05053485a9..42163f8296 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -27,6 +27,7 @@ include::{spring-data-commons-docs}/repositories.adoc[] include::reference/introduction.adoc[] include::reference/mongodb.adoc[] include::reference/mongo-repositories.adoc[] +include::{spring-data-commons-docs}/query-by-example.adoc[] include::reference/query-by-example.adoc[] include::{spring-data-commons-docs}/auditing.adoc[] include::reference/mongo-auditing.adoc[] diff --git a/src/main/asciidoc/reference/query-by-example.adoc b/src/main/asciidoc/reference/query-by-example.adoc index 63f3cebf78..0a9abee098 100644 --- a/src/main/asciidoc/reference/query-by-example.adoc +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -1,126 +1,47 @@ -[[query.by.example]] -= Query by Example +[[query.by.example.execution]] +== Executing Query by Example -== Introduction - -This chapter will give you an introduction to Query by Example and explain how to use example specifications. - -Query by Example (QBE) is a user-friendly querying technique with a simple interface. It allows dynamic query creation and does not require to write queries containing field names. In fact, Query by Example does not require to write queries using store-specific query languages at all. - -== Usage - -An `Example` takes a data object (usually the domain object object or a subtype of it) and a specification how to match properties. You can use Query by Example with the `MongoTemplate` and with Repositories. - -Query by Example is suited for several use-cases but also comes with limitations: - -**When to use** - -* Querying your data store with a set of static or dynamic constraints -* Frequent refactoring of the domain objects without worrying about breaking existing queries -* Works independently from the data store API - -**Limitations** - -* Query predicates are combined using the `AND` keyword -* No support for nested/grouped property constraints like `firstname = ?0 or (firstname = ?1 and lastname = ?2)` -* Only supports starts/contains/ends/regex matching for strings and exact matching for other property types - - -Before getting started with Query by Example you need to have your interface to the data store set up. - -.Sample Person object -==== -[source,java] ----- -public class Person { - - @Id - private String id; - private String firstname; - private String lastname; - private Address address; - - // … getters and setters omitted -} ----- -==== - -This is a simple domain object. You can use it to create an Example specification. By default, fields having `null` values are ignored, and strings are matched using the store specific defaults. Examples can be built by either using the `exampleOf` factory method or by using the <>. Once the `Example` is constructed it becomes immutable. - -.Simple Example specification -==== -[source,xml] ----- -Person person = new Person(); <1> - -person.setFirstname("Dave"); <2> - -Example example = Example.exampleOf(person); <3> ----- -<1> Create a new instance of the domain object -<2> Set the properties to query -<3> Create an `Example` -==== - - -NOTE: Property names of the sample object must correlate with the property names of the queried domain object. +In Spring Data MongoDB you can use Query by Example with the `MongoTemplate` and with Repositories. .Query by Example using MongoTemplate ==== [source,xml] ---- -@Autowired -MongoTemplate template; +public class PersonService { + @Autowired MongoTemplate template; -public List findPeople(Person sampleObject) { - return template.findByExample(Example.exampleOf(person)); + public List findPeople(Person probe) { + return template.findByExample(Example.of(probe)); + } } ---- ==== -`MongoTemplate.findByExample` accepts either the sample object or an `Example` object to create and execute a query. +`MongoTemplate.findByExample` accepts either the probe or an `Example` object to create and execute a query. .Query by Example using a Repository ==== [source, java] ---- -public interface MongoRepository { +public interface PersonRepository extends MongoRepository { - List findAllByExample(Example example); +} - List findAllByExample(Example example, Sort sort); +public class PersonService { - Page findAllByExample(Example example, Pageable pageable); + @Autowired PersonRepository personRepository; - // … more functionality omitted. + public List findPeople(Person probe) { + return personRepository.findAll(Example.of(probe)); + } } ---- ==== -[[query.by.example.builder]] -== Example builder - -Examples are not limited to default settings. You can specify own defaults for string matching, null handling and property-specific settings using the example builder. - -.Query by Example builder -==== -[source, java] ----- -Example.newExampleOf(person) - .withStringMatcher(StringMatcher.ENDING) - .includeNullValues() - .withPropertySpecifier( - newPropertySpecifier("firstname").matchString(StringMatcher.CONTAINING).get()) - .withPropertySpecifier( - newPropertySpecifier("lastname").matchStringsWithIgnoreCase().get()) - .withPropertySpecifier( - newPropertySpecifier("address.city").matchStringStartingWith().get()) - .get(); ----- -==== +NOTE: When including `null` values in the `ExampleSpec` Spring Data Mongo uses embedded document matching instead of dot notation property matching. This forces exact document matching for all property values and the property order in the embedded document. -Property specifier accepts property names (e.g. "firstname" and "lastname"). You can navigate by chaining properties together with dots ("address.city"). You can tune it with matching options and case sensitivity. +Spring Data MongoDB provides support for the following matching options: [cols="1,2", options="header"] .`StringMatcher` options From 4223a0c6c9c57e1e56ed017b2dec6cd06ea58a3a Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 10:20:42 +0100 Subject: [PATCH 12/14] DATAMONGO-1245 - Extend Query by Example API to typed and untyped specs. --- .../data/mongodb/core/MongoTemplate.java | 4 +- .../core/convert/MongoExampleMapper.java | 40 +++++++++---- .../data/mongodb/core/query/Criteria.java | 2 +- .../mongodb/repository/MongoRepository.java | 10 ++-- .../support/SimpleMongoRepository.java | 58 +++++++++++++------ .../convert/MongoExampleMapperUnitTests.java | 47 ++++++++++----- .../core/temp/QueryByExampleTests.java | 8 ++- ...tractPersonRepositoryIntegrationTests.java | 13 +++-- .../ContactRepositoryIntegrationTests.java | 45 +++++++++++++- .../support/SimpleMongoRepositoryTests.java | 32 +++++----- 10 files changed, 179 insertions(+), 80 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 7af96585db..2be3753c43 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -644,7 +644,7 @@ public T findById(Object id, Class entityClass, String collectionName) { * @see org.springframework.data.mongodb.core.MongoOperations#findByExample(java.lang.Object) */ public List findByExample(S sample) { - return findByExample(new Example(sample)); + return findByExample(Example.of(sample)); } /* @@ -655,7 +655,7 @@ public List findByExample(S sample) { public List findByExample(Example sample) { Assert.notNull(sample, "Sample object must not be null!"); - return (List) find(new Query(new Criteria().alike(sample)), sample.getProbeType()); + return (List) find(new Query(new Criteria().alike(sample)), sample.getResultType()); } public GeoResults geoNear(NearQuery near, Class entityClass) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 6052e32df2..f4a4379e44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -17,30 +17,33 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleSpec; +import org.springframework.data.domain.ExampleSpec.NullHandler; import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; -import org.springframework.data.domain.ExampleSpecAccessor; +import org.springframework.data.domain.ExampleSpec.StringMatcher; import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.MongoRegexCreator; import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.data.repository.core.support.ExampleSpecAccessor; +import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; -import static org.springframework.data.domain.ExampleSpec.*; - /** * @author Christoph Strobl * @author Mark Paluch @@ -50,11 +53,18 @@ public class MongoExampleMapper { private final MappingContext, MongoPersistentProperty> mappingContext; private final MongoConverter converter; + private final Map stringMatcherPartMapping = new HashMap(); public MongoExampleMapper(MongoConverter converter) { this.converter = converter; this.mappingContext = converter.getMappingContext(); + + stringMatcherPartMapping.put(StringMatcher.EXACT, Type.SIMPLE_PROPERTY); + stringMatcherPartMapping.put(StringMatcher.CONTAINING, Type.CONTAINING); + stringMatcherPartMapping.put(StringMatcher.STARTING, Type.STARTING_WITH); + stringMatcherPartMapping.put(StringMatcher.ENDING, Type.ENDING_WITH); + stringMatcherPartMapping.put(StringMatcher.REGEX, Type.REGEX); } /** @@ -90,11 +100,17 @@ public DBObject getMappedExample(Example example, MongoPersistentEntity en applyPropertySpecs("", reference, example.getProbeType(), exampleSpecAccessor); - return ObjectUtils.nullSafeEquals( NullHandler.INCLUDE, exampleSpecAccessor.getNullHandler()) ? reference + if (exampleSpecAccessor.isTyped()) { + Set> restrictedTypes = new HashSet>(); + restrictedTypes.add(example.getResultType()); + this.converter.getTypeMapper().writeTypeRestrictions(reference, restrictedTypes); + } + + return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, exampleSpecAccessor.getNullHandler()) ? reference : new BasicDBObject(SerializationUtils.flatMap(reference)); } - private String getMappedPropertyPath(String path, Class probeType, ExampleSpecAccessor exampleSpecAccessor) { + private String getMappedPropertyPath(String path, Class probeType) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(probeType); @@ -161,7 +177,7 @@ private void applyPropertySpecs(String path, DBObject source, Class probeType String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); - String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType, exampleSpecAccessor); + String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); if (exampleSpecAccessor.isIgnoredPath(propertyPath) || exampleSpecAccessor.isIgnoredPath(mappedPropertyPath)) { iter.remove(); continue; @@ -174,7 +190,7 @@ private void applyPropertySpecs(String path, DBObject source, Class probeType if (exampleSpecAccessor.hasPropertySpecifiers()) { mappedPropertyPath = exampleSpecAccessor.hasPropertySpecifier(propertyPath) ? propertyPath - : getMappedPropertyPath(propertyPath, probeType, exampleSpecAccessor); + : getMappedPropertyPath(propertyPath, probeType); stringMatcher = exampleSpecAccessor.getStringMatcherForPath(mappedPropertyPath); ignoreCase = exampleSpecAccessor.isIgnoreCaseForPath(mappedPropertyPath); @@ -201,8 +217,7 @@ private void applyPropertySpecs(String path, DBObject source, Class probeType } } - private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, - boolean ignoreCase) { + private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, boolean ignoreCase) { BasicDBObject dbo = new BasicDBObject(); @@ -214,8 +229,8 @@ private void applyStringMatcher(Map.Entry entry, StringMatcher s } } else { - String expression = MongoRegexCreator.INSTANCE.toRegularExpression((String) entry.getValue(), - stringMatcher.getPartType()); + Type type = stringMatcherPartMapping.get(stringMatcher); + String expression = MongoRegexCreator.INSTANCE.toRegularExpression((String) entry.getValue(), type); dbo.put("$regex", expression); entry.setValue(dbo); } @@ -224,4 +239,5 @@ private void applyStringMatcher(Map.Entry entry, StringMatcher s dbo.put("$options", "i"); } } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 1bbf306a82..0836cdae9d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -98,7 +98,7 @@ public static Criteria where(String key) { * @since 1.8 */ public static Criteria byExample(Object example) { - return byExample(new Example(example)); + return byExample(Example.of(example)); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java index 3daf98d20a..cf3b11db96 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java @@ -79,18 +79,16 @@ public interface MongoRepository */ List insert(Iterable entities); - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ @Override - List findAll(Example example); + List findAll(Example example); - /* - * (non-Javadoc) + /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ @Override - List findAll(Example example, Sort sort); + List findAll(Example example, Sort sort); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 68a401d3ab..c4cfc0db3e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -30,6 +30,7 @@ import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.TypedExampleSpec; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.query.Criteria; @@ -70,6 +71,7 @@ public SimpleMongoRepository(MongoEntityInformation metadata, MongoOperat * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) */ + @Override public S save(S entity) { Assert.notNull(entity, "Entity must not be null!"); @@ -87,6 +89,7 @@ public S save(S entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ + @Override public List save(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities not be null!"); @@ -116,6 +119,7 @@ public List save(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) */ + @Override public T findOne(ID id) { Assert.notNull(id, "The given id must not be null!"); @@ -135,6 +139,7 @@ private Criteria getIdCriteria(Object id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) */ + @Override public boolean exists(ID id) { Assert.notNull(id, "The given id must not be null!"); @@ -147,6 +152,7 @@ public boolean exists(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#count() */ + @Override public long count() { return mongoOperations.getCollection(entityInformation.getCollectionName()).count(); } @@ -155,6 +161,7 @@ public long count() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) */ + @Override public void delete(ID id) { Assert.notNull(id, "The given id must not be null!"); @@ -166,6 +173,7 @@ public void delete(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) */ + @Override public void delete(T entity) { Assert.notNull(entity, "The given entity must not be null!"); @@ -177,6 +185,7 @@ public void delete(T entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) */ + @Override public void delete(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities not be null!"); @@ -190,6 +199,7 @@ public void delete(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#deleteAll() */ + @Override public void deleteAll() { mongoOperations.remove(new Query(), entityInformation.getCollectionName()); } @@ -198,6 +208,7 @@ public void deleteAll() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ + @Override public List findAll() { return findAll(new Query()); } @@ -206,6 +217,7 @@ public List findAll() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ + @Override public Iterable findAll(Iterable ids) { Set parameters = new HashSet(tryDetermineRealSizeOrReturn(ids, 10)); @@ -220,6 +232,7 @@ public Iterable findAll(Iterable ids) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Pageable) */ + @Override public Page findAll(final Pageable pageable) { Long count = count(); @@ -232,6 +245,7 @@ public Page findAll(final Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ + @Override public List findAll(Sort sort) { return findAll(new Query().with(sort)); } @@ -273,18 +287,18 @@ public List insert(Iterable entities) { * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Pageable) */ @Override - public Page findAll(Example example, Pageable pageable) { + public Page findAll(Example example, Pageable pageable) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)).with(pageable); - long count = mongoOperations.count(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + long count = mongoOperations.count(q, getResultType(example), getCollectionName(example)); if (count == 0) { - return new PageImpl(Collections. emptyList()); + return new PageImpl(Collections. emptyList()); } - return new PageImpl(mongoOperations.find(q, entityInformation.getJavaType(), - entityInformation.getCollectionName()), pageable, count); + return new PageImpl(mongoOperations.find(q, getResultType(example), getCollectionName(example)), + pageable, count); } /* @@ -292,7 +306,7 @@ public Page findAll(Example example, Pageable pageable) { * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ @Override - public List findAll(Example example, Sort sort) { + public List findAll(Example example, Sort sort) { Assert.notNull(example, "Sample must not be null!"); @@ -302,7 +316,7 @@ public List findAll(Example example, Sort sort) { q.with(sort); } - return findAll(q); + return mongoOperations.find(q, getResultType(example), getCollectionName(example)); } /* @@ -310,22 +324,17 @@ public List findAll(Example example, Sort sort) { * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example) */ @Override - public List findAll(Example example) { - - Assert.notNull(example, "Sample must not be null!"); - - Query q = new Query(new Criteria().alike(example)); - - return findAll(q); + public List findAll(Example example) { + return findAll(example, (Sort) null); } @Override - public T findOne(Example example) { + public S findOne(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.findOne(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + return mongoOperations.findOne(q, getResultType(example), getCollectionName(example)); } @Override @@ -334,7 +343,7 @@ public long count(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.count(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + return mongoOperations.count(q, getResultType(example), getCollectionName(example)); } @Override @@ -343,7 +352,20 @@ public boolean exists(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.exists(q, entityInformation.getJavaType(), entityInformation.getCollectionName()); + return mongoOperations.exists(q, getResultType(example), getCollectionName(example)); + } + + private Class getResultType(Example example) { + + if(example.getExampleSpec() instanceof TypedExampleSpec){ + return example.getResultType(); + } + + return (Class) entityInformation.getJavaType(); + } + + private String getCollectionName(Example example) { + return entityInformation.getCollectionName(); } private List findAll(Query query) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java index c4ca1ef7d1..ba01bacaa0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -147,6 +147,23 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenFieldNameIsCustomized() assertThat(dbo, is(new BasicDBObjectBuilder().add("custom_field_name", "Mitosis").get())); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void typedExampleShouldContainTypeRestriction() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.stringValue = "conflux"; + + DBObject dbo = mapper.getMappedExample(of(probe, ExampleSpec.typed(WrapperDocument.class)), + context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, + isBsonObject().containing("_class", new BasicDBObject("$in", new String[] { probe.getClass().getName() }))); + } + /** * @see DATAMONGO-1245 */ @@ -172,7 +189,7 @@ public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStrictM probe.flatDoc = new FlatDocument(); probe.flatDoc.stringValue = "conflux"; - Example example = ExampleSpec.of(WrapperDocument.class).withIncludeNullValues().createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIncludeNullValues()); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -189,8 +206,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsStarti probe.stringValue = "firefight"; probe.intValue = 100; - Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.STARTING) - .createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING)); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -208,7 +224,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeContainingDotsWhenStringMat probe.stringValue = "fire.ight"; probe.intValue = 100; - Example example = ExampleSpec.of(FlatDocument.class).withStringMatcherStarting().createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcherStarting()); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -226,7 +242,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding probe.stringValue = "firefight"; probe.intValue = 100; - Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.ENDING).createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING)); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -244,7 +260,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeRegex() probe.stringValue = "firefight"; probe.customNamedField = "^(cat|dog).*shelter\\d?"; - Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.REGEX).createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.REGEX)); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -262,8 +278,8 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMat probe.stringValue = "firefight"; probe.intValue = 100; - Example example = ExampleSpec.of(FlatDocument.class).withStringMatcher(StringMatcher.ENDING).withIgnoreCase() - .createExample(probe); + Example example = Example.of(probe, + ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING).withIgnoreCase()); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -283,7 +299,7 @@ public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabled() { probe.stringValue = "firefight"; probe.intValue = 100; - Example example = ExampleSpec.of(FlatDocument.class).withIgnoreCase().createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIgnoreCase()); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -352,7 +368,7 @@ public void mappingShouldExcludeFieldWithCustomNameCorrectly() { probe.intValue = 10; probe.stringValue = "string"; - Example example = ExampleSpec.of(FlatDocument.class).withIgnorePaths("customNamedField").createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("customNamedField")); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -370,7 +386,7 @@ public void mappingShouldExcludeFieldCorrectly() { probe.intValue = 10; probe.stringValue = "string"; - Example example = ExampleSpec.of(FlatDocument.class).withIgnorePaths("stringValue").createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("stringValue")); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); @@ -389,7 +405,7 @@ public void mappingShouldExcludeNestedFieldCorrectly() { probe.flatDoc.intValue = 10; probe.flatDoc.stringValue = "string"; - Example example = ExampleSpec.of(WrapperDocument.class).withIgnorePaths("flatDoc.stringValue").createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("flatDoc.stringValue")); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -409,8 +425,7 @@ public void mappingShouldExcludeNestedFieldWithCustomNameCorrectly() { probe.flatDoc.intValue = 10; probe.flatDoc.stringValue = "string"; - Example example = ExampleSpec.of(WrapperDocument.class).withIgnorePaths("flatDoc.customNamedField") - .createExample(probe); + Example example = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("flatDoc.customNamedField")); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(WrapperDocument.class)); @@ -428,8 +443,8 @@ public void mappingShouldFavorFieldSpecificationStringMatcherOverDefaultStringMa probe.stringValue = "firefight"; probe.customNamedField = "steelheart"; - Example example = ExampleSpec.of(FlatDocument.class) - .withMatcher("stringValue", new GenericPropertyMatcher().contains()).createExample(probe); + Example example = Example.of(probe, + ExampleSpec.untyped().withMatcher("stringValue", new GenericPropertyMatcher().contains())); DBObject dbo = mapper.getMappedExample(example, context.getPersistentEntity(FlatDocument.class)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java index 677ec3bb1c..c289ed57b6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,6 +32,10 @@ import com.mongodb.MongoClient; +/** + * @author Christoph Strobl + * @author Mark Paluch + */ public class QueryByExampleTests { MongoTemplate template; @@ -133,7 +137,7 @@ public void findByExampleWithCriteria() { Person sample = new Person(); sample.lastname = "stark"; - Query query = new Query(new Criteria().alike(new Example(sample)).and("firstname").regex("^ary*")); + Query query = new Query(new Criteria().alike(Example.of(sample)).and("firstname").regex("^ary*")); List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(1)); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 3bae8c4ce1..10a4e39219 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -37,6 +37,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleSpec; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; @@ -62,7 +63,7 @@ /** * Base class for tests for {@link PersonRepository}. - * + * * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl @@ -997,7 +998,7 @@ public void gettingNonFirstPageWorksWithoutLimitBeingSet() { /** * Ignored for now as this requires Querydsl 3.4.1 to succeed. - * + * * @see DATAMONGO-972 */ @Test @@ -1241,8 +1242,8 @@ public void findByExampleShouldResolveStuffCorrectly() { ReflectionTestUtils.setField(sample, "createdAt", null); ReflectionTestUtils.setField(sample, "email", null); - Page result = repository.findAll(new Example(sample), new PageRequest(0, 10)); - Assert.assertThat(result.getNumberOfElements(), Is.is(2)); + Page result = repository.findAll(Example.of(sample), new PageRequest(0, 10)); + assertThat(result.getNumberOfElements(), is(2)); } /** @@ -1259,8 +1260,8 @@ public void findAllByExampleShouldResolveStuffCorrectly() { ReflectionTestUtils.setField(sample, "createdAt", null); ReflectionTestUtils.setField(sample, "email", null); - List result = repository.findAll(new Example(sample)); - Assert.assertThat(result.size(), Is.is(2)); + List result = repository.findAll(Example.of(sample)); + assertThat(result.size(), is(2)); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java index d71e09a3f5..69724fb89b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2011 the original author or authors. + * Copyright 2010-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,11 +15,15 @@ */ package org.springframework.data.mongodb.repository; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleSpec; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @@ -27,6 +31,7 @@ * Integration tests for {@link ContactRepository}. Mostly related to mapping inheritance. * * @author Oliver Gierke + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("config/MongoNamespaceIntegrationTests-context.xml") @@ -35,6 +40,11 @@ public class ContactRepositoryIntegrationTests { @Autowired ContactRepository repository; + @Before + public void setUp() throws Exception { + repository.deleteAll(); + } + @Test public void readsAndWritesContactCorrectly() { @@ -43,4 +53,37 @@ public void readsAndWritesContactCorrectly() { assertTrue(repository.findOne(result.getId().toString()) instanceof Person); } + + @Test + public void findsContactByUntypedExample() { + + Person person = new Person("Oliver", "Gierke"); + Contact result = repository.save(person); + + Example example = Example.of(person, ExampleSpec.untyped()); + + assertThat(repository.findOne(example), instanceOf(Person.class)); + } + + @Test + public void findsContactByTypedExample() { + + Person person = new Person("Oliver", "Gierke"); + Contact result = repository.save(person); + + Example example = Example.of(person, ExampleSpec.typed(Person.class)); + + assertThat(repository.findOne(example), instanceOf(Person.class)); + } + + @Test + public void findsNoContactByExample() { + + Person person = new Person("Oliver", "Gierke"); + Contact result = repository.save(person); + + Example example = Example.of(person, ExampleSpec.typed(Contact.class)); + + assertThat(repository.findOne(example), is(nullValue())); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java index 7982bd3d0a..1d4b4460a6 100755 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepositoryTests.java @@ -183,7 +183,7 @@ public void findByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - Page result = repository.findAll(new Example(sample), new PageRequest(0, 10)); + Page result = repository.findAll(Example.of(sample), new PageRequest(0, 10)); assertThat(result.getContent(), hasItems(dave, oliver)); assertThat(result.getContent(), hasSize(2)); @@ -199,9 +199,9 @@ public void findAllByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); - assertThat(result, hasItems(dave, oliver)); + assertThat(result, containsInAnyOrder(dave, oliver)); assertThat(result, hasSize(2)); } @@ -221,7 +221,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObject() sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); assertThat(result, hasItem(dave)); assertThat(result, hasSize(1)); @@ -243,7 +243,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingPartialNestedOb sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); assertThat(result, hasItems(dave, oliver)); assertThat(result, hasSize(2)); @@ -262,7 +262,7 @@ public void findAllByExampleShouldNotFindEntriesWhenUsingPartialNestedObjectInSt sample.setAddress(new Address(null, null, "Washington")); trimDomainType(sample, "id", "createdAt", "email"); - Example example = ExampleSpec.of(Person.class).withIncludeNullValues().createExample(sample); + Example example = Example.of(sample, ExampleSpec.typed(Person.class).withIncludeNullValues()); List result = repository.findAll(example); assertThat(result, empty()); @@ -281,7 +281,7 @@ public void findAllByExampleShouldLookUpEntriesCorrectlyWhenUsingNestedObjectInS sample.setAddress(dave.getAddress()); trimDomainType(sample, "id", "createdAt", "email"); - Example example = ExampleSpec.of(Person.class).withIncludeNullValues().createExample(sample); + Example example = Example.of(sample, ExampleSpec.untyped().withIncludeNullValues()); List result = repository.findAll(example); assertThat(result, hasItem(dave)); @@ -298,7 +298,7 @@ public void findAllByExampleShouldRespectStringMatchMode() { sample.setLastname("Mat"); trimDomainType(sample, "id", "createdAt", "email"); - Example example = ExampleSpec.of(Person.class).withStringMatcher(StringMatcher.STARTING).createExample(sample); + Example example = Example.of(sample, ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING)); List result = repository.findAll(example); assertThat(result, hasItems(dave, oliver)); @@ -325,7 +325,7 @@ public void findAllByExampleShouldResolveDbRefCorrectly() { sample.setCreator(user); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -346,7 +346,7 @@ public void findAllByExampleShouldResolveLegacyCoordinatesCorrectly() { sample.setLocation(megan.getLocation()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -367,7 +367,7 @@ public void findAllByExampleShouldResolveGeoJsonCoordinatesCorrectly() { sample.setLocation(megan.getLocation()); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); assertThat(result, hasItem(megan)); assertThat(result, hasSize(1)); @@ -383,9 +383,9 @@ public void findAllByExampleShouldProcessInheritanceCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - List result = repository.findAll(new Example(sample)); + List result = repository.findAll(Example.of(sample)); - assertThat(result, hasItems(dave, oliver)); + assertThat(result, containsInAnyOrder(dave, oliver)); assertThat(result, hasSize(2)); } @@ -400,7 +400,7 @@ public void findOneByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - Person result = repository.findOne(new Example(sample)); + Person result = repository.findOne(Example.of(sample)); assertThat(result, is(equalTo(dave))); } @@ -416,7 +416,7 @@ public void existsByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - boolean result = repository.exists(new Example(sample)); + boolean result = repository.exists(Example.of(sample)); assertThat(result, is(true)); } @@ -431,7 +431,7 @@ public void countByExampleShouldLookUpEntriesCorrectly() { sample.setLastname("Matthews"); trimDomainType(sample, "id", "createdAt", "email"); - long result = repository.count(new Example(sample)); + long result = repository.count(Example.of(sample)); assertThat(result, is(equalTo(2L))); } From 6be07adf39cc680cad27eb6e6ccef5cbe4f03e8f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 11:33:06 +0100 Subject: [PATCH 13/14] DATAMONGO-1245 - Update documentation. --- src/main/asciidoc/index.adoc | 4 +--- src/main/asciidoc/reference/mongodb.adoc | 4 ++++ src/main/asciidoc/reference/query-by-example.adoc | 6 ++++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 42163f8296..aee79f8aed 100644 --- a/src/main/asciidoc/index.adoc +++ b/src/main/asciidoc/index.adoc @@ -1,5 +1,5 @@ = Spring Data MongoDB - Reference Documentation -Mark Pollack; Thomas Risberg; Oliver Gierke; Costin Leau; Jon Brisbin; Thomas Darimont; Christoph Strobl +Mark Pollack; Thomas Risberg; Oliver Gierke; Costin Leau; Jon Brisbin; Thomas Darimont; Christoph Strobl; Mark Paluch :revnumber: {version} :revdate: {localdate} :toc: @@ -27,8 +27,6 @@ include::{spring-data-commons-docs}/repositories.adoc[] include::reference/introduction.adoc[] include::reference/mongodb.adoc[] include::reference/mongo-repositories.adoc[] -include::{spring-data-commons-docs}/query-by-example.adoc[] -include::reference/query-by-example.adoc[] include::{spring-data-commons-docs}/auditing.adoc[] include::reference/mongo-auditing.adoc[] include::reference/mapping.adoc[] diff --git a/src/main/asciidoc/reference/mongodb.adoc b/src/main/asciidoc/reference/mongodb.adoc index 976f9c18a5..00eea75141 100644 --- a/src/main/asciidoc/reference/mongodb.adoc +++ b/src/main/asciidoc/reference/mongodb.adoc @@ -1330,6 +1330,10 @@ TextQuery.searching(new TextCriteria().matching("\"coffee cake\"")); TextQuery.searching(new TextCriteria().phrase("coffee cake")); ---- +include::../{spring-data-commons-docs}/query-by-example.adoc[] +include::query-by-example.adoc[] + + [[mongo.mapreduce]] == Map-Reduce Operations diff --git a/src/main/asciidoc/reference/query-by-example.adoc b/src/main/asciidoc/reference/query-by-example.adoc index 0a9abee098..ea30113d72 100644 --- a/src/main/asciidoc/reference/query-by-example.adoc +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -1,5 +1,5 @@ [[query.by.example.execution]] -== Executing Query by Example +=== Executing Example In Spring Data MongoDB you can use Query by Example with the `MongoTemplate` and with Repositories. @@ -17,7 +17,7 @@ public class PersonService { ---- ==== -`MongoTemplate.findByExample` accepts either the probe or an `Example` object to create and execute a query. +`MongoTemplate.findByExample` accepts either the probe or an `Example` object to create and execute a query. An `Example` containing an untyped `ExampleSpec` uses the probe type to determine the collection name and the result type. Typed `ExampleSpec` use their type to determine collection name and result type. .Query by Example using a Repository @@ -39,6 +39,8 @@ public class PersonService { ---- ==== +An `Example` containing an untyped `ExampleSpec` uses the Repository type and its collection name. Typed `ExampleSpec` use their type as result type and the collection name from the Repository. + NOTE: When including `null` values in the `ExampleSpec` Spring Data Mongo uses embedded document matching instead of dot notation property matching. This forces exact document matching for all property values and the property order in the embedded document. Spring Data MongoDB provides support for the following matching options: From c9b6344a81482787670e60e0e790e02a92265589 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 1 Mar 2016 17:28:16 +0100 Subject: [PATCH 14/14] DATAMONGO-1245 - Add changes from review. --- .../data/mongodb/core/MongoOperations.java | 32 --- .../data/mongodb/core/MongoTemplate.java | 213 ++++++++---------- .../core/convert/MongoExampleMapper.java | 26 ++- .../mongodb/core/convert/QueryMapper.java | 8 +- .../core/query/SerializationUtils.java | 4 +- .../mongodb/repository/MongoRepository.java | 5 - .../query/ConvertingParameterAccessor.java | 6 - .../query/MongoParameterAccessor.java | 9 - .../repository/query/MongoParameters.java | 24 +- .../MongoParametersParameterAccessor.java | 13 +- .../repository/query/MongoQueryCreator.java | 23 +- .../support/SimpleMongoRepository.java | 62 ++--- .../core/SerializationUtilsUnitTests.java | 38 ++-- .../core/temp/QueryByExampleTests.java | 21 +- ...tractPersonRepositoryIntegrationTests.java | 59 +++-- .../data/mongodb/repository/Contact.java | 3 +- .../ContactRepositoryIntegrationTests.java | 9 + .../mongodb/repository/PersonRepository.java | 7 +- .../query/StubParameterAccessor.java | 10 - .../asciidoc/reference/query-by-example.adoc | 21 +- 20 files changed, 234 insertions(+), 359 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index be72a7be89..dab3b0de9d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -19,7 +19,6 @@ import java.util.List; import java.util.Set; -import org.springframework.data.domain.Example; import org.springframework.data.geo.GeoResults; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -57,7 +56,6 @@ * @author Chuong Ngo * @author Christoph Strobl * @author Thomas Darimont - * @author Mark Paluch */ public interface MongoOperations { @@ -630,36 +628,6 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin */ T findById(Object id, Class entityClass, String collectionName); - /** - * Map the results of a Query-by-Example query onto the given {@code probe} class. The collection is determined from the {@code probe} type. - *

        - * The objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless - * configured otherwise, an instance of MappingMongoConverter will be used. - *

        - * The query is constructed from the {@code probe}. - * - * @param probe the probe to be used for the Example, must not be {@literal null}. - * @param - * @param - * @return the List of converted objects - */ - List findByExample(S probe); - - /** - * Map the results of a Query-by-Example query onto the given {@link Example#getProbeType()} class. The collection is determined from the {@link Example#getProbeType()}. - *

        - * The objects are converted from the MongoDB native representation using an instance of {@see MongoConverter}. Unless - * configured otherwise, an instance of MappingMongoConverter will be used. - *

        - * The query is constructed from the {@link Example}. - * - * @param example the example, must not be {@literal null}. - * @param - * @param - * @return the List of converted objects - */ - List findByExample(Example example); - /** * Triggers findAndModify * to apply provided {@link Update} on documents matching {@link Criteria} of given {@link Query}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 2be3753c43..8bc83ff0b1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2016 the original author or authors. + * Copyright 2010-2015 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -51,7 +51,6 @@ import org.springframework.data.annotation.Id; import org.springframework.data.authentication.UserCredentials; import org.springframework.data.convert.EntityReader; -import org.springframework.data.domain.Example; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; @@ -126,7 +125,7 @@ /** * Primary implementation of {@link MongoOperations}. - * + * * @author Thomas Risberg * @author Graeme Rocher * @author Mark Pollack @@ -139,7 +138,6 @@ * @author Chuong Ngo * @author Christoph Strobl * @author Doménique Tilleuil - * @author Mark Paluch */ @SuppressWarnings("deprecation") public class MongoTemplate implements MongoOperations, ApplicationContextAware { @@ -176,7 +174,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware { /** * Constructor used for a basic template configuration - * + * * @param mongo must not be {@literal null}. * @param databaseName must not be {@literal null} or empty. */ @@ -187,7 +185,7 @@ public MongoTemplate(Mongo mongo, String databaseName) { /** * Constructor used for a template configuration with user credentials in the form of * {@link org.springframework.data.authentication.UserCredentials} - * + * * @param mongo must not be {@literal null}. * @param databaseName must not be {@literal null} or empty. * @param userCredentials @@ -198,7 +196,7 @@ public MongoTemplate(Mongo mongo, String databaseName, UserCredentials userCrede /** * Constructor used for a basic template configuration. - * + * * @param mongoDbFactory must not be {@literal null}. */ public MongoTemplate(MongoDbFactory mongoDbFactory) { @@ -207,7 +205,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory) { /** * Constructor used for a basic template configuration. - * + * * @param mongoDbFactory must not be {@literal null}. * @param mongoConverter */ @@ -236,7 +234,7 @@ public MongoTemplate(MongoDbFactory mongoDbFactory, MongoConverter mongoConverte /** * Configures the {@link WriteResultChecking} to be used with the template. Setting {@literal null} will reset the * default of {@value #DEFAULT_WRITE_RESULT_CHECKING}. - * + * * @param resultChecking */ public void setWriteResultChecking(WriteResultChecking resultChecking) { @@ -247,7 +245,7 @@ public void setWriteResultChecking(WriteResultChecking resultChecking) { * Configures the {@link WriteConcern} to be used with the template. If none is configured the {@link WriteConcern} * configured on the {@link MongoDbFactory} will apply. If you configured a {@link Mongo} instance no * {@link WriteConcern} will be used. - * + * * @param writeConcern */ public void setWriteConcern(WriteConcern writeConcern) { @@ -256,7 +254,7 @@ public void setWriteConcern(WriteConcern writeConcern) { /** * Configures the {@link WriteConcernResolver} to be used with the template. - * + * * @param writeConcernResolver */ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { @@ -266,7 +264,7 @@ public void setWriteConcernResolver(WriteConcernResolver writeConcernResolver) { /** * Used by @{link {@link #prepareCollection(DBCollection)} to set the {@link ReadPreference} before any operations are * performed. - * + * * @param readPreference */ public void setReadPreference(ReadPreference readPreference) { @@ -293,7 +291,7 @@ public void setApplicationContext(ApplicationContext applicationContext) throws * they were registered for the current {@link MappingContext}. If no creator for the current {@link MappingContext} * can be found we manually add the internally created one as {@link ApplicationListener} to make sure indexes get * created appropriately for entity types persisted through this {@link MongoTemplate} instance. - * + * * @param context must not be {@literal null}. */ private void prepareIndexCreator(ApplicationContext context) { @@ -314,14 +312,14 @@ private void prepareIndexCreator(ApplicationContext context) { /** * Returns the default {@link org.springframework.data.mongodb.core.core.convert.MongoConverter}. - * + * * @return */ public MongoConverter getConverter() { return this.mongoConverter; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.MongoOperations#executeAsStream(org.springframework.data.mongodb.core.query.Query, java.lang.Class) */ @@ -341,8 +339,8 @@ public CloseableIterator doInCollection(DBCollection collection) throws Mongo DBCursor cursor = collection.find(mappedQuery, mappedFields); QueryCursorPreparer cursorPreparer = new QueryCursorPreparer(query, entityType); - ReadDbObjectCallback readCallback = new ReadDbObjectCallback(mongoConverter, entityType, collection - .getName()); + ReadDbObjectCallback readCallback = new ReadDbObjectCallback(mongoConverter, entityType, + collection.getName()); return new CloseableIterableCursorAdapter(cursorPreparer.prepare(cursor), exceptionTranslator, readCallback); } @@ -375,8 +373,8 @@ public CommandResult doInDB(DB db) throws MongoException, DataAccessException { */ @Deprecated public CommandResult executeCommand(final DBObject command, final int options) { - return executeCommand(command, (options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred() - : ReadPreference.primary()); + return executeCommand(command, + (options & Bytes.QUERYOPTION_SLAVEOK) != 0 ? ReadPreference.secondaryPreferred() : ReadPreference.primary()); } /* @@ -416,7 +414,7 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan /** * Execute a MongoDB query and iterate over the query results on a per-document basis with a * {@link DocumentCallbackHandler} using the provided CursorPreparer. - * + * * @param query the query class that specifies the criteria used to find a record and also an optional fields * specification, must not be {@literal null}. * @param collectionName name of the collection to retrieve the objects from @@ -424,7 +422,8 @@ public void executeQuery(Query query, String collectionName, DocumentCallbackHan * @param preparer allows for customization of the {@link DBCursor} used when iterating over the result set, (apply * limits, skips and so on). */ - protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch, CursorPreparer preparer) { + protected void executeQuery(Query query, String collectionName, DocumentCallbackHandler dch, + CursorPreparer preparer) { Assert.notNull(query); @@ -639,25 +638,6 @@ public T findById(Object id, Class entityClass, String collectionName) { return doFindOne(collectionName, new BasicDBObject(idKey, id), null, entityClass); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.MongoOperations#findByExample(java.lang.Object) - */ - public List findByExample(S sample) { - return findByExample(Example.of(sample)); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.MongoOperations#findByExample(org.springframework.data.domain.Example) - */ - @SuppressWarnings("unchecked") - public List findByExample(Example sample) { - - Assert.notNull(sample, "Sample object must not be null!"); - return (List) find(new Query(new Criteria().alike(sample)), sample.getResultType()); - } - public GeoResults geoNear(NearQuery near, Class entityClass) { return geoNear(near, entityClass, determineCollectionName(entityClass)); } @@ -688,8 +668,8 @@ public GeoResults geoNear(NearQuery near, Class entityClass, String co List results = (List) commandResult.get("results"); results = results == null ? Collections.emptyList() : results; - DbObjectCallback> callback = new GeoNearResultDbObjectCallback(new ReadDbObjectCallback( - mongoConverter, entityClass, collectionName), near.getMetric()); + DbObjectCallback> callback = new GeoNearResultDbObjectCallback( + new ReadDbObjectCallback(mongoConverter, entityClass, collectionName), near.getMetric()); List> result = new ArrayList>(results.size()); int index = 0; @@ -700,7 +680,7 @@ public GeoResults geoNear(NearQuery near, Class entityClass, String co /* * As MongoDB currently (2.4.4) doesn't support the skipping of elements in near queries * we skip the elements ourselves to avoid at least the document 2 object mapping overhead. - * + * * @see https://jira.mongodb.org/browse/SERVER-3925 */ if (index >= elementsToSkip) { @@ -765,8 +745,9 @@ public long count(final Query query, String collectionName) { public long count(Query query, Class entityClass, String collectionName) { Assert.hasText(collectionName); - final DBObject dbObject = query == null ? null : queryMapper.getMappedObject(query.getQueryObject(), - entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); + final DBObject dbObject = query == null ? null + : queryMapper.getMappedObject(query.getQueryObject(), + entityClass == null ? null : mappingContext.getPersistentEntity(entityClass)); return execute(collectionName, new CollectionCallback() { public Long doInCollection(DBCollection collection) throws MongoException, DataAccessException { @@ -804,7 +785,7 @@ protected void ensureNotIterable(Object o) { /** * Prepare the collection before any processing is done using it. This allows a convenient way to apply settings like * slaveOk() etc. Can be overridden in sub-classes. - * + * * @param collection */ protected void prepareCollection(DBCollection collection) { @@ -818,7 +799,7 @@ protected void prepareCollection(DBCollection collection) { * settings in sub-classes.
        * In case of using MongoDB Java driver version 3 the returned {@link WriteConcern} will be defaulted to * {@link WriteConcern#ACKNOWLEDGED} when {@link WriteResultChecking} is set to {@link WriteResultChecking#EXCEPTION}. - * + * * @param writeConcern any WriteConcern already configured or null * @return The prepared WriteConcern or null */ @@ -1043,8 +1024,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc) : collection.insert(dbDoc, - writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDoc) + : collection.insert(dbDoc, writeConcernToUse); handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.INSERT); return dbDoc.get(ID_FIELD); } @@ -1064,8 +1045,8 @@ public Void doInCollection(DBCollection collection) throws MongoException, DataA MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.INSERT_LIST, collectionName, null, null, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList) : collection.insert( - dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.insert(dbDocList) + : collection.insert(dbDocList.toArray((DBObject[]) new BasicDBObject[dbDocList.size()]), writeConcernToUse); handleAnyWriteResultErrors(writeResult, null, MongoActionOperation.INSERT_LIST); return null; } @@ -1093,8 +1074,8 @@ public Object doInCollection(DBCollection collection) throws MongoException, Dat MongoAction mongoAction = new MongoAction(writeConcern, MongoActionOperation.SAVE, collectionName, entityClass, dbDoc, null); WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); - WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc) : collection.save(dbDoc, - writeConcernToUse); + WriteResult writeResult = writeConcernToUse == null ? collection.save(dbDoc) + : collection.save(dbDoc, writeConcernToUse); handleAnyWriteResultErrors(writeResult, dbDoc, MongoActionOperation.SAVE); return dbDoc.get(ID_FIELD); } @@ -1147,10 +1128,10 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException increaseVersionForUpdateIfNecessary(entity, update); - DBObject queryObj = query == null ? new BasicDBObject() : queryMapper.getMappedObject(query.getQueryObject(), - entity); - DBObject updateObj = update == null ? new BasicDBObject() : updateMapper.getMappedObject( - update.getUpdateObject(), entity); + DBObject queryObj = query == null ? new BasicDBObject() + : queryMapper.getMappedObject(query.getQueryObject(), entity); + DBObject updateObj = update == null ? new BasicDBObject() + : updateMapper.getMappedObject(update.getUpdateObject(), entity); if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Calling update using query: %s and update: %s in collection: %s", @@ -1219,7 +1200,7 @@ public WriteResult remove(Object object, String collection) { /** * Returns {@link Entry} containing the field name of the id property as {@link Entry#getKey()} and the {@link Id}s * property value as its {@link Entry#getValue()}. - * + * * @param object * @return */ @@ -1246,7 +1227,7 @@ private Entry extractIdPropertyAndValue(Object object) { /** * Returns a {@link Query} for the given entity by its id. - * + * * @param object must not be {@literal null}. * @return */ @@ -1258,7 +1239,7 @@ private Query getIdQueryFor(Object object) { /** * Returns a {@link Query} for the given entities by their ids. - * + * * @param objects must not be {@literal null} or {@literal empty}. * @return */ @@ -1291,9 +1272,9 @@ private void assertUpdateableIdIfNotSet(Object entity) { Object idValue = persistentEntity.getPropertyAccessor(entity).getProperty(idProperty); if (idValue == null && !MongoSimpleTypes.AUTOGENERATED_ID_TYPES.contains(idProperty.getType())) { - throw new InvalidDataAccessApiUsageException(String.format( - "Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(), entity.getClass() - .getName())); + throw new InvalidDataAccessApiUsageException( + String.format("Cannot autogenerate id of type %s for entity of type %s!", idProperty.getType().getName(), + entity.getClass().getName())); } } @@ -1332,12 +1313,12 @@ public WriteResult doInCollection(DBCollection collection) throws MongoException WriteConcern writeConcernToUse = prepareWriteConcern(mongoAction); if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Remove using query: {} in collection: {}.", new Object[] { serializeToJsonSafely(dboq), - collection.getName() }); + LOGGER.debug("Remove using query: {} in collection: {}.", + new Object[] { serializeToJsonSafely(dboq), collection.getName() }); } - WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) : collection.remove(dboq, - writeConcernToUse); + WriteResult wr = writeConcernToUse == null ? collection.remove(dboq) + : collection.remove(dboq, writeConcernToUse); handleAnyWriteResultErrors(wr, dboq, MongoActionOperation.REMOVE); @@ -1353,8 +1334,8 @@ public List findAll(Class entityClass) { } public List findAll(Class entityClass, String collectionName) { - return executeFindMultiInternal(new FindCallback(null), null, new ReadDbObjectCallback(mongoConverter, - entityClass, collectionName), collectionName); + return executeFindMultiInternal(new FindCallback(null), null, + new ReadDbObjectCallback(mongoConverter, entityClass, collectionName), collectionName); } public MapReduceResults mapReduce(String inputCollectionName, String mapFunction, String reduceFunction, @@ -1370,8 +1351,8 @@ public MapReduceResults mapReduce(String inputCollectionName, String mapF public MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, String reduceFunction, Class entityClass) { - return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, - new MapReduceOptions().outputTypeInline(), entityClass); + return mapReduce(query, inputCollectionName, mapFunction, reduceFunction, new MapReduceOptions().outputTypeInline(), + entityClass); } public MapReduceResults mapReduce(Query query, String inputCollectionName, String mapFunction, @@ -1382,8 +1363,9 @@ public MapReduceResults mapReduce(Query query, String inputCollectionName DBCollection inputCollection = getCollection(inputCollectionName); MapReduceCommand command = new MapReduceCommand(inputCollection, mapFunc, reduceFunc, - mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), query == null - || query.getQueryObject() == null ? null : queryMapper.getMappedObject(query.getQueryObject(), null)); + mapReduceOptions.getOutputCollection(), mapReduceOptions.getOutputType(), + query == null || query.getQueryObject() == null ? null + : queryMapper.getMappedObject(query.getQueryObject(), null)); copyMapReduceOptionsToCommand(query, mapReduceOptions, command); @@ -1528,7 +1510,7 @@ public List findAllAndRemove(Query query, Class entityClass, String co * Retrieve and remove all documents matching the given {@code query} by calling {@link #find(Query, Class, String)} * and {@link #remove(Query, Class, String)}, whereas the {@link Query} for {@link #remove(Query, Class, String)} is * constructed out of the find result. - * + * * @param collectionName * @param query * @param entityClass @@ -1568,7 +1550,7 @@ protected AggregationResults aggregate(Aggregation aggregation, String co /** * Returns the potentially mapped results of the given {@commandResult} contained some. - * + * * @param outputType * @param commandResult * @return @@ -1680,7 +1662,7 @@ protected void maybeEmitEvent(MongoMappingEvent event) { /** * Create the specified collection using the provided options - * + * * @param collectionName * @param collectionOptions * @return the collection that was created @@ -1701,7 +1683,7 @@ public DBCollection doInDB(DB db) throws MongoException, DataAccessException { /** * Map the results of an ad-hoc query on the default MongoDB collection to an object using the template's converter. * The query document is specified as a standard {@link DBObject} and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. @@ -1719,14 +1701,14 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields serializeToJsonSafely(query), mappedFields, entityClass, collectionName)); } - return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), new ReadDbObjectCallback( - this.mongoConverter, entityClass, collectionName), collectionName); + return executeFindOneInternal(new FindOneCallback(mappedQuery, mappedFields), + new ReadDbObjectCallback(this.mongoConverter, entityClass, collectionName), collectionName); } /** * Map the results of an ad-hoc query on the default MongoDB collection to a List using the template's converter. The * query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param fields the document that specifies the fields to be returned @@ -1734,15 +1716,15 @@ protected T doFindOne(String collectionName, DBObject query, DBObject fields * @return the List of converted objects. */ protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass) { - return doFind(collectionName, query, fields, entityClass, null, new ReadDbObjectCallback(this.mongoConverter, - entityClass, collectionName)); + return doFind(collectionName, query, fields, entityClass, null, + new ReadDbObjectCallback(this.mongoConverter, entityClass, collectionName)); } /** * Map the results of an ad-hoc query on the default MongoDB collection to a List of the specified type. The object is * converted from the MongoDB native representation using an instance of {@see MongoConverter}. The query document is * specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from. * @param query the query document that specifies the criteria used to find a record. * @param fields the document that specifies the fields to be returned. @@ -1753,8 +1735,8 @@ protected List doFind(String collectionName, DBObject query, DBObject fie */ protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, CursorPreparer preparer) { - return doFind(collectionName, query, fields, entityClass, preparer, new ReadDbObjectCallback(mongoConverter, - entityClass, collectionName)); + return doFind(collectionName, query, fields, entityClass, preparer, + new ReadDbObjectCallback(mongoConverter, entityClass, collectionName)); } protected List doFind(String collectionName, DBObject query, DBObject fields, Class entityClass, @@ -1795,7 +1777,7 @@ protected DBObject convertToDbObject(CollectionOptions collectionOptions) { * The first document that matches the query is returned and also removed from the collection in the database. *

        * The query document is specified as a standard DBObject and so is the fields specification. - * + * * @param collectionName name of the collection to retrieve the objects from * @param query the query document that specifies the criteria used to find a record * @param entityClass the parameterized type of the returned list. @@ -1830,9 +1812,13 @@ protected T doFindAndModify(String collectionName, DBObject query, DBObject DBObject mappedUpdate = updateMapper.getMappedObject(update.getUpdateObject(), entity); if (LOGGER.isDebugEnabled()) { - LOGGER.debug(String.format("findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " - + "in collection: %s", serializeToJsonSafely(mappedQuery), fields, sort, entityClass, - serializeToJsonSafely(mappedUpdate), collectionName)); + LOGGER + .debug( + String.format( + "findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " + + "in collection: %s", + serializeToJsonSafely(mappedQuery), fields, sort, entityClass, serializeToJsonSafely(mappedUpdate), + collectionName)); } return executeFindOneInternal(new FindAndModifyCallback(mappedQuery, fields, sort, mappedUpdate, options), @@ -1841,7 +1827,7 @@ protected T doFindAndModify(String collectionName, DBObject query, DBObject /** * Populates the id property of the saved object, if it's not set already. - * + * * @param savedObject * @param id */ @@ -1891,7 +1877,7 @@ private DBCollection getAndPrepareCollection(DB db, String collectionName) { *

      1. Execute the given {@link ConnectionCallback} for a {@link DBObject}.
      2. *
      3. Apply the given {@link DbObjectCallback} to each of the {@link DBObject}s to obtain the result.
      4. *
          - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBObject} with * @param objectCallback the {@link DbObjectCallback} to transform {@link DBObject}s into the actual domain type @@ -1902,8 +1888,8 @@ private T executeFindOneInternal(CollectionCallback collectionCall DbObjectCallback objectCallback, String collectionName) { try { - T result = objectCallback.doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), - collectionName))); + T result = objectCallback + .doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDb(), collectionName))); return result; } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -1920,7 +1906,7 @@ private T executeFindOneInternal(CollectionCallback collectionCall *
        1. Iterate over the {@link DBCursor} and applies the given {@link DbObjectCallback} to each of the * {@link DBObject}s collecting the actual result {@link List}.
        2. *
            - * + * * @param * @param collectionCallback the callback to retrieve the {@link DBCursor} with * @param preparer the {@link CursorPreparer} to potentially modify the {@link DBCursor} before ireating over it @@ -1928,8 +1914,8 @@ private T executeFindOneInternal(CollectionCallback collectionCall * @param collectionName the collection to be queried * @return */ - private List executeFindMultiInternal(CollectionCallback collectionCallback, - CursorPreparer preparer, DbObjectCallback objectCallback, String collectionName) { + private List executeFindMultiInternal(CollectionCallback collectionCallback, CursorPreparer preparer, + DbObjectCallback objectCallback, String collectionName) { try { @@ -2019,15 +2005,15 @@ String determineCollectionName(Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); if (entity == null) { - throw new InvalidDataAccessApiUsageException("No Persistent Entity information found for the class " - + entityClass.getName()); + throw new InvalidDataAccessApiUsageException( + "No Persistent Entity information found for the class " + entityClass.getName()); } return entity.getCollection(); } /** * Handles {@link WriteResult} errors based on the configured {@link WriteResultChecking}. - * + * * @param writeResult * @param query * @param operation @@ -2071,7 +2057,7 @@ protected void handleAnyWriteResultErrors(WriteResult writeResult, DBObject quer /** * Inspects the given {@link CommandResult} for erros and potentially throws an * {@link InvalidDataAccessApiUsageException} for that error. - * + * * @param result must not be {@literal null}. * @param source must not be {@literal null}. */ @@ -2084,8 +2070,8 @@ private void handleCommandError(CommandResult result, DBObject source) { String error = result.getErrorMessage(); error = error == null ? "NO MESSAGE" : error; - throw new InvalidDataAccessApiUsageException("Command execution failed: Error [" + error + "], Command = " - + source, ex); + throw new InvalidDataAccessApiUsageException( + "Command execution failed: Error [" + error + "], Command = " + source, ex); } } @@ -2109,7 +2095,7 @@ private DBObject getMappedSortObject(Query query, Class type) { /** * Tries to convert the given {@link RuntimeException} into a {@link DataAccessException} but returns the original * exception if the conversation failed. Thus allows safe re-throwing of the return value. - * + * * @param ex the exception to translate * @param exceptionTranslator the {@link PersistenceExceptionTranslator} to be used for translation * @return @@ -2125,7 +2111,7 @@ private static RuntimeException potentiallyConvertRuntimeException(RuntimeExcept /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -2159,7 +2145,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Oliver Gierke * @author Thomas Risberg */ @@ -2190,7 +2176,7 @@ public DBCursor doInCollection(DBCollection collection) throws MongoException, D /** * Simple {@link CollectionCallback} that takes a query {@link DBObject} plus an optional fields specification * {@link DBObject} and executes that against the {@link DBCollection}. - * + * * @author Thomas Risberg */ private static class FindAndRemoveCallback implements CollectionCallback { @@ -2235,7 +2221,7 @@ public DBObject doInCollection(DBCollection collection) throws MongoException, D /** * Simple internal callback to allow operations on a {@link DBObject}. - * + * * @author Oliver Gierke * @author Thomas Darimont */ @@ -2248,7 +2234,7 @@ static interface DbObjectCallback { /** * Simple {@link DbObjectCallback} that will transform {@link DBObject} into the given target type using the given * {@link MongoReader}. - * + * * @author Oliver Gierke * @author Christoph Strobl */ @@ -2281,7 +2267,8 @@ public T doWith(DBObject object) { class UnwrapAndReadDbObjectCallback extends ReadDbObjectCallback { - public UnwrapAndReadDbObjectCallback(EntityReader reader, Class type, String collectionName) { + public UnwrapAndReadDbObjectCallback(EntityReader reader, Class type, + String collectionName) { super(reader, type, collectionName); } @@ -2367,7 +2354,7 @@ public DBCursor prepare(DBCursor cursor) { /** * {@link DbObjectCallback} that assumes a {@link GeoResult} to be created, delegates actual content unmarshalling to * a delegate and creates a {@link GeoResult} from the result. - * + * * @author Oliver Gierke */ static class GeoNearResultDbObjectCallback implements DbObjectCallback> { @@ -2378,7 +2365,7 @@ static class GeoNearResultDbObjectCallback implements DbObjectCallback delegate, Metric metric) { @@ -2400,7 +2387,7 @@ public GeoResult doWith(DBObject object) { /** * A {@link CloseableIterator} that is backed by a MongoDB {@link Cursor}. - * + * * @since 1.7 * @author Thomas Darimont */ @@ -2412,7 +2399,7 @@ static class CloseableIterableCursorAdapter implements CloseableIterator { /** * Creates a new {@link CloseableIterableCursorAdapter} backed by the given {@link Cursor}. - * + * * @param cursor * @param exceptionTranslator * @param objectReadCallback diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index f4a4379e44..7463921fe5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -17,16 +17,19 @@ import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import java.util.Stack; import java.util.regex.Pattern; +import org.bson.BasicBSONObject; import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleSpec; import org.springframework.data.domain.ExampleSpec.NullHandler; import org.springframework.data.domain.ExampleSpec.PropertyValueTransformer; import org.springframework.data.domain.ExampleSpec.StringMatcher; @@ -101,13 +104,11 @@ public DBObject getMappedExample(Example example, MongoPersistentEntity en applyPropertySpecs("", reference, example.getProbeType(), exampleSpecAccessor); if (exampleSpecAccessor.isTyped()) { - Set> restrictedTypes = new HashSet>(); - restrictedTypes.add(example.getResultType()); - this.converter.getTypeMapper().writeTypeRestrictions(reference, restrictedTypes); + this.converter.getTypeMapper().writeTypeRestrictions(reference, (Set) Collections.singleton(example.getResultType())); } return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, exampleSpecAccessor.getNullHandler()) ? reference - : new BasicDBObject(SerializationUtils.flatMap(reference)); + : new BasicDBObject(SerializationUtils.flattenMap(reference)); } private String getMappedPropertyPath(String path, Class probeType) { @@ -169,15 +170,14 @@ private void applyPropertySpecs(String path, DBObject source, Class probeType while (iter.hasNext()) { Map.Entry entry = iter.next(); - - if (entry.getKey().equals("_id") && entry.getValue() == null) { + String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); + String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); + + if(isEmptyIdProperty(entry)) { iter.remove(); continue; } - - String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); - - String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); + if (exampleSpecAccessor.isIgnoredPath(propertyPath) || exampleSpecAccessor.isIgnoredPath(mappedPropertyPath)) { iter.remove(); continue; @@ -217,6 +217,10 @@ private void applyPropertySpecs(String path, DBObject source, Class probeType } } + private boolean isEmptyIdProperty(Entry entry) { + return entry.getKey().equals("_id") && entry.getValue() == null; + } + private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, boolean ignoreCase) { BasicDBObject dbo = new BasicDBObject(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 34177102b7..e6b9587c2a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -261,8 +261,8 @@ protected DBObject getMappedKeyword(Field property, Keyword keyword) { boolean needsAssociationConversion = property.isAssociation() && !keyword.isExists(); Object value = keyword.getValue(); - Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) : getMappedValue( - property.with(keyword.getKey()), value); + Object convertedValue = needsAssociationConversion ? convertAssociation(value, property) + : getMappedValue(property.with(keyword.getKey()), value); return new BasicDBObject(keyword.key, convertedValue); } @@ -484,8 +484,8 @@ public Object convertId(Object id) { } try { - return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService - .convert(id, ObjectId.class) : delegateConvertToMongoType(id, null); + return conversionService.canConvert(id.getClass(), ObjectId.class) ? conversionService.convert(id, ObjectId.class) + : delegateConvertToMongoType(id, null); } catch (ConversionException o_O) { return delegateConvertToMongoType(id, null); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java index efc904b03a..4064b239b8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -63,7 +63,7 @@ private SerializationUtils() { * @return {@link Collections#emptyMap()} when source is {@literal null} * @since 1.8 */ - public static Map flatMap(DBObject source) { + public static Map flattenMap(DBObject source) { if (source == null) { return Collections.emptyMap(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java index cf3b11db96..59b013d37a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/MongoRepository.java @@ -40,21 +40,18 @@ public interface MongoRepository * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ - @Override List save(Iterable entites); /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ - @Override List findAll(); /* * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ - @Override List findAll(Sort sort); /** @@ -82,13 +79,11 @@ public interface MongoRepository /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) */ - @Override List findAll(Example example); /* (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) */ - @Override List findAll(Example example, Sort sort); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index d6e8dfbd6b..15d0cd25e6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -21,7 +21,6 @@ import java.util.Iterator; import java.util.List; -import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; @@ -133,11 +132,6 @@ public TextCriteria getFullText() { return delegate.getFullText(); } - @Override - public Example getSampleObject() { - return delegate.getSampleObject(); - } - /** * Converts the given value with the underlying {@link MongoWriter}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index a64793e645..6c67778e30 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.repository.query; -import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -61,12 +60,4 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @since 1.8 */ Object[] getValues(); - - /** - * Get the sample for query by example - * - * @return - * @since 1.8 - */ - Example getSampleObject(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 3173011451..1e36047b94 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -20,7 +20,6 @@ import java.util.List; import org.springframework.core.MethodParameter; -import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -43,7 +42,6 @@ public class MongoParameters extends Parameters private final int rangeIndex; private final int maxDistanceIndex; private final Integer fullTextIndex; - private final int sampleObjectIndex; private Integer nearIndex; @@ -71,12 +69,10 @@ public MongoParameters(Method method, boolean isGeoNearMethod) { } else if (this.nearIndex == null) { this.nearIndex = -1; } - - this.sampleObjectIndex = parameterTypes.indexOf(Example.class); } private MongoParameters(List parameters, int maxDistanceIndex, Integer nearIndex, - Integer fullTextIndex, int rangeIndex, int sampleObjectIndex) { + Integer fullTextIndex, int rangeIndex) { super(parameters); @@ -84,7 +80,6 @@ private MongoParameters(List parameters, int maxDistanceIndex, I this.fullTextIndex = fullTextIndex; this.maxDistanceIndex = maxDistanceIndex; this.rangeIndex = rangeIndex; - this.sampleObjectIndex = sampleObjectIndex; } private final int getNearIndex(List> parameterTypes) { @@ -187,22 +182,13 @@ public int getRangeIndex() { return rangeIndex; } - /** - * @return - * @since 1.8 - */ - public int getSampleObjectParameterIndex() { - return sampleObjectIndex; - } - /* * (non-Javadoc) * @see org.springframework.data.repository.query.Parameters#createFrom(java.util.List) */ @Override protected MongoParameters createFrom(List parameters) { - return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex, - this.sampleObjectIndex); + return new MongoParameters(parameters, this.maxDistanceIndex, this.nearIndex, this.fullTextIndex, this.rangeIndex); } private int getTypeIndex(List> parameterTypes, Class type, Class componentType) { @@ -254,7 +240,7 @@ class MongoParameter extends Parameter { @Override public boolean isSpecialParameter() { return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter() - || TextCriteria.class.isAssignableFrom(getType()) || isExample(); + || TextCriteria.class.isAssignableFrom(getType()); } private boolean isNearParameter() { @@ -274,10 +260,6 @@ private boolean hasNearAnnotation() { return parameter.getParameterAnnotation(Near.class) != null; } - private boolean isExample() { - return Example.class.isAssignableFrom(getType()); - } - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index 9cbe003e3d..7f8ac79b4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.List; -import org.springframework.data.domain.Example; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -126,15 +125,9 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) { return ((TextCriteria) fullText); } - throw new IllegalArgumentException(String.format( - "Expected full text parameter to be one of String, Term or TextCriteria but found %s.", - ClassUtils.getShortName(fullText.getClass()))); - } - - public Example getSampleObject() { - - int index = method.getParameters().getSampleObjectParameterIndex(); - return index >= 0 ? (Example) getValue(index) : null; + throw new IllegalArgumentException( + String.format("Expected full text parameter to be one of String, Term or TextCriteria but found %s.", + ClassUtils.getShortName(fullText.getClass()))); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 9c09a149f0..f0ba85d924 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -151,20 +151,7 @@ protected Criteria or(Criteria base, Criteria criteria) { @Override protected Query complete(Criteria criteria, Sort sort) { - Criteria toUse = null; - if (accessor.getSampleObject() != null) { - toUse = new Criteria().alike(accessor.getSampleObject()); - } - - if (criteria != null) { - if (toUse == null) { - toUse = criteria; - } else { - toUse.andOperator(criteria); - } - } - - Query query = (toUse == null ? new Query() : new Query(toUse)).with(sort); + Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort); if (LOG.isDebugEnabled()) { LOG.debug("Created query " + query); @@ -299,8 +286,8 @@ private Criteria createLikeRegexCriteriaOrThrow(Part part, MongoPersistentProper case ALWAYS: if (path.getType() != String.class) { - throw new IllegalArgumentException(String.format("Part %s must be of type String but was %s", path, - path.getType())); + throw new IllegalArgumentException( + String.format("Part %s must be of type String but was %s", path, path.getType())); } // fall-through @@ -386,8 +373,8 @@ private T nextAs(Iterator iterator, Class type) { return (T) parameter; } - throw new IllegalArgumentException(String.format("Expected parameter type of %s but got %s!", type, - parameter.getClass())); + throw new IllegalArgumentException( + String.format("Expected parameter type of %s but got %s!", type, parameter.getClass())); } private Object[] nextAsArray(PotentiallyConvertingIterator iterator, MongoPersistentProperty property) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index c4cfc0db3e..fcca93e668 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -41,7 +41,7 @@ /** * Repository base implementation for Mongo. - * + * * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont @@ -54,7 +54,7 @@ public class SimpleMongoRepository implements MongoR /** * Creates a new {@link SimpleMongoRepository} for the given {@link MongoEntityInformation} and {@link MongoTemplate}. - * + * * @param metadata must not be {@literal null}. * @param mongoOperations must not be {@literal null}. */ @@ -71,7 +71,6 @@ public SimpleMongoRepository(MongoEntityInformation metadata, MongoOperat * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Object) */ - @Override public S save(S entity) { Assert.notNull(entity, "Entity must not be null!"); @@ -89,7 +88,6 @@ public S save(S entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#save(java.lang.Iterable) */ - @Override public List save(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities not be null!"); @@ -119,11 +117,8 @@ public List save(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findOne(java.io.Serializable) */ - @Override public T findOne(ID id) { - Assert.notNull(id, "The given id must not be null!"); - return mongoOperations.findById(id, entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -139,11 +134,9 @@ private Criteria getIdCriteria(Object id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#exists(java.io.Serializable) */ - @Override public boolean exists(ID id) { Assert.notNull(id, "The given id must not be null!"); - return mongoOperations.exists(getIdQuery(id), entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -152,7 +145,6 @@ public boolean exists(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#count() */ - @Override public long count() { return mongoOperations.getCollection(entityInformation.getCollectionName()).count(); } @@ -161,11 +153,8 @@ public long count() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.io.Serializable) */ - @Override public void delete(ID id) { - Assert.notNull(id, "The given id must not be null!"); - mongoOperations.remove(getIdQuery(id), entityInformation.getJavaType(), entityInformation.getCollectionName()); } @@ -173,11 +162,8 @@ public void delete(ID id) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Object) */ - @Override public void delete(T entity) { - Assert.notNull(entity, "The given entity must not be null!"); - delete(entityInformation.getId(entity)); } @@ -185,7 +171,6 @@ public void delete(T entity) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#delete(java.lang.Iterable) */ - @Override public void delete(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities not be null!"); @@ -199,7 +184,6 @@ public void delete(Iterable entities) { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#deleteAll() */ - @Override public void deleteAll() { mongoOperations.remove(new Query(), entityInformation.getCollectionName()); } @@ -208,16 +192,14 @@ public void deleteAll() { * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll() */ - @Override public List findAll() { return findAll(new Query()); } - /* + /* * (non-Javadoc) * @see org.springframework.data.repository.CrudRepository#findAll(java.lang.Iterable) */ - @Override public Iterable findAll(Iterable ids) { Set parameters = new HashSet(tryDetermineRealSizeOrReturn(ids, 10)); @@ -232,7 +214,6 @@ public Iterable findAll(Iterable ids) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Pageable) */ - @Override public Page findAll(final Pageable pageable) { Long count = count(); @@ -245,12 +226,11 @@ public Page findAll(final Pageable pageable) { * (non-Javadoc) * @see org.springframework.data.repository.PagingAndSortingRepository#findAll(org.springframework.data.domain.Sort) */ - @Override public List findAll(Sort sort) { return findAll(new Query().with(sort)); } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoRepository#insert(java.lang.Object) */ @@ -263,7 +243,7 @@ public S insert(S entity) { return entity; } - /* + /* * (non-Javadoc) * @see org.springframework.data.mongodb.repository.MongoRepository#insert(java.lang.Iterable) */ @@ -293,12 +273,12 @@ public Page findAll(Example example, Pageable pageable) { Query q = new Query(new Criteria().alike(example)).with(pageable); - long count = mongoOperations.count(q, getResultType(example), getCollectionName(example)); + long count = mongoOperations.count(q, getResultType(example), entityInformation.getCollectionName()); if (count == 0) { return new PageImpl(Collections. emptyList()); } - return new PageImpl(mongoOperations.find(q, getResultType(example), getCollectionName(example)), - pageable, count); + return new PageImpl(mongoOperations.find(q, getResultType(example), entityInformation.getCollectionName()), pageable, + count); } /* @@ -316,7 +296,7 @@ public List findAll(Example example, Sort sort) { q.with(sort); } - return mongoOperations.find(q, getResultType(example), getCollectionName(example)); + return mongoOperations.find(q, getResultType(example), entityInformation.getCollectionName()); } /* @@ -328,46 +308,54 @@ public List findAll(Example example) { return findAll(example, (Sort) null); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example) + */ @Override public S findOne(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.findOne(q, getResultType(example), getCollectionName(example)); + return mongoOperations.findOne(q, getResultType(example), entityInformation.getCollectionName()); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#count(org.springframework.data.domain.Example) + */ @Override public long count(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.count(q, getResultType(example), getCollectionName(example)); + return mongoOperations.count(q, getResultType(example), entityInformation.getCollectionName()); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#exists(org.springframework.data.domain.Example) + */ @Override public boolean exists(Example example) { Assert.notNull(example, "Sample must not be null!"); Query q = new Query(new Criteria().alike(example)); - return mongoOperations.exists(q, getResultType(example), getCollectionName(example)); + return mongoOperations.exists(q, getResultType(example), entityInformation.getCollectionName()); } private Class getResultType(Example example) { - if(example.getExampleSpec() instanceof TypedExampleSpec){ + if (example.getExampleSpec() instanceof TypedExampleSpec) { return example.getResultType(); } return (Class) entityInformation.getJavaType(); } - private String getCollectionName(Example example) { - return entityInformation.getCollectionName(); - } - private List findAll(Query query) { if (query == null) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java index 231070d34f..0b42b27c03 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SerializationUtilsUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,8 +23,6 @@ import java.util.Map; import org.hamcrest.Matcher; -import org.hamcrest.collection.IsMapContaining; -import org.hamcrest.core.Is; import org.junit.Test; import org.springframework.data.mongodb.core.query.SerializationUtils; @@ -70,68 +68,68 @@ public void writesCollection() { * @see DATAMONGO-1245 */ @Test - public void flatMapShouldFlatOutNestedStructureCorrectly() { + public void flattenMapShouldFlatOutNestedStructureCorrectly() { DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", "conflux")).get(); - assertThat(flatMap(dbo), IsMapContaining. hasEntry("_id", 1)); - assertThat(flatMap(dbo), IsMapContaining. hasEntry("nested.value", "conflux")); + assertThat(flattenMap(dbo), hasEntry("_id", (Object) 1)); + assertThat(flattenMap(dbo), hasEntry("nested.value", (Object) "conflux")); } /** * @see DATAMONGO-1245 */ @Test - public void flatMapShouldFlatOutNestedStructureWithListCorrectly() { + public void flattenMapShouldFlatOutNestedStructureWithListCorrectly() { BasicDBList dbl = new BasicDBList(); dbl.addAll(Arrays.asList("nightwielder", "calamity")); DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", dbl)).get(); - assertThat(flatMap(dbo), IsMapContaining. hasEntry("_id", 1)); - assertThat(flatMap(dbo), IsMapContaining. hasEntry("nested.value", dbl)); + assertThat(flattenMap(dbo), hasEntry("_id", (Object) 1)); + assertThat(flattenMap(dbo), hasEntry("nested.value", (Object) dbl)); } /** * @see DATAMONGO-1245 */ @Test - public void flatMapShouldLeaveKeywordsUntouched() { + public void flattenMapShouldLeaveKeywordsUntouched() { DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("$regex", "^conflux$")) .get(); - Map map = flatMap(dbo); + Map map = flattenMap(dbo); - assertThat(map, IsMapContaining. hasEntry("_id", 1)); + assertThat(map, hasEntry("_id", (Object) 1)); assertThat(map.get("nested"), notNullValue()); - assertThat(((Map) map.get("nested")).get("$regex"), Is. is("^conflux$")); + assertThat(((Map) map.get("nested")).get("$regex"), is((Object) "^conflux$")); } /** * @see DATAMONGO-1245 */ @Test - public void flatMapSouldAppendCommandsCorrectly() { + public void flattenMapShouldAppendCommandsCorrectly() { DBObject dbo = new BasicDBObjectBuilder().add("_id", 1) .add("nested", new BasicDBObjectBuilder().add("$regex", "^conflux$").add("$options", "i").get()).get(); - Map map = flatMap(dbo); + Map map = flattenMap(dbo); - assertThat(map, IsMapContaining. hasEntry("_id", 1)); + assertThat(map, hasEntry("_id", (Object) 1)); assertThat(map.get("nested"), notNullValue()); - assertThat(((Map) map.get("nested")).get("$regex"), Is. is("^conflux$")); - assertThat(((Map) map.get("nested")).get("$options"), Is. is("i")); + assertThat(((Map) map.get("nested")).get("$regex"), is((Object) "^conflux$")); + assertThat(((Map) map.get("nested")).get("$options"), is((Object) "i")); } /** * @see DATAMONGO-1245 */ @Test - public void flatMapShouldReturnEmptyMapWhenSourceIsNull() { - assertThat(flatMap(null).isEmpty(), is(true)); + public void flattenMapShouldReturnEmptyMapWhenSourceIsNull() { + assertThat(flattenMap(null).isEmpty(), is(true)); } static class Complex { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java index c289ed57b6..57a92e08c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java @@ -58,7 +58,9 @@ public void findByExampleShouldWorkForSimpleProperty() { Person sample = new Person(); sample.lastname = "stark"; - List result = template.findByExample(sample); + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(2)); } @@ -74,7 +76,9 @@ public void findByExampleShouldWorkForMultipleProperties() { sample.lastname = "stark"; sample.firstname = "arya"; - List result = template.findByExample(sample); + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(1)); } @@ -92,7 +96,9 @@ public void findByExampleShouldWorkForIdProperty() { Person sample = new Person(); sample.id = p4.id; - List result = template.findByExample(sample); + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(1)); } @@ -108,7 +114,10 @@ public void findByExampleShouldReturnEmptyListIfNotMatching() { sample.firstname = "jon"; sample.firstname = "stark"; - List result = template.findByExample(sample); + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(0)); } @@ -122,7 +131,9 @@ public void findByExampleShouldReturnEverythingWhenSampleIsEmpty() { Person sample = new Person(); - List result = template.findByExample(sample); + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); Assert.assertThat(result.size(), Is.is(3)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 10a4e39219..b924d81883 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -28,8 +28,6 @@ import java.util.stream.Stream; import org.hamcrest.Matchers; -import org.hamcrest.core.Is; -import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -37,7 +35,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleSpec; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Range; @@ -63,7 +60,7 @@ /** * Base class for tests for {@link PersonRepository}. - * + * * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl @@ -181,8 +178,8 @@ public void findsPagedPersons() throws Exception { @Test public void executesPagedFinderCorrectly() throws Exception { - Page page = repository.findByLastnameLike("*a*", new PageRequest(0, 2, Direction.ASC, "lastname", - "firstname")); + Page page = repository.findByLastnameLike("*a*", + new PageRequest(0, 2, Direction.ASC, "lastname", "firstname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -192,8 +189,8 @@ public void executesPagedFinderCorrectly() throws Exception { @Test public void executesPagedFinderWithAnnotatedQueryCorrectly() throws Exception { - Page page = repository.findByLastnameLikeWithPageable(".*a.*", new PageRequest(0, 2, Direction.ASC, - "lastname", "firstname")); + Page page = repository.findByLastnameLikeWithPageable(".*a.*", + new PageRequest(0, 2, Direction.ASC, "lastname", "firstname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -317,8 +314,8 @@ public void findsPeopleByLocationWithinPolygon() { @Test public void findsPagedPeopleByPredicate() throws Exception { - Page page = repository.findAll(person.lastname.contains("a"), new PageRequest(0, 2, Direction.ASC, - "lastname")); + Page page = repository.findAll(person.lastname.contains("a"), + new PageRequest(0, 2, Direction.ASC, "lastname")); assertThat(page.isFirst(), is(true)); assertThat(page.isLast(), is(false)); assertThat(page.getNumberOfElements(), is(2)); @@ -404,8 +401,8 @@ public void executesGeoNearQueryForResultsCorrectly() { dave.setLocation(point); repository.save(dave); - GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS)); + GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS)); assertThat(results.getContent().isEmpty(), is(false)); } @@ -416,8 +413,8 @@ public void executesGeoPageQueryForResultsCorrectly() { dave.setLocation(point); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS), new PageRequest(0, 20)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 20)); assertThat(results.getContent().isEmpty(), is(false)); // DATAMONGO-607 @@ -627,8 +624,8 @@ public void executesGeoPageQueryForWithPageRequestForPageInBetween() { repository.save(Arrays.asList(dave, oliver, carter, boyd, leroi)); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(2)); @@ -652,8 +649,8 @@ public void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() { repository.save(Arrays.asList(dave, oliver, carter)); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(1)); assertThat(results.isFirst(), is(false)); @@ -671,8 +668,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElement() { dave.setLocation(point); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS), new PageRequest(0, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS), new PageRequest(0, 2)); assertThat(results.getContent().isEmpty(), is(false)); assertThat(results.getNumberOfElements(), is(1)); @@ -690,8 +687,8 @@ public void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() { dave.setLocation(new Point(-73.99171, 40.738868)); repository.save(dave); - GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), new Distance(2000, - Metrics.KILOMETERS), new PageRequest(1, 2)); + GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), + new Distance(2000, Metrics.KILOMETERS), new PageRequest(1, 2)); assertThat(results.getContent().isEmpty(), is(true)); assertThat(results.getNumberOfElements(), is(0)); @@ -941,8 +938,8 @@ public void findByCustomQueryLastnameAndStreetInList() { @Test public void shouldLimitCollectionQueryToMaxResultsWhenPresent() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", - "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), + new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); List result = repository.findTop3ByLastnameStartingWith("Dylan"); assertThat(result.size(), is(3)); } @@ -953,8 +950,8 @@ public void shouldLimitCollectionQueryToMaxResultsWhenPresent() { @Test public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", - "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), + new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(0, 2)); assertThat(result.getContent().size(), is(2)); } @@ -965,8 +962,8 @@ public void shouldNotLimitPagedQueryWhenPageRequestWithinBounds() { @Test public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", - "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), + new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(1, 2)); assertThat(result.getContent().size(), is(1)); } @@ -977,8 +974,8 @@ public void shouldLimitPagedQueryWhenPageRequestExceedsUpperBoundary() { @Test public void shouldReturnEmptyWhenPageRequestedPageIsTotallyOutOfScopeForLimit() { - repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), new Person("Bob-3", - "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); + repository.save(Arrays.asList(new Person("Bob-1", "Dylan"), new Person("Bob-2", "Dylan"), + new Person("Bob-3", "Dylan"), new Person("Bob-4", "Dylan"), new Person("Bob-5", "Dylan"))); Page result = repository.findTop3ByLastnameStartingWith("Dylan", new PageRequest(2, 2)); assertThat(result.getContent().size(), is(0)); } @@ -998,7 +995,7 @@ public void gettingNonFirstPageWorksWithoutLimitBeingSet() { /** * Ignored for now as this requires Querydsl 3.4.1 to succeed. - * + * * @see DATAMONGO-972 */ @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java index 1a7383601d..1d15730afc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Contact.java @@ -27,7 +27,8 @@ @Document public abstract class Contact { - @Id protected String id; + @Id + protected final String id; public Contact() { this.id = new ObjectId().toString(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java index 69724fb89b..224b85a53b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ContactRepositoryIntegrationTests.java @@ -54,6 +54,9 @@ public void readsAndWritesContactCorrectly() { assertTrue(repository.findOne(result.getId().toString()) instanceof Person); } + /** + * @see DATAMONGO-1245 + */ @Test public void findsContactByUntypedExample() { @@ -65,6 +68,9 @@ public void findsContactByUntypedExample() { assertThat(repository.findOne(example), instanceOf(Person.class)); } + /** + * @see DATAMONGO-1245 + */ @Test public void findsContactByTypedExample() { @@ -76,6 +82,9 @@ public void findsContactByTypedExample() { assertThat(repository.findOne(example), instanceOf(Person.class)); } + /** + * @see DATAMONGO-1245 + */ @Test public void findsNoContactByExample() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 0796ba2012..eae2c02e10 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -334,23 +334,22 @@ public interface PersonRepository extends MongoRepository, Query */ @Query("{ firstname : { $in : ?0 }}") Stream findByCustomQueryWithStreamingCursorByFirstnames(List firstnames); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : ?#{[0]}}") List findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }") List findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname); - + /** * @see DATAMONGO-990 */ @Query("{ firstname : :#{#firstname}}") List findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname); - } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 1be130373d..c13b27eb09 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -18,7 +18,6 @@ import java.util.Arrays; import java.util.Iterator; -import org.springframework.data.domain.Example; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; @@ -148,13 +147,4 @@ public Object[] getValues() { public Class getDynamicProjection() { return null; } - - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getSampleObject() - */ - @Override - public Example getSampleObject() { - return null; - } } diff --git a/src/main/asciidoc/reference/query-by-example.adoc b/src/main/asciidoc/reference/query-by-example.adoc index ea30113d72..d3c0f0b1a3 100644 --- a/src/main/asciidoc/reference/query-by-example.adoc +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -1,30 +1,11 @@ [[query.by.example.execution]] === Executing Example -In Spring Data MongoDB you can use Query by Example with the `MongoTemplate` and with Repositories. - -.Query by Example using MongoTemplate -==== -[source,xml] ----- -public class PersonService { - @Autowired MongoTemplate template; - - public List findPeople(Person probe) { - return template.findByExample(Example.of(probe)); - } -} ----- -==== - -`MongoTemplate.findByExample` accepts either the probe or an `Example` object to create and execute a query. An `Example` containing an untyped `ExampleSpec` uses the probe type to determine the collection name and the result type. Typed `ExampleSpec` use their type to determine collection name and result type. - - .Query by Example using a Repository ==== [source, java] ---- -public interface PersonRepository extends MongoRepository { +public interface PersonRepository extends QueryByExampleExecutor { }