From 1b83ff0382c9b8452ab3f92e28cbeebf67b70bf5 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 8 Oct 2013 12:23:24 +0200 Subject: [PATCH] DATAMONGO-766 - Support for nested field references through @Field. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The MappingMongoConverter now supports reading and writing nested fields from and to documents by using @Field(…) with a path expression, e.g. @Field("a.b"). We now correctly create the nested objects and also reuse the previously created intermediates when populating further properties. Not that this might cause the need to define field ordering explicitly as later properties might override the values set using a path reference in @Field. --- .../core/convert/DBObjectAccessor.java | 123 ++++++++++++++++++ .../core/convert/MappingMongoConverter.java | 31 +++-- .../convert/DBObjectAccessorUnitTests.java | 94 +++++++++++++ .../MappingMongoConverterUnitTests.java | 26 ++++ 4 files changed, 263 insertions(+), 11 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DBObjectAccessor.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DBObjectAccessorUnitTests.java 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;