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 8d9039441..493d0ddfd 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 @@ -48,16 +48,26 @@ import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** + * Mapper from {@link Example} to a query {@link Document}. + * * @author Christoph Strobl * @author Mark Paluch * @author Jens Schauder * @since 1.8 + * @see Example + * @see org.springframework.data.domain.ExampleMatcher + * @see UntypedExampleMatcher */ public class MongoExampleMapper { private final MappingContext, MongoPersistentProperty> mappingContext; private final MongoConverter converter; + /** + * Create a new {@link MongoTypeMapper} given {@link MongoConverter}. + * + * @param converter must not be {@literal null}. + */ public MongoExampleMapper(MongoConverter converter) { this.converter = converter; @@ -112,28 +122,63 @@ public class MongoExampleMapper { return updateTypeRestrictions(result, example); } - private static Document orConcatenate(Document source) { - - List foo = new ArrayList(source.keySet().size()); + private void applyPropertySpecs(String path, Document source, Class probeType, + ExampleMatcherAccessor exampleSpecAccessor) { - for (String key : source.keySet()) { - foo.add(new Document(key, source.get(key))); + if (source == null) { + return; } - return new Document("$or", foo); - } + Iterator> iter = source.entrySet().iterator(); - private Set> getTypesToMatch(Example example) { + while (iter.hasNext()) { - Set> types = new HashSet>(); + Map.Entry entry = iter.next(); + String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); + String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); - for (TypeInformation reference : mappingContext.getManagedTypes()) { - if (example.getProbeType().isAssignableFrom(reference.getType())) { - types.add(reference.getType()); + if (isEmptyIdProperty(entry)) { + iter.remove(); + continue; } - } - return types; + 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 Document) { + applyPropertySpecs(propertyPath, (Document) entry.getValue(), probeType, exampleSpecAccessor); + } + } } private String getMappedPropertyPath(String path, Class probeType) { @@ -142,9 +187,9 @@ public class MongoExampleMapper { Iterator parts = Arrays.asList(path.split("\\.")).iterator(); - final Stack stack = new Stack(); + final Stack stack = new Stack<>(); - List resultParts = new ArrayList(); + List resultParts = new ArrayList<>(); while (parts.hasNext()) { @@ -178,70 +223,64 @@ public class MongoExampleMapper { return StringUtils.collectionToDelimitedString(resultParts, "."); } - private void applyPropertySpecs(String path, Document source, Class probeType, - ExampleMatcherAccessor exampleSpecAccessor) { - - if (!(source instanceof Document)) { - return; - } + private Document updateTypeRestrictions(Document query, Example example) { - Iterator> iter = ((Document) source).entrySet().iterator(); + Document result = new Document(); - while (iter.hasNext()) { + if (isTypeRestricting(example)) { - Map.Entry entry = iter.next(); - String propertyPath = StringUtils.hasText(path) ? path + "." + entry.getKey() : entry.getKey(); - String mappedPropertyPath = getMappedPropertyPath(propertyPath, probeType); + result.putAll(query); + this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example)); + return result; + } - if (isEmptyIdProperty(entry)) { - iter.remove(); - continue; + for (Map.Entry entry : query.entrySet()) { + if (!this.converter.getTypeMapper().isTypeKey(entry.getKey())) { + result.put(entry.getKey(), entry.getValue()); } + } - if (exampleSpecAccessor.isIgnoredPath(propertyPath) || exampleSpecAccessor.isIgnoredPath(mappedPropertyPath)) { - iter.remove(); - continue; - } + return result; + } - StringMatcher stringMatcher = exampleSpecAccessor.getDefaultStringMatcher(); - Object value = entry.getValue(); - boolean ignoreCase = exampleSpecAccessor.isIgnoreCaseEnabled(); + private boolean isTypeRestricting(Example example) { - if (exampleSpecAccessor.hasPropertySpecifiers()) { + if (example.getMatcher() instanceof UntypedExampleMatcher) { + return false; + } - mappedPropertyPath = exampleSpecAccessor.hasPropertySpecifier(propertyPath) ? propertyPath - : getMappedPropertyPath(propertyPath, probeType); + if (example.getMatcher().getIgnoredPaths().isEmpty()) { + return true; + } - stringMatcher = exampleSpecAccessor.getStringMatcherForPath(mappedPropertyPath); - ignoreCase = exampleSpecAccessor.isIgnoreCaseForPath(mappedPropertyPath); + for (String path : example.getMatcher().getIgnoredPaths()) { + if (this.converter.getTypeMapper().isTypeKey(path)) { + return false; } + } - // TODO: should a PropertySpecifier outrule the later on string matching? - if (exampleSpecAccessor.hasPropertySpecifier(mappedPropertyPath)) { + return true; + } - PropertyValueTransformer valueTransformer = exampleSpecAccessor.getValueTransformerForPath(mappedPropertyPath); - value = valueTransformer.convert(value); - if (value == null) { - iter.remove(); - continue; - } + private Set> getTypesToMatch(Example example) { - entry.setValue(value); - } + Set> types = new HashSet<>(); - if (entry.getValue() instanceof String) { - applyStringMatcher(entry, stringMatcher, ignoreCase); - } else if (entry.getValue() instanceof Document) { - applyPropertySpecs(propertyPath, (Document) entry.getValue(), probeType, exampleSpecAccessor); + for (TypeInformation reference : mappingContext.getManagedTypes()) { + if (example.getProbeType().isAssignableFrom(reference.getType())) { + types.add(reference.getType()); } } + + return types; } - private boolean isEmptyIdProperty(Entry entry) { + private static boolean isEmptyIdProperty(Entry entry) { return entry.getKey().equals("_id") && entry.getValue() == null || entry.getValue().equals(Optional.empty()); } - private void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, boolean ignoreCase) { + private static void applyStringMatcher(Map.Entry entry, StringMatcher stringMatcher, + boolean ignoreCase) { Document document = new Document(); @@ -264,9 +303,20 @@ public class MongoExampleMapper { } } + private static Document orConcatenate(Document source) { + + List or = new ArrayList<>(source.keySet().size()); + + for (String key : source.keySet()) { + or.add(new Document(key, source.get(key))); + } + + return new Document("$or", or); + } + /** * Return the {@link MatchMode} for the given {@link StringMatcher}. - * + * * @param matcher must not be {@literal null}. * @return */ @@ -288,43 +338,4 @@ public class MongoExampleMapper { return MatchMode.DEFAULT; } } - - private Document updateTypeRestrictions(Document query, Example example) { - - Document result = new Document(); - - if (isTypeRestricting(example)) { - - result.putAll(query); - this.converter.getTypeMapper().writeTypeRestrictions(result, getTypesToMatch(example)); - return result; - } - - for (Map.Entry entry : query.entrySet()) { - if (!this.converter.getTypeMapper().isTypeKey(entry.getKey())) { - result.put(entry.getKey(), entry.getValue()); - } - } - - return result; - } - - private boolean isTypeRestricting(Example example) { - - if (example.getMatcher() instanceof UntypedExampleMatcher) { - return false; - } - - if (example.getMatcher().getIgnoredPaths().isEmpty()) { - return true; - } - - for (String path : example.getMatcher().getIgnoredPaths()) { - if (this.converter.getTypeMapper().isTypeKey(path)) { - return false; - } - } - - return true; - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java index 34a031441..893fff69c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java @@ -15,36 +15,29 @@ */ package org.springframework.data.mongodb.core.query; +import lombok.AccessLevel; import lombok.EqualsAndHashCode; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; import java.util.Set; import org.springframework.data.domain.ExampleMatcher; -import org.springframework.util.Assert; /** * {@link ExampleMatcher} implementation for query by example (QBE). Unlike plain {@link ExampleMatcher} this untyped - * counterpart does not enforce a strict type match when executing the query. This allows to use totally unrelated - * example documents as references for querying collections as long as the used field/property names match. + * counterpart does not enforce type matching when executing the query. This allows to query unrelated example documents + * as references for querying collections as long as the used field/property names match. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ @EqualsAndHashCode +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) public class UntypedExampleMatcher implements ExampleMatcher { - private final ExampleMatcher delegate; - - /** - * Creates new {@link UntypedExampleMatcher}. - * - * @param delegate must not be {@literal null}. - */ - private UntypedExampleMatcher(ExampleMatcher delegate) { - - Assert.notNull(delegate, "Delegate must not be null!"); - this.delegate = delegate; - } + private final @NonNull ExampleMatcher delegate; /* * (non-Javadoc) @@ -167,7 +160,7 @@ public class UntypedExampleMatcher implements ExampleMatcher { return delegate.getNullHandler(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.domain.ExampleMatcher#getDefaultStringMatcher() */ @@ -175,7 +168,7 @@ public class UntypedExampleMatcher implements ExampleMatcher { return delegate.getDefaultStringMatcher(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.domain.ExampleMatcher#isIgnoreCaseEnabled() */ 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 8e8ba379e..21249ab47 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 @@ -484,7 +484,7 @@ public class MongoExampleMapperUnitTests { } @Test // DATAMONGO-1768 - public void untypedExampleShouldNotInfereTypeRestriction() { + public void untypedExampleShouldNotInferTypeRestriction() { WrapperDocument probe = new WrapperDocument(); probe.flatDoc = new FlatDocument();