DATAMONGO-379 - Improved entity instantiation.

Huge refactoring of the way MappingMongoConverter instantiates entities. The constructor arguments now have to mirror a property exactly in terms of name. Thus we can pick up mapping information from the property to lookup the correct value from the source document. The @Value annotation can be used to either inject completely arbitrary values into the instance (e.g. by referring to a Spring bean) or simply define an expression against DBObject's fields:

class Sample {
  String foo;
  String bar;

  Sample(String foo, @Value("#root._bar") String bar) {
    this.foo = foo;
    this.bar = bar;
  }
}

trying to create an instance of this class from

{ "foo" : "FOO" } -> new Sample("FOO", null)
{ "_bar" : "BAR" } -> new Sample(null, "BAR").
This commit is contained in:
Oliver Gierke
2012-01-16 18:19:15 +00:00
parent 3207a81555
commit 33dd00f0b8
8 changed files with 388 additions and 84 deletions

View File

@@ -0,0 +1,55 @@
/*
* Copyright 2012 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.Map;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import com.mongodb.DBObject;
/**
* {@link PropertyAccessor} to allow entity based field access to {@link DBObject}s.
*
* @author Oliver Gierke
*/
class DBObjectPropertyAccessor extends MapAccessor {
static MapAccessor INSTANCE = new DBObjectPropertyAccessor();
@Override
public Class<?>[] getSpecificTargetClasses() {
return new Class[] { DBObject.class };
}
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return true;
}
@Override
@SuppressWarnings("unchecked")
public TypedValue read(EvaluationContext context, Object target, String name) {
Map<String, Object> source = (Map<String, Object>) target;
Object value = source.get(name);
return value == null ? TypedValue.NULL : new TypedValue(value);
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright 2012 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.HashSet;
import java.util.Set;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PropertyPath;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
/**
* Abstraction for a {@link PreferredConstructor} alongside mapping information.
*
* @author Oliver Gierke
*/
class MappedConstructor {
private final Set<MappedConstructor.MappedParameter> parameters;
/**
* Creates a new {@link MappedConstructor} from the given {@link MongoPersistentEntity} and {@link MappingContext}.
*
* @param entity must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public MappedConstructor(MongoPersistentEntity<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
Assert.notNull(entity);
Assert.notNull(context);
this.parameters = new HashSet<MappedConstructor.MappedParameter>();
for (Parameter<?> parameter : entity.getPreferredConstructor().getParameters()) {
parameters.add(new MappedParameter(parameter, entity, context));
}
}
/**
* Returns whether the given {@link PersistentProperty} is referenced in a constructor argument of the
* {@link PersistentEntity} backing this {@link MappedConstructor}.
*
* @param property must not be {@literal null}.
* @return
*/
public boolean isConstructorParameter(PersistentProperty<?> property) {
Assert.notNull(property);
for (MappedConstructor.MappedParameter parameter : parameters) {
if (parameter.maps(property)) {
return true;
}
}
return false;
}
/**
* Returns the {@link MappedParameter} for the given {@link Parameter}.
*
* @param parameter must not be {@literal null}.
* @return
*/
public MappedParameter getFor(Parameter<?> parameter) {
for (MappedParameter mappedParameter : parameters) {
if (mappedParameter.parameter.equals(parameter)) {
return mappedParameter;
}
}
throw new IllegalStateException(String.format("Didn't find a MappedParameter for %s!", parameter.toString()));
}
/**
* Abstraction of a {@link Parameter} alongside mapping information.
*
* @author Oliver Gierke
*/
static class MappedParameter {
private final MongoPersistentProperty property;
private final Parameter<?> parameter;
/**
* Creates a new {@link MappedParameter} for the given {@link Parameter}, {@link MongoPersistentProperty} and
* {@link MappingContext}.
*
* @param parameter must not be {@literal null}.
* @param entity must not be {@literal null}.
* @param context must not be {@literal null}.
*/
public MappedParameter(Parameter<?> parameter, MongoPersistentEntity<?> entity,
MappingContext<? extends MongoPersistentEntity<?>, ? extends MongoPersistentProperty> context) {
Assert.notNull(parameter);
Assert.notNull(entity);
Assert.notNull(context);
this.parameter = parameter;
PropertyPath propertyPath = PropertyPath.from(parameter.getName(), entity.getType());
PersistentPropertyPath<? extends MongoPersistentProperty> path = context.getPersistentPropertyPath(propertyPath);
this.property = path == null ? null : path.getLeafProperty();
}
/**
* Returns whether there is a SpEL expression configured for this parameter.
*
* @return
*/
public boolean hasSpELExpression() {
return parameter.getKey() != null;
}
/**
* Returns the field name to be used to lookup the value which in turn shall be converted into the constructor
* parameter.
*
* @return
*/
public String getFieldName() {
return property.getFieldName();
}
/**
* Returns the type of the property backing the {@link Parameter}.
*
* @return
*/
public TypeInformation<?> getPropertyTypeInformation() {
return property.getTypeInformation();
}
/**
* Returns whether the given {@link PersistentProperty} is mapped by the parameter.
*
* @param property
* @return
*/
public boolean maps(PersistentProperty<?> property) {
return this.property.equals(property);
}
}
}

View File

@@ -37,7 +37,7 @@ import org.springframework.core.convert.support.ConversionServiceFactory;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PreferredConstructor.Parameter;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.model.BeanWrapper;
@@ -189,52 +189,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
private <S extends Object> S read(final MongoPersistentEntity<S> entity, final DBObject dbo) {
final StandardEvaluationContext spelCtx = new StandardEvaluationContext();
if (null != applicationContext) {
final StandardEvaluationContext spelCtx = new StandardEvaluationContext(dbo);
spelCtx.addPropertyAccessor(DBObjectPropertyAccessor.INSTANCE);
if (applicationContext != null) {
spelCtx.setBeanResolver(new BeanFactoryResolver(applicationContext));
}
if (!(dbo instanceof BasicDBList)) {
String[] keySet = dbo.keySet().toArray(new String[dbo.keySet().size()]);
for (String key : keySet) {
spelCtx.setVariable(key, dbo.get(key));
}
}
final List<String> ctorParamNames = new ArrayList<String>();
final MongoPersistentProperty idProperty = entity.getIdProperty();
final MappedConstructor constructor = new MappedConstructor(entity, mappingContext);
ParameterValueProvider provider = new SpELAwareParameterValueProvider(spelExpressionParser, spelCtx) {
@Override
@SuppressWarnings("unchecked")
public <T> T getParameterValue(PreferredConstructor.Parameter<T> parameter) {
if (parameter.getKey() != null) {
return super.getParameterValue(parameter);
}
String name = parameter.getName();
TypeInformation<T> type = parameter.getType();
Class<T> rawType = parameter.getRawType();
String key = idProperty == null ? name : idProperty.getName().equals(name) ? idProperty.getFieldName() : name;
Object obj = dbo.get(key);
ctorParamNames.add(name);
if (obj instanceof DBRef) {
return read(type, ((DBRef) obj).fetch());
} else if (obj instanceof BasicDBList) {
BasicDBList objAsDbList = (BasicDBList) obj;
return conversionService.convert(readCollectionOrArray(type, objAsDbList), rawType);
} else if (obj instanceof DBObject) {
return read(type, ((DBObject) obj));
} else if (null != obj && obj.getClass().isAssignableFrom(rawType)) {
return (T) obj;
} else if (null != obj) {
return conversionService.convert(obj, rawType);
}
return null;
}
};
SpELAwareParameterValueProvider delegate = new SpELAwareParameterValueProvider(spelExpressionParser, spelCtx);
ParameterValueProvider provider = new DelegatingParameterValueProvider(constructor, dbo, delegate);
final BeanWrapper<MongoPersistentEntity<S>, S> wrapper = BeanWrapper.create(entity, provider, conversionService);
@@ -242,7 +207,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
public void doWithPersistentProperty(MongoPersistentProperty prop) {
boolean isConstructorProperty = ctorParamNames.contains(prop.getName());
boolean isConstructorProperty = constructor.isConstructorParameter(prop);
boolean hasValueForProperty = dbo.containsField(prop.getFieldName());
if (!hasValueForProperty || isConstructorProperty) {
@@ -892,4 +857,55 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return dbObject;
}
private class DelegatingParameterValueProvider implements ParameterValueProvider {
private final DBObject source;
private final ParameterValueProvider delegate;
private final MappedConstructor constructor;
/**
* {@link ParameterValueProvider} to delegate source object lookup to a {@link SpELAwareParameterValueProvider} in
* case a MappCon
*
* @param constructor must not be {@literal null}.
* @param source must not be {@literal null}.
* @param delegate must not be {@literal null}.
*/
public DelegatingParameterValueProvider(MappedConstructor constructor, DBObject source,
SpELAwareParameterValueProvider delegate) {
Assert.notNull(constructor);
Assert.notNull(source);
Assert.notNull(delegate);
this.constructor = constructor;
this.source = source;
this.delegate = delegate;
}
/*
* (non-Javadoc)
* @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
*/
@SuppressWarnings("unchecked")
public <T> T getParameterValue(Parameter<T> parameter) {
MappedConstructor.MappedParameter mappedParameter = constructor.getFor(parameter);
Object value = mappedParameter.hasSpELExpression() ? delegate.getParameterValue(parameter) : source
.get(mappedParameter.getFieldName());
TypeInformation<?> type = mappedParameter.getPropertyTypeInformation();
if (value instanceof DBRef) {
return (T) read(type, ((DBRef) value).fetch());
} else if (value instanceof BasicDBList) {
return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value), type.getType());
} else if (value instanceof DBObject) {
return (T) read(type, (DBObject) value);
} else {
return (T) getPotentiallyConvertedSimpleRead(value, type.getType());
}
}
}
}

View File

@@ -1041,7 +1041,7 @@ public class MongoTemplateTests {
List<TestClass> testClassList = mappingTemplate.find(new Query(Criteria.where("myDate").is(dateTime.toDate())),
TestClass.class);
assertThat(testClassList.size(), is(1));
assertThat(testClassList.get(0).getMyDate(), is(testClass.getMyDate()));
assertThat(testClassList.get(0).myDate, is(testClass.myDate));
}
/**
@@ -1080,23 +1080,19 @@ public class MongoTemplateTests {
assertThat(template.findOne(query(where("id").is(id)), Sample.class), is(nullValue()));
}
public class Sample {
public static class Sample {
@Id
String id;
}
public class TestClass {
static class TestClass {
private DateTime myDate;
DateTime myDate;
@PersistenceConstructor
public TestClass(DateTime date) {
this.myDate = date;
}
public DateTime getMyDate() {
return myDate;
TestClass(DateTime myDate) {
this.myDate = myDate;
}
}

View File

@@ -41,9 +41,11 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.PersistenceConstructor;
import org.springframework.data.mapping.model.MappingInstantiationException;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
@@ -274,6 +276,22 @@ public class MappingMongoConverterUnitTests {
assertThat(result.firstname, is("Oliver"));
}
@Test
public void resolvesNestedComplexTypeForConstructorCorrectly() {
DBObject address = new BasicDBObject("street", "110 Southwark Street");
address.put("city", "London");
BasicDBList addresses = new BasicDBList();
addresses.add(address);
DBObject person = new BasicDBObject("firstname", "Oliver");
person.put("addresses", addresses);
Person result = converter.read(Person.class, person);
assertThat(result.addresses, is(notNullValue()));
}
/**
* @see DATAMONGO-145
*/
@@ -873,17 +891,55 @@ public class MappingMongoConverterUnitTests {
assertThat(((Collection<Object>) contacts), hasItem(nullValue()));
}
class GenericType<T> {
/**
* @see DATAMONGO-379
*/
@Test
public void considersDefaultingExpressionsAtConstructorArguments() {
DBObject dbObject = new BasicDBObject("foo", "bar");
dbObject.put("foobar", 2.5);
DefaultedConstructorArgument result = converter.read(DefaultedConstructorArgument.class, dbObject);
assertThat(result.bar, is(-1));
}
/**
* @see DATAMONGO-379
*/
@Test
public void usesDocumentFieldIfReferencedInAtValue() {
DBObject dbObject = new BasicDBObject("foo", "bar");
dbObject.put("something", 37);
dbObject.put("foobar", 2.5);
DefaultedConstructorArgument result = converter.read(DefaultedConstructorArgument.class, dbObject);
assertThat(result.bar, is(37));
}
/**
* @see DATAMONGO-379
*/
@Test(expected = MappingInstantiationException.class)
public void rejectsNotFoundConstructorParameterForPrimitiveType() {
DBObject dbObject = new BasicDBObject("foo", "bar");
converter.read(DefaultedConstructorArgument.class, dbObject);
}
static class GenericType<T> {
T content;
}
class ClassWithEnumProperty {
static class ClassWithEnumProperty {
SampleEnum sampleEnum;
List<SampleEnum> enums;
}
enum SampleEnum {
static enum SampleEnum {
FIRST {
@Override
void method() {
@@ -899,7 +955,7 @@ public class MappingMongoConverterUnitTests {
abstract void method();
}
class Address {
static class Address {
String street;
String city;
}
@@ -908,7 +964,7 @@ public class MappingMongoConverterUnitTests {
}
public static class Person implements Contact {
static class Person implements Contact {
LocalDate birthDate;
@Field("foo")
@@ -926,46 +982,46 @@ public class MappingMongoConverterUnitTests {
}
}
class ClassWithSortedMap {
static class ClassWithSortedMap {
SortedMap<String, String> map;
}
class ClassWithMapProperty {
static class ClassWithMapProperty {
Map<Locale, String> map;
Map<String, List<String>> mapOfLists;
Map<String, Object> mapOfObjects;
}
class ClassWithNestedMaps {
static class ClassWithNestedMaps {
Map<String, Map<String, Map<String, String>>> nestedMaps;
}
class BirthDateContainer {
static class BirthDateContainer {
LocalDate birthDate;
}
class BigDecimalContainer {
static class BigDecimalContainer {
BigDecimal value;
Map<String, BigDecimal> map;
List<BigDecimal> collection;
}
class CollectionWrapper {
static class CollectionWrapper {
List<Contact> contacts;
List<List<String>> strings;
List<Map<String, Locale>> listOfMaps;
}
class LocaleWrapper {
static class LocaleWrapper {
Locale locale;
}
class ClassWithBigIntegerId {
static class ClassWithBigIntegerId {
@Id
BigInteger id;
}
class A<T> {
static class A<T> {
String valueType;
T value;
@@ -976,12 +1032,25 @@ public class MappingMongoConverterUnitTests {
}
}
class ClassWithIntId {
static class ClassWithIntId {
@Id
int id;
}
static class DefaultedConstructorArgument {
String foo;
int bar;
double foobar;
DefaultedConstructorArgument(String foo, @Value("#root.something ?: -1") int bar, double foobar) {
this.foo = foo;
this.bar = bar;
this.foobar = foobar;
}
}
private class LocalDateToDateConverter implements Converter<LocalDate, Date> {
public Date convert(LocalDate source) {

View File

@@ -54,7 +54,6 @@ import com.mongodb.WriteConcern;
* Modified from https://github.com/deftlabs/mongo-java-geospatial-example
*
* @author Mark Pollack
*
*/
public class GeoSpatialTests {

View File

@@ -20,8 +20,7 @@ import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.util.Collections;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -30,7 +29,9 @@ import org.mockito.runners.MockitoJUnitRunner;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import com.mongodb.BasicDBObject;
import com.mongodb.DBObject;
/**
* Unit tests for testing the mapping works with generic types.
@@ -85,15 +86,15 @@ public class GenericMappingTests {
assertThat(result.container.content, is("Foo!"));
}
public class StringWrapper extends Wrapper<String> {
static class StringWrapper extends Wrapper<String> {
}
public class Wrapper<S> {
static class Wrapper<S> {
Container<S> container;
}
public class Container<T> {
static class Container<T> {
T content;
}
}

View File

@@ -28,11 +28,6 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.types.ObjectId;
@@ -46,12 +41,17 @@ import org.springframework.data.mongodb.MongoCollectionUtils;
import org.springframework.data.mongodb.core.CollectionCallback;
import org.springframework.data.mongodb.core.MongoDbUtils;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Order;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.test.util.ReflectionTestUtils;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBObject;
import com.mongodb.Mongo;
import com.mongodb.MongoException;
/**
* @author Jon Brisbin <jbrisbin@vmware.com>
*/
@@ -511,7 +511,7 @@ public class MappingTests {
assertThat(result.items.get(0).id, is(items.id));
}
class Container {
static class Container {
@Id
final String id;
@@ -526,7 +526,7 @@ public class MappingTests {
List<Item> items;
}
class Item {
static class Item {
@Id
final String id;