diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DBObjectAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DBObjectAccessor.java new file mode 100644 index 000000000..dd28c43be --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DBObjectAccessor.java @@ -0,0 +1,123 @@ +/* + * Copyright 2013 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.Arrays; +import java.util.Iterator; +import java.util.Map; + +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.util.Assert; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Wrapper value object for a {@link BasicDBObject} to be able to access raw values by {@link MongoPersistentProperty} + * references. The accessors will transparently resolve nested document values that a {@link MongoPersistentProperty} + * might refer to through a path expression in field names. + * + * @author Oliver Gierke + */ +class DBObjectAccessor { + + private final DBObject dbObject; + + /** + * Creates a new {@link DBObjectAccessor} for the given {@link DBObject}. + * + * @param dbObject must be a {@link BasicDBObject} effectively, must not be {@literal null}. + */ + public DBObjectAccessor(DBObject dbObject) { + + Assert.notNull(dbObject, "DBObject must not be null!"); + Assert.isInstanceOf(BasicDBObject.class, dbObject, "Given DBObject must be a BasicDBObject!"); + + this.dbObject = dbObject; + } + + /** + * Puts the given value into the backing {@link DBObject} based on the coordinates defined through the given + * {@link MongoPersistentProperty}. By default this will be the plain field name. But field names might also consist + * of path traversals so we might need to create intermediate {@link BasicDBObject}s. + * + * @param prop must not be {@literal null}. + * @param value + */ + public void put(MongoPersistentProperty prop, Object value) { + + Assert.notNull(prop, "MongoPersistentProperty must not be null!"); + String fieldName = prop.getFieldName(); + + Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator(); + DBObject dbObject = this.dbObject; + + while (parts.hasNext()) { + + String part = parts.next(); + + if (parts.hasNext()) { + BasicDBObject nestedDbObject = new BasicDBObject(); + dbObject.put(part, nestedDbObject); + dbObject = nestedDbObject; + } else { + dbObject.put(part, value); + } + } + } + + /** + * Returns the value the given {@link MongoPersistentProperty} refers to. By default this will be a direct field but + * the method will also transparently resolve nested values the {@link MongoPersistentProperty} might refer to through + * a path expression in the field name metadata. + * + * @param property must not be {@literal null}. + * @return + */ + @SuppressWarnings("unchecked") + public Object get(MongoPersistentProperty property) { + + String fieldName = property.getFieldName(); + Iterator parts = Arrays.asList(fieldName.split("\\.")).iterator(); + Map source = this.dbObject.toMap(); + Object result = null; + + while (source != null && parts.hasNext()) { + + result = source.get(parts.next()); + + if (parts.hasNext()) { + source = getAsMap(result); + } + } + + return result; + } + + @SuppressWarnings("unchecked") + private Map getAsMap(Object source) { + + if (source instanceof BasicDBObject) { + return ((DBObject) source).toMap(); + } + + if (source instanceof Map) { + return (Map) source; + } + + return null; + } +} 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 119aaabc4..a0911efce 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 @@ -385,7 +385,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App if (!conversions.isSimpleType(propertyObj.getClass())) { writePropertyInternal(propertyObj, dbo, prop); } else { - writeSimpleInternal(propertyObj, dbo, prop.getFieldName()); + writeSimpleInternal(propertyObj, dbo, prop); } } } @@ -410,26 +410,27 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return; } - String name = prop.getFieldName(); + DBObjectAccessor accessor = new DBObjectAccessor(dbo); + TypeInformation valueType = ClassTypeInformation.from(obj.getClass()); TypeInformation type = prop.getTypeInformation(); if (valueType.isCollectionLike()) { DBObject collectionInternal = createCollection(asCollection(obj), prop); - dbo.put(name, collectionInternal); + accessor.put(prop, collectionInternal); return; } if (valueType.isMap()) { DBObject mapDbObj = createMap((Map) obj, prop); - dbo.put(name, mapDbObj); + accessor.put(prop, mapDbObj); return; } if (prop.isDbReference()) { DBRef dbRefObj = createDBRef(obj, prop.getDBRef()); if (null != dbRefObj) { - dbo.put(name, dbRefObj); + accessor.put(prop, dbRefObj); return; } } @@ -438,18 +439,20 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Class basicTargetType = conversions.getCustomWriteTarget(obj.getClass(), null); if (basicTargetType != null) { - dbo.put(name, conversionService.convert(obj, basicTargetType)); + accessor.put(prop, conversionService.convert(obj, basicTargetType)); return; } - BasicDBObject propDbObj = new BasicDBObject(); + Object existingValue = accessor.get(prop); + BasicDBObject propDbObj = existingValue instanceof BasicDBObject ? (BasicDBObject) existingValue + : new BasicDBObject(); addCustomTypeKeyIfNecessary(type, obj, propDbObj); MongoPersistentEntity entity = isSubtype(prop.getType(), obj.getClass()) ? mappingContext .getPersistentEntity(obj.getClass()) : mappingContext.getPersistentEntity(type); writeInternal(obj, propDbObj, entity); - dbo.put(name, propDbObj); + accessor.put(prop, propDbObj); } private boolean isSubtype(Class left, Class right) { @@ -667,6 +670,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App dbObject.put(key, getPotentiallyConvertedSimpleWrite(value)); } + private void writeSimpleInternal(Object value, DBObject dbObject, MongoPersistentProperty property) { + DBObjectAccessor accessor = new DBObjectAccessor(dbObject); + accessor.put(property, getPotentiallyConvertedSimpleWrite(value)); + } + /** * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type. * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. @@ -965,7 +973,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App private class MongoDbPropertyValueProvider implements PropertyValueProvider { - private final DBObject source; + private final DBObjectAccessor source; private final SpELExpressionEvaluator evaluator; private final Object parent; @@ -978,7 +986,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Assert.notNull(source); Assert.notNull(evaluator); - this.source = source; + this.source = new DBObjectAccessor(source); this.evaluator = evaluator; this.parent = parent; } @@ -990,7 +998,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App public T getPropertyValue(MongoPersistentProperty property) { String expression = property.getSpelExpression(); - Object value = expression != null ? evaluator.evaluate(expression) : source.get(property.getFieldName()); + Object value = expression != null ? evaluator.evaluate(expression) : source.get(property); if (value == null) { return null; @@ -1052,4 +1060,5 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return (T) getPotentiallyConvertedSimpleRead(value, rawType); } } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java new file mode 100644 index 000000000..6501d4ff8 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2013 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.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.mapping.Field; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link DbObjectAccessor}. + * + * @see DATAMONGO-766 + * @author Oliver Gierke + */ +public class DBObjectAccessorUnitTests { + + MongoMappingContext context = new MongoMappingContext(); + MongoPersistentEntity projectingTypeEntity = context.getPersistentEntity(ProjectingType.class); + MongoPersistentProperty fooProperty = projectingTypeEntity.getPersistentProperty("foo"); + + @Test + public void putsNestedFieldCorrectly() { + + DBObject dbObject = new BasicDBObject(); + + DBObjectAccessor accessor = new DBObjectAccessor(dbObject); + accessor.put(fooProperty, "FooBar"); + + DBObject aDbObject = DBObjectUtils.getAsDBObject(dbObject, "a"); + assertThat(aDbObject.get("b"), is((Object) "FooBar")); + } + + @Test + public void getsNestedFieldCorrectly() { + + DBObject source = new BasicDBObject("a", new BasicDBObject("b", "FooBar")); + + DBObjectAccessor accessor = new DBObjectAccessor(source); + assertThat(accessor.get(fooProperty), is((Object) "FooBar")); + } + + @Test + public void returnsNullForNonExistingFieldPath() { + + DBObjectAccessor accessor = new DBObjectAccessor(new BasicDBObject()); + assertThat(accessor.get(fooProperty), is(nullValue())); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNonBasicDBObjects() { + new DBObjectAccessor(new BasicDBList()); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullDBObject() { + new DBObjectAccessor(null); + } + + static class ProjectingType { + + String name; + @Field("a.b") String foo; + NestedType a; + } + + static class NestedType { + String b; + String c; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 3282a0430..57f709d94 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -53,6 +53,9 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.model.MappingException; import org.springframework.data.mapping.model.MappingInstantiationException; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.data.mongodb.core.DBObjectUtils; +import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.NestedType; +import org.springframework.data.mongodb.core.convert.DBObjectAccessorUnitTests.ProjectingType; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -1425,6 +1428,29 @@ public class MappingMongoConverterUnitTests { assertThat(converter.read(String.class, dbObject), is("{ \"firstname\" : \"Dave\"}")); } + /** + * @see DATAMONGO-766 + */ + @Test + public void writesProjectingTypeCorrectly() { + + NestedType nested = new NestedType(); + nested.c = "C"; + + ProjectingType type = new ProjectingType(); + type.name = "name"; + type.foo = "bar"; + type.a = nested; + + BasicDBObject result = new BasicDBObject(); + converter.write(type, result); + + assertThat(result.get("name"), is((Object) "name")); + DBObject aValue = DBObjectUtils.getAsDBObject(result, "a"); + assertThat(aValue.get("b"), is((Object) "bar")); + assertThat(aValue.get("c"), is((Object) "C")); + } + @Document class MapDBRef { @org.springframework.data.mongodb.core.mapping.DBRef Map map;