diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 498f60fa3..3a7d44b6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -1014,6 +1014,10 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return removeTypeInfoRecursively(newDbo); } + if (typeInformation.getType().equals(NestedDocument.class)) { + return removeTypeInfo(newDbo); + } + return !obj.getClass().equals(typeInformation.getType()) ? newDbo : removeTypeInfoRecursively(newDbo); } @@ -1028,7 +1032,35 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } /** - * Removes the type information from the conversion result. + * Removes only the type information from the root document. + * + * @param object + * @return + */ + private Object removeTypeInfo(Object object) { + + if (!(object instanceof DBObject)) { + return object; + } + + DBObject dbObject = (DBObject) object; + String keyToRemove = null; + for (String key : dbObject.keySet()) { + + if (typeMapper.isTypeKey(key)) { + keyToRemove = key; + } + } + + if (keyToRemove != null) { + dbObject.removeField(keyToRemove); + } + + return dbObject; + } + + /** + * Removes the type information from the entire conversion result. * * @param object * @return @@ -1189,4 +1221,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App DBObject readRef(DBRef ref) { return ref.fetch(); } + + /** + * Marker class used to indicate we have a non root document object here that might be used within an update - so we + * need to preserve type hints for potential nested elements but need to remove it on top level. + * + * @author Christoph Strobl + * @since 1.8 + */ + static class NestedDocument { + + } } 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 661f6940b..af16170d7 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 @@ -816,7 +816,7 @@ public class QueryMapper { try { - PropertyPath path = PropertyPath.from(pathExpression, entity.getTypeInformation()); + PropertyPath path = PropertyPath.from(pathExpression.replaceAll("\\.\\d", ""), entity.getTypeInformation()); PersistentPropertyPath propertyPath = mappingContext.getPersistentPropertyPath(path); Iterator iterator = propertyPath.iterator(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index c9314b965..d3e89c6ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2014 the original author or authors. + * Copyright 2013-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. @@ -22,6 +22,7 @@ import java.util.Map.Entry; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty.PropertyToFieldNameConverter; @@ -29,6 +30,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update.Modifier; import org.springframework.data.mongodb.core.query.Update.Modifiers; import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; import com.mongodb.BasicDBObject; @@ -43,6 +45,7 @@ import com.mongodb.DBObject; */ public class UpdateMapper extends QueryMapper { + private static final ClassTypeInformation NESTED_DOCUMENT = ClassTypeInformation.from(NestedDocument.class); private final MongoConverter converter; /** @@ -66,7 +69,7 @@ public class UpdateMapper extends QueryMapper { @Override protected Object delegateConvertToMongoType(Object source, MongoPersistentEntity entity) { return entity == null ? super.delegateConvertToMongoType(source, null) : converter.convertToMongoType(source, - entity.getTypeInformation()); + getTypeHintForEntity(entity)); } /* @@ -97,14 +100,14 @@ public class UpdateMapper extends QueryMapper { if (rawValue instanceof Modifier) { - value = getMappedValue((Modifier) rawValue); + value = getMappedValue(field, (Modifier) rawValue); } else if (rawValue instanceof Modifiers) { DBObject modificationOperations = new BasicDBObject(); for (Modifier modifier : ((Modifiers) rawValue).getModifiers()) { - modificationOperations.putAll(getMappedValue(modifier).toMap()); + modificationOperations.putAll(getMappedValue(field, modifier).toMap()); } value = modificationOperations; @@ -132,12 +135,40 @@ public class UpdateMapper extends QueryMapper { return value instanceof Query; } - private DBObject getMappedValue(Modifier modifier) { + private DBObject getMappedValue(Field field, Modifier modifier) { - Object value = converter.convertToMongoType(modifier.getValue(), ClassTypeInformation.OBJECT); + Object value = converter.convertToMongoType(modifier.getValue(), getTypeHintForField(field)); return new BasicDBObject(modifier.getKey(), value); } + private TypeInformation getTypeHintForField(Field field) { + + if (field == null || field.getProperty() == null) { + return ClassTypeInformation.OBJECT; + } + + if (field.getProperty().getActualType().isInterface() + || java.lang.reflect.Modifier.isAbstract(field.getProperty().getActualType().getModifiers())) { + return ClassTypeInformation.OBJECT; + } + + return NESTED_DOCUMENT; + } + + private TypeInformation getTypeHintForEntity(MongoPersistentEntity entity) { + return processTypeHintForNestedDocuments(entity.getTypeInformation()); + } + + private TypeInformation processTypeHintForNestedDocuments(TypeInformation info) { + + Class type = info.getActualType().getType(); + if (type.isInterface() || java.lang.reflect.Modifier.isAbstract(type.getModifiers())) { + return info; + } + return NESTED_DOCUMENT; + + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.QueryMapper#createPropertyField(org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.String, org.springframework.data.mapping.context.MappingContext) @@ -233,7 +264,35 @@ public class UpdateMapper extends QueryMapper { protected String mapPropertyName(MongoPersistentProperty property) { String mappedName = PropertyToFieldNameConverter.INSTANCE.convert(property); - return iterator.hasNext() && iterator.next().equals("$") ? String.format("%s.$", mappedName) : mappedName; + + boolean inspect = iterator.hasNext(); + while (inspect) { + + String partial = iterator.next(); + + boolean isPositional = isPositionalParameter(partial); + if (isPositional) { + mappedName += "." + partial; + } + + inspect = isPositional && iterator.hasNext(); + } + + return mappedName; + } + + boolean isPositionalParameter(String partial) { + + if (partial.equals("$")) { + return true; + } + + try { + Long.valueOf(partial); + return true; + } catch (NumberFormatException e) { + return false; + } } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index b341345c9..a4083b46c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -193,6 +193,7 @@ public class MongoTemplateTests { template.dropCollection(SomeContent.class); template.dropCollection(SomeTemplate.class); template.dropCollection(Address.class); + template.dropCollection(DocumentWithCollectionOfSamples.class); } @Test @@ -2226,11 +2227,12 @@ public class MongoTemplateTests { assertThat(retrieved.model.value(), equalTo("value2")); } - // Rewrite the whole collection - // Passes in 1.6.0+, but changes order position of type hint _class + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnDocumentWithNestedCollection() - { + public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnDocumentWithNestedCollectionWhenWholeCollectionIsReplaced() { + DocumentWithNestedCollection doc = new DocumentWithNestedCollection(); Map entry = new HashMap(); @@ -2262,12 +2264,12 @@ public class MongoTemplateTests { assertThat(retrieved.models.get(0).get("key2").value(), equalTo("value2")); } - - // Update first list element - // Fails in 1.6.2+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnDocumentWithNestedCollection2() - { + public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnDocumentWithNestedCollectionWhenFirstElementIsReplaced() { + DocumentWithNestedCollection doc = new DocumentWithNestedCollection(); Map entry = new HashMap(); @@ -2299,11 +2301,12 @@ public class MongoTemplateTests { assertThat(retrieved.models.get(0).get("key2").value(), equalTo("value2")); } - // Add second list element - // Fails in 1.6.2+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldAddTypeInformationOnDocumentWithNestedCollection2() - { + public void findAndModifyShouldAddTypeInformationOnDocumentWithNestedCollectionObjectInsertedAtSecondIndex() { + DocumentWithNestedCollection doc = new DocumentWithNestedCollection(); Map entry = new HashMap(); @@ -2334,15 +2337,18 @@ public class MongoTemplateTests { assertThat(retrieved.models.get(1).get("key2").value(), equalTo("value2")); } - // Update the collection of the embedded document - // Fails in 1.6.0+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollection() throws Exception - { + public void findAndModifyShouldRetainTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollectionWhenUpdatingPositionedElement() + throws Exception { + List models = new ArrayList(); models.add(new ModelA("value1")); - DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection(new DocumentWithCollection(models)); + DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection( + new DocumentWithCollection(models)); template.save(doc); @@ -2353,22 +2359,26 @@ public class MongoTemplateTests { template.findAndModify(query, update, DocumentWithEmbeddedDocumentWithCollection.class); - DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, DocumentWithEmbeddedDocumentWithCollection.class); + DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, + DocumentWithEmbeddedDocumentWithCollection.class); assertThat(retrieved, notNullValue()); assertThat(retrieved.embeddedDocument.models, hasSize(1)); assertThat(retrieved.embeddedDocument.models.get(0).value(), is("value2")); } - // Update the collection of the embedded document - // Fails in 1.6.0+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollection2() throws Exception - { + public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollectionWhenUpdatingSecondElement() + throws Exception { + List models = new ArrayList(); models.add(new ModelA("value1")); - DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection(new DocumentWithCollection(models)); + DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection( + new DocumentWithCollection(models)); template.save(doc); @@ -2379,7 +2389,8 @@ public class MongoTemplateTests { template.findAndModify(query, update, DocumentWithEmbeddedDocumentWithCollection.class); - DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, DocumentWithEmbeddedDocumentWithCollection.class); + DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, + DocumentWithEmbeddedDocumentWithCollection.class); assertThat(retrieved, notNullValue()); assertThat(retrieved.embeddedDocument.models, hasSize(2)); @@ -2387,35 +2398,42 @@ public class MongoTemplateTests { assertThat(retrieved.embeddedDocument.models.get(1).value(), is("value2")); } - // Rewrite the embedded document - // Fails in 1.6.0+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollection3() throws Exception - { - List models = Arrays.asList(new ModelA("value1")); + public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnEmbeddedDocumentWithCollectionWhenRewriting() + throws Exception { - DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection(new DocumentWithCollection(models)); + List models = Arrays. asList(new ModelA("value1")); + + DocumentWithEmbeddedDocumentWithCollection doc = new DocumentWithEmbeddedDocumentWithCollection( + new DocumentWithCollection(models)); template.save(doc); Query query = query(where("id").is(doc.id)); - Update update = Update.update("embeddedDocument", new DocumentWithCollection(Arrays.asList(new ModelA("value2")))); + Update update = Update.update("embeddedDocument", + new DocumentWithCollection(Arrays. asList(new ModelA("value2")))); assertThat(template.findOne(query, DocumentWithEmbeddedDocumentWithCollection.class), notNullValue()); template.findAndModify(query, update, DocumentWithEmbeddedDocumentWithCollection.class); - DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, DocumentWithEmbeddedDocumentWithCollection.class); + DocumentWithEmbeddedDocumentWithCollection retrieved = template.findOne(query, + DocumentWithEmbeddedDocumentWithCollection.class); assertThat(retrieved, notNullValue()); assertThat(retrieved.embeddedDocument.models, hasSize(1)); assertThat(retrieved.embeddedDocument.models.get(0).value(), is("value2")); } - // Fails in 1.6.0+ + /** + * @see DATAMONGO-1210 + */ @Test - public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnDocumentWithNestedLists() - { + public void findAndModifyShouldAddTypeInformationWithinUpdatedTypeOnDocumentWithNestedLists() { + DocumentWithNestedList doc = new DocumentWithNestedList(); List entry = new ArrayList(); @@ -2851,8 +2869,12 @@ public class MongoTemplateTests { assertThat(template.findOne(query, DocumentWithCollectionOfSimpleType.class).values, hasSize(3)); } + /** + * @see DATAMONGO-1210 + */ @Test public void findAndModifyAddToSetWithEachShouldNotAddDuplicatesNorTypeHintForSimpleDocuments() { + DocumentWithCollectionOfSamples doc = new DocumentWithCollectionOfSamples(); doc.samples = Arrays.asList(new Sample(null, "sample1")); @@ -2862,7 +2884,7 @@ public class MongoTemplateTests { assertThat(template.findOne(query, DocumentWithCollectionOfSamples.class), notNullValue()); - Update update = new Update().addToSet("samples").each(Arrays.asList(new Sample(null, "sample2"), new Sample(null, "sample1"))); + Update update = new Update().addToSet("samples").each(new Sample(null, "sample2"), new Sample(null, "sample1")); template.findAndModify(query, update, DocumentWithCollectionOfSamples.class); @@ -3080,8 +3102,7 @@ public class MongoTemplateTests { List values; } - static class DocumentWithCollectionOfSamples - { + static class DocumentWithCollectionOfSamples { @Id String id; List samples; } @@ -3092,25 +3113,21 @@ public class MongoTemplateTests { List string2; } - static class DocumentWithNestedCollection - { + static class DocumentWithNestedCollection { @Id String id; List> models = new ArrayList>(); } - static class DocumentWithNestedList - { + static class DocumentWithNestedList { @Id String id; List> models = new ArrayList>(); - } + } - static class DocumentWithEmbeddedDocumentWithCollection - { + static class DocumentWithEmbeddedDocumentWithCollection { @Id String id; DocumentWithCollection embeddedDocument; - DocumentWithEmbeddedDocumentWithCollection(DocumentWithCollection embeddedDocument) - { + DocumentWithEmbeddedDocumentWithCollection(DocumentWithCollection embeddedDocument) { this.embeddedDocument = embeddedDocument; } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index 94af26d3d..a80ea9570 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -20,8 +20,10 @@ import static org.hamcrest.collection.IsMapContaining.*; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.DBObjectTestUtils.*; +import static org.springframework.data.mongodb.test.util.IsBsonObject.*; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.hamcrest.Matcher; @@ -454,7 +456,6 @@ public class UpdateMapperUnitTests { assertThat(((DBObject) updateValue).get("_class").toString(), equalTo("org.springframework.data.mongodb.core.convert.UpdateMapperUnitTests$ModelImpl")); } - } /** @@ -525,6 +526,109 @@ public class UpdateMapperUnitTests { assertThat($unset, equalTo(new BasicDBObjectBuilder().add("dbRefAnnotatedList.$", 1).get())); } + /** + * @see DATAMONGO-1210 + */ + @Test + public void mappingEachOperatorShouldNotAddTypeInfoForNonInterfaceNonAbstractTypes() { + + Update update = new Update().addToSet("nestedDocs").each(new NestedDocument("nested-1"), + new NestedDocument("nested-2")); + + DBObject mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(DocumentWithNestedCollection.class)); + + assertThat(mappedUpdate, isBsonObject().notContaining("$addToSet.nestedDocs.$each.[0]._class")); + assertThat(mappedUpdate, isBsonObject().notContaining("$addToSet.nestedDocs.$each.[1]._class")); + } + + /** + * @see DATAMONGO-1210 + */ + @Test + public void mappingEachOperatorShouldAddTypeHintForInterfaceTypes() { + + Update update = new Update().addToSet("models").each(new ModelImpl(1), new ModelImpl(2)); + + DBObject mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(ListModelWrapper.class)); + + assertThat(mappedUpdate, isBsonObject().containing("$addToSet.models.$each.[0]._class", ModelImpl.class.getName())); + assertThat(mappedUpdate, isBsonObject().containing("$addToSet.models.$each.[1]._class", ModelImpl.class.getName())); + } + + /** + * @see DATAMONGO-1210 + */ + @Test + public void mappingEachOperatorShouldAddTypeHintForAbstractTypes() { + + Update update = new Update().addToSet("list").each(new ConcreteChildClass("foo", "one"), + new ConcreteChildClass("bar", "two")); + + DBObject mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(ParentClass.class)); + + assertThat(mappedUpdate, + isBsonObject().containing("$addToSet.aliased.$each.[0]._class", ConcreteChildClass.class.getName())); + assertThat(mappedUpdate, + isBsonObject().containing("$addToSet.aliased.$each.[1]._class", ConcreteChildClass.class.getName())); + } + + /** + * @see DATAMONGO-1210 + */ + @Test + public void mappingShouldOnlyRemoveTypeHintFromTopLevelTypeInCaseOfNestedDocument() { + + WrapperAroundInterfaceType wait = new WrapperAroundInterfaceType(); + wait.interfaceType = new ModelImpl(1); + + Update update = new Update().addToSet("listHoldingConcretyTypeWithInterfaceTypeAttribute").each(wait); + DBObject mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(DomainTypeWithListOfConcreteTypesHavingSingleInterfaceTypeAttribute.class)); + + assertThat(mappedUpdate, + isBsonObject().notContaining("$addToSet.listHoldingConcretyTypeWithInterfaceTypeAttribute.$each.[0]._class")); + assertThat( + mappedUpdate, + isBsonObject().containing( + "$addToSet.listHoldingConcretyTypeWithInterfaceTypeAttribute.$each.[0].interfaceType._class", + ModelImpl.class.getName())); + } + + /** + * @see DATAMONGO-1210 + */ + @Test + public void mappingShouldRetainTypeInformationOfNestedListWhenUpdatingConcreteyParentType() { + + ListModelWrapper lmw = new ListModelWrapper(); + lmw.models = Collections. singletonList(new ModelImpl(1)); + + Update update = new Update().set("concreteTypeWithListAttributeOfInterfaceType", lmw); + DBObject mappedUpdate = mapper.getMappedObject(update.getUpdateObject(), + context.getPersistentEntity(DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes.class)); + + assertThat(mappedUpdate, isBsonObject().notContaining("$set.concreteTypeWithListAttributeOfInterfaceType._class")); + assertThat( + mappedUpdate, + isBsonObject().containing("$set.concreteTypeWithListAttributeOfInterfaceType.models.[0]._class", + ModelImpl.class.getName())); + } + + static class DomainTypeWrappingConcreteyTypeHavingListOfInterfaceTypeAttributes { + ListModelWrapper concreteTypeWithListAttributeOfInterfaceType; + } + + static class DomainTypeWithListOfConcreteTypesHavingSingleInterfaceTypeAttribute { + List listHoldingConcretyTypeWithInterfaceTypeAttribute; + } + + static class WrapperAroundInterfaceType { + Model interfaceType; + } + @org.springframework.data.mongodb.core.mapping.Document(collection = "DocumentWithReferenceToInterface") static interface DocumentWithReferenceToInterface { @@ -658,6 +762,10 @@ public class UpdateMapperUnitTests { static class DomainEntity { List collectionOfNestedEntities; + + public List getCollectionOfNestedEntities() { + return collectionOfNestedEntities; + } } static class NestedEntity { @@ -700,4 +808,19 @@ public class UpdateMapperUnitTests { @Field("mapped") DocumentWithDBRefCollection nested; } + + static class DocumentWithNestedCollection { + List nestedDocs; + } + + static class NestedDocument { + String name; + + public NestedDocument(String name) { + super(); + this.name = name; + } + + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java new file mode 100644 index 000000000..0657dd131 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/IsBsonObject.java @@ -0,0 +1,193 @@ +/* + * 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.test.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import org.bson.BSONObject; +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.hamcrest.core.IsEqual; +import org.springframework.data.mongodb.core.query.SerializationUtils; +import org.springframework.util.ClassUtils; + +import com.mongodb.DBObject; + +/** + * @author Christoph Strobl + * @param + */ +public class IsBsonObject extends TypeSafeMatcher { + + private List expectations = new ArrayList();; + + public static IsBsonObject isBsonObject() { + return new IsBsonObject(); + } + + @Override + protected void describeMismatchSafely(T item, Description mismatchDescription) { + mismatchDescription.appendText("was ").appendValue(SerializationUtils.serializeToJsonSafely(item)); + } + + @Override + public void describeTo(Description description) { + + for (ExpectedBsonContent expectation : expectations) { + + if (expectation.not) { + description.appendText(String.format("Path %s should not be present.", expectation.path)); + } else if (expectation.value == null) { + description.appendText(String.format("Expected to find path %s.", expectation.path)); + } else { + description.appendText(String.format("Expected to find %s for path %s.", expectation.value, expectation.path)); + } + } + + } + + @Override + protected boolean matchesSafely(T item) { + + if (expectations.isEmpty()) { + return true; + } + + for (ExpectedBsonContent expectation : expectations) { + + Object o = getValue(item, expectation.path); + + if (o == null && expectation.not) { + return true; + } + + if (o == null) { + return false; + } + + if (expectation.type != null && !ClassUtils.isAssignable(expectation.type, o.getClass())) { + return false; + } + + if (expectation.value != null && !new IsEqual(expectation.value).matches(o)) { + return false; + } + + if (o != null && expectation.not) { + return false; + } + + } + return true; + } + + public IsBsonObject containing(String key) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject containing(String key, Class type) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.type = type; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject containing(String key, Object value) { + + if (value == null) { + return notContaining(key); + } + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.type = ClassUtils.getUserClass(value); + expected.value = value; + + this.expectations.add(expected); + return this; + } + + public IsBsonObject notContaining(String key) { + + ExpectedBsonContent expected = new ExpectedBsonContent(); + expected.path = key; + expected.not = true; + + this.expectations.add(expected); + return this; + } + + static class ExpectedBsonContent { + String path; + Class type; + Object value; + boolean not = false; + } + + Object getValue(BSONObject source, String path) { + + String[] fragments = path.split("\\."); + if (fragments.length == 1) { + return source.get(path); + } + + Iterator it = Arrays.asList(fragments).iterator(); + + Object current = source; + while (it.hasNext()) { + + String key = it.next(); + + if (!(current instanceof BSONObject) && !key.startsWith("[")) { + return null; + } + + if (key.startsWith("[")) { + String indexNumber = key.substring(1, key.indexOf("]")); + if (current instanceof List) { + current = ((List) current).get(Integer.valueOf(indexNumber)); + } + if (!it.hasNext()) { + return current; + } + } else { + + if (current instanceof DBObject) { + current = ((DBObject) current).get(key); + } + + if (!it.hasNext()) { + return current; + } + + } + } + + throw new NoSuchElementException(String.format("Unable to find '%s' in %s.", path, source)); + } +}