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 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/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java new file mode 100644 index 0000000000..7463921fe5 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -0,0 +1,247 @@ +/* + * 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. + * 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.Collections; +import java.util.HashMap; +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; +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; + +/** + * @author Christoph Strobl + * @author Mark Paluch + * @since 1.8 + */ +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); + } + + /** + * 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()); + } + + ExampleSpecAccessor exampleSpecAccessor = new ExampleSpecAccessor(example.getExampleSpec()); + + applyPropertySpecs("", reference, example.getProbeType(), exampleSpecAccessor); + + if (exampleSpecAccessor.isTyped()) { + this.converter.getTypeMapper().writeTypeRestrictions(reference, (Set) Collections.singleton(example.getResultType())); + } + + return ObjectUtils.nullSafeEquals(NullHandler.INCLUDE, exampleSpecAccessor.getNullHandler()) ? reference + : new BasicDBObject(SerializationUtils.flattenMap(reference)); + } + + private String getMappedPropertyPath(String path, Class probeType) { + + MongoPersistentEntity entity = mappingContext.getPersistentEntity(probeType); + + 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()) { + return ""; + } + 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, Class probeType, + ExampleSpecAccessor exampleSpecAccessor) { + + if (!(source instanceof BasicDBObject)) { + return; + } + + Iterator> iter = ((BasicDBObject) source).entrySet().iterator(); + + while (iter.hasNext()) { + + Map.Entry entry = iter.next(); + String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); + String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); + + if(isEmptyIdProperty(entry)) { + iter.remove(); + continue; + } + + if (exampleSpecAccessor.isIgnoredPath(propertyPath) || exampleSpecAccessor.isIgnoredPath(mappedPropertyPath)) { + iter.remove(); + continue; + } + + StringMatcher stringMatcher = exampleSpecAccessor.getDefaultStringMatcher(); + Object value = entry.getValue(); + boolean ignoreCase = exampleSpecAccessor.isIgnoreCaseEnabled(); + + if (exampleSpecAccessor.hasPropertySpecifiers()) { + + mappedPropertyPath = exampleSpecAccessor.hasPropertySpecifier(propertyPath) ? propertyPath + : getMappedPropertyPath(propertyPath, probeType); + + stringMatcher = exampleSpecAccessor.getStringMatcherForPath(mappedPropertyPath); + ignoreCase = exampleSpecAccessor.isIgnoreCaseForPath(mappedPropertyPath); + } + + // TODO: should a PropertySpecifier outrule the later on string matching? + if (exampleSpecAccessor.hasPropertySpecifier(mappedPropertyPath)) { + + PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath); + value = valueTransformer.convert(value); + if (value == null) { + iter.remove(); + continue; + } + + entry.setValue(value); + } + + if (entry.getValue() instanceof String) { + applyStringMatcher(entry, stringMatcher, ignoreCase); + } else if (entry.getValue() instanceof BasicDBObject) { + applyPropertySpecs(propertyPath, (BasicDBObject) entry.getValue(), probeType, exampleSpecAccessor); + } + } + } + + 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(); + + if (ObjectUtils.nullSafeEquals(StringMatcher.DEFAULT, stringMatcher)) { + + if (ignoreCase) { + dbo.put("$regex", Pattern.quote((String) entry.getValue())); + entry.setValue(dbo); + } + } else { + + Type type = stringMatcherPartMapping.get(stringMatcher); + String expression = MongoRegexCreator.INSTANCE.toRegularExpression((String) entry.getValue(), type); + dbo.put("$regex", expression); + entry.setValue(dbo); + } + + if (ignoreCase) { + 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 490325461b..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 @@ -27,6 +27,7 @@ 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.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PropertyPath; @@ -70,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}. @@ -83,6 +85,7 @@ public QueryMapper(MongoConverter converter) { this.conversionService = converter.getConversionService(); this.converter = converter; this.mappingContext = converter.getMappingContext(); + this.exampleMapper = new MongoExampleMapper(converter); } /** @@ -239,6 +242,10 @@ protected DBObject getMappedKeyword(Keyword keyword, MongoPersistentEntity en return new BasicDBObject(keyword.getKey(), newConditions); } + if (keyword.isSample()) { + return exampleMapper.getMappedExample(keyword.> getValue(), entity); + } + return new BasicDBObject(keyword.getKey(), convertSimpleOrDBObject(keyword.getValue(), entity)); } @@ -566,6 +573,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..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 @@ -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(Example.of(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/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java new file mode 100644 index 0000000000..a1d85dff99 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java @@ -0,0 +1,106 @@ +/* + * 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. + * 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 + * @author Mark Paluch + * @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.REGEX, type)) { + return source; + } + + 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/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java index b11948220c..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 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. @@ -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 flattenMap(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..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 @@ -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. @@ -18,19 +18,23 @@ import java.io.Serializable; import java.util.List; +import org.springframework.data.domain.Example; 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) @@ -54,7 +58,7 @@ public interface MongoRepository extends PagingAndSo * 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 @@ -65,10 +69,21 @@ 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); + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example) + */ + List findAll(Example example); + + /* (non-Javadoc) + * @see org.springframework.data.repository.query.QueryByExampleExecutor#findAll(org.springframework.data.domain.Example, org.springframework.data.domain.Sort) + */ + List findAll(Example example, Sort sort); + } 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..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 @@ -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. @@ -390,61 +390,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/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..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 @@ -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. @@ -25,10 +25,12 @@ 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; 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; @@ -43,6 +45,7 @@ * @author Oliver Gierke * @author Christoph Strobl * @author Thomas Darimont + * @author Mark Paluch */ public class SimpleMongoRepository implements MongoRepository { @@ -53,7 +56,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 template must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. */ public SimpleMongoRepository(MongoEntityInformation metadata, MongoOperations mongoOperations) { @@ -259,6 +262,100 @@ public List insert(Iterable entities) { return list; } + /* + * (non-Javadoc) + * @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) { + + Assert.notNull(example, "Sample must not be null!"); + + Query q = new Query(new Criteria().alike(example)).with(pageable); + + 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), 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 findAll(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 mongoOperations.find(q, getResultType(example), entityInformation.getCollectionName()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.repository.MongoRepository#findAllByExample(org.springframework.data.domain.Example) + */ + @Override + 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), 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), 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), entityInformation.getCollectionName()); + } + + private Class getResultType(Example example) { + + if (example.getExampleSpec() instanceof TypedExampleSpec) { + return example.getResultType(); + } + + return (Class) entityInformation.getJavaType(); + } + private List findAll(Query query) { if (query == null) { @@ -291,4 +388,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..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 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. @@ -20,18 +20,22 @@ import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import java.util.Arrays; +import java.util.Map; import org.hamcrest.Matcher; 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 +64,74 @@ public void writesCollection() { assertThat(serializeToJsonSafely(dbObject), is(expectedOutput)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void flattenMapShouldFlatOutNestedStructureCorrectly() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("value", "conflux")).get(); + + assertThat(flattenMap(dbo), hasEntry("_id", (Object) 1)); + assertThat(flattenMap(dbo), hasEntry("nested.value", (Object) "conflux")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + 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(flattenMap(dbo), hasEntry("_id", (Object) 1)); + assertThat(flattenMap(dbo), hasEntry("nested.value", (Object) dbl)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flattenMapShouldLeaveKeywordsUntouched() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1).add("nested", new BasicDBObject("$regex", "^conflux$")) + .get(); + + Map map = flattenMap(dbo); + + assertThat(map, hasEntry("_id", (Object) 1)); + assertThat(map.get("nested"), notNullValue()); + assertThat(((Map) map.get("nested")).get("$regex"), is((Object) "^conflux$")); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void flattenMapShouldAppendCommandsCorrectly() { + + DBObject dbo = new BasicDBObjectBuilder().add("_id", 1) + .add("nested", new BasicDBObjectBuilder().add("$regex", "^conflux$").add("$options", "i").get()).get(); + + Map map = flattenMap(dbo); + + assertThat(map, hasEntry("_id", (Object) 1)); + assertThat(map.get("nested"), notNullValue()); + 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 flattenMapShouldReturnEmptyMapWhenSourceIsNull() { + assertThat(flattenMap(null).isEmpty(), is(true)); + } + static class Complex { } 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..ba01bacaa0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoExampleMapperUnitTests.java @@ -0,0 +1,499 @@ +/* + * 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. + * 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.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.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; +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 + * @author Mark Paluch + */ +@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(of(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(of(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(of(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(of(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(of(probe), context.getPersistentEntity(FlatDocument.class)); + + 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 + */ + @Test + public void exampleShouldBeMappedAsFlatMapWhenGivenNestedElementsWithLenientMatchMode() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.stringValue = "conflux"; + + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(WrapperDocument.class)); + + assertThat(dbo, is(new BasicDBObjectBuilder().add("flatDoc.stringValue", "conflux").get())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedAsExactObjectWhenGivenNestedElementsWithStrictMatchMode() { + + WrapperDocument probe = new WrapperDocument(); + probe.flatDoc = new FlatDocument(); + probe.flatDoc.stringValue = "conflux"; + + Example example = Example.of(probe, ExampleSpec.untyped().withIncludeNullValues()); + + 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 = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING)); + + 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 exampleShouldBeMappedCorrectlyForFlatTypeContainingDotsWhenStringMatchModeIsStarting() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "fire.ight"; + probe.intValue = 100; + + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcherStarting()); + + 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 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeIsEnding() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING)); + + 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 exampleShouldBeMappedCorrectlyForFlatTypeWhenStringMatchModeRegex() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.customNamedField = "^(cat|dog).*shelter\\d?"; + + Example example = Example.of(probe, ExampleSpec.untyped().withStringMatcher(StringMatcher.REGEX)); + + 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 + */ + @Test + public void exampleShouldBeMappedCorrectlyForFlatTypeWhenIgnoreCaseEnabledAndMatchModeSet() { + + FlatDocument probe = new FlatDocument(); + probe.stringValue = "firefight"; + probe.intValue = 100; + + Example example = Example.of(probe, + ExampleSpec.untyped().withStringMatcher(StringMatcher.ENDING).withIgnoreCase()); + + 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 = Example.of(probe, ExampleSpec.untyped().withIgnoreCase()); + + 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(of(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(of(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(of(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 = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("customNamedField")); + + 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 = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("stringValue")); + + 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 = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("flatDoc.stringValue")); + + 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 = Example.of(probe, ExampleSpec.untyped().withIgnorePaths("flatDoc.customNamedField")); + + 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 = Example.of(probe, + ExampleSpec.untyped().withMatcher("stringValue", new GenericPropertyMatcher().contains())); + + 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())); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void mappingShouldIncludePropertiesFromHierarchicalDocument() { + + HierachicalDocument probe = new HierachicalDocument(); + probe.stringValue = "firefight"; + probe.customNamedField = "steelheart"; + probe.anotherStringValue = "calamity"; + + DBObject dbo = mapper.getMappedExample(of(probe), context.getPersistentEntity(FlatDocument.class)); + + assertThat(dbo, isBsonObject().containing("anotherStringValue", "calamity")); + } + + 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 8cf5852b76..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 @@ -29,6 +29,7 @@ import java.util.Map; import org.bson.types.ObjectId; +import org.hamcrest.core.Is; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -791,6 +792,8 @@ public void intersectsShouldUseGeoJsonRepresentationCorrectly() { } /** + * <<<<<<< HEAD + * * @see DATAMONGO-1269 */ @Test @@ -818,6 +821,40 @@ public void mappingShouldRetainNumericPositionInList() { assertThat(dbo.containsField("list.1.stringProperty"), is(true)); } + /** + * @see DATAMONGO-1245 + */ + @Test + public void exampleShouldBeMappedCorrectly() { + + 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 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)); + } + @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..57a92e08c0 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/temp/QueryByExampleTests.java @@ -0,0 +1,189 @@ +/* + * 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. + * 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; + +/** + * @author Christoph Strobl + * @author Mark Paluch + */ +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"; + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); + 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"; + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); + 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; + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); + 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"; + + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); + Assert.assertThat(result.size(), Is.is(0)); + } + + /** + * @see DATAMONGO-1245 + */ + @Test + public void findByExampleShouldReturnEverythingWhenSampleIsEmpty() { + + init(); + + Person sample = new Person(); + + Query query = new Query(new Criteria().alike(Example.of(sample))); + + List result = template.find(query, Person.class); + 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(Example.of(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..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 @@ -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. @@ -34,6 +34,7 @@ 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 +56,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}. @@ -62,6 +64,7 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) public abstract class AbstractPersonRepositoryIntegrationTests { @@ -1221,4 +1224,41 @@ public void shouldFindByFirstnameForSpELExpressionWithParameterVariableOnly() { assertThat(users, hasSize(1)); 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.findAll(Example.of(sample), new PageRequest(0, 10)); + assertThat(result.getNumberOfElements(), 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.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..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 @@ -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,46 @@ public void readsAndWritesContactCorrectly() { assertTrue(repository.findOne(result.getId().toString()) instanceof Person); } + + /** + * @see DATAMONGO-1245 + */ + @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)); + } + + /** + * @see DATAMONGO-1245 + */ + @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)); + } + + /** + * @see DATAMONGO-1245 + */ + @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 5e9177ae39..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 @@ -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. @@ -31,16 +31,29 @@ 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.data.domain.ExampleSpec.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; +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 + * @author Mark Paluch */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:infrastructure.xml") @@ -120,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(); @@ -160,6 +173,274 @@ 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.findAll(Example.of(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.findAll(Example.of(sample)); + + assertThat(result, containsInAnyOrder(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.findAll(Example.of(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.findAll(Example.of(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"); + + Example example = Example.of(sample, ExampleSpec.typed(Person.class).withIncludeNullValues()); + List result = repository.findAll(example); + + 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"); + + Example example = Example.of(sample, ExampleSpec.untyped().withIncludeNullValues()); + List result = repository.findAll(example); + + 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"); + + Example example = Example.of(sample, ExampleSpec.untyped().withStringMatcher(StringMatcher.STARTING)); + List result = repository.findAll(example); + + 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.findAll(Example.of(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.findAll(Example.of(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.findAll(Example.of(sample)); + + assertThat(result, hasItem(megan)); + 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.findAll(Example.of(sample)); + + assertThat(result, containsInAnyOrder(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(Example.of(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(Example.of(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(Example.of(sample)); + + assertThat(result, is(equalTo(2L))); + } + + @Document(collection = "customizedPerson") + static class PersonExtended extends Person { + + } + private void assertThatAllReferencePersonsWereStoredCorrectly(Map references, List saved) { for (Person person : saved) { @@ -168,6 +449,13 @@ private void assertThatAllReferencePersonsWereStoredCorrectly(Map { @Override diff --git a/src/main/asciidoc/index.adoc b/src/main/asciidoc/index.adoc index 4ae939dba8..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: 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 new file mode 100644 index 0000000000..d3c0f0b1a3 --- /dev/null +++ b/src/main/asciidoc/reference/query-by-example.adoc @@ -0,0 +1,71 @@ +[[query.by.example.execution]] +=== Executing Example + +.Query by Example using a Repository +==== +[source, java] +---- +public interface PersonRepository extends QueryByExampleExecutor { + +} + +public class PersonService { + + @Autowired PersonRepository personRepository; + + public List findPeople(Person probe) { + return personRepository.findAll(Example.of(probe)); + } +} +---- +==== + +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: + +[cols="1,2", options="header"] +.`StringMatcher` options +|=== +| 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'}}` + +|===