Skip to content

Commit 33dd00f

Browse files
committed
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").
1 parent 3207a81 commit 33dd00f

File tree

8 files changed

+388
-84
lines changed

8 files changed

+388
-84
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright 2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert;
17+
18+
import java.util.Map;
19+
20+
import org.springframework.context.expression.MapAccessor;
21+
import org.springframework.expression.EvaluationContext;
22+
import org.springframework.expression.PropertyAccessor;
23+
import org.springframework.expression.TypedValue;
24+
25+
import com.mongodb.DBObject;
26+
27+
/**
28+
* {@link PropertyAccessor} to allow entity based field access to {@link DBObject}s.
29+
*
30+
* @author Oliver Gierke
31+
*/
32+
class DBObjectPropertyAccessor extends MapAccessor {
33+
34+
static MapAccessor INSTANCE = new DBObjectPropertyAccessor();
35+
36+
@Override
37+
public Class<?>[] getSpecificTargetClasses() {
38+
return new Class[] { DBObject.class };
39+
}
40+
41+
@Override
42+
public boolean canRead(EvaluationContext context, Object target, String name) {
43+
return true;
44+
}
45+
46+
@Override
47+
@SuppressWarnings("unchecked")
48+
public TypedValue read(EvaluationContext context, Object target, String name) {
49+
50+
Map<String, Object> source = (Map<String, Object>) target;
51+
52+
Object value = source.get(name);
53+
return value == null ? TypedValue.NULL : new TypedValue(value);
54+
}
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright 2012 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.convert;
17+
18+
import java.util.HashSet;
19+
import java.util.Set;
20+
21+
import org.springframework.data.mapping.PersistentEntity;
22+
import org.springframework.data.mapping.PersistentProperty;
23+
import org.springframework.data.mapping.PreferredConstructor;
24+
import org.springframework.data.mapping.PreferredConstructor.Parameter;
25+
import org.springframework.data.mapping.PropertyPath;
26+
import org.springframework.data.mapping.context.MappingContext;
27+
import org.springframework.data.mapping.context.PersistentPropertyPath;
28+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
29+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
30+
import org.springframework.data.util.TypeInformation;
31+
import org.springframework.util.Assert;
32+
33+
/**
34+
* Abstraction for a {@link PreferredConstructor} alongside mapping information.
35+
*
36+
* @author Oliver Gierke
37+
*/
38+
class MappedConstructor {
39+
40+
private final Set<MappedConstructor.MappedParameter> parameters;
41+
42+
/**
43+
* Creates a new {@link MappedConstructor} from the given {@link MongoPersistentEntity} and {@link MappingContext}.
44+
*
45+
* @param entity must not be {@literal null}.
46+
* @param context must not be {@literal null}.
47+
*/
48+
public MappedConstructor(MongoPersistentEntity<?> entity,
49+
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> context) {
50+
51+
Assert.notNull(entity);
52+
Assert.notNull(context);
53+
54+
this.parameters = new HashSet<MappedConstructor.MappedParameter>();
55+
56+
for (Parameter<?> parameter : entity.getPreferredConstructor().getParameters()) {
57+
parameters.add(new MappedParameter(parameter, entity, context));
58+
}
59+
}
60+
61+
/**
62+
* Returns whether the given {@link PersistentProperty} is referenced in a constructor argument of the
63+
* {@link PersistentEntity} backing this {@link MappedConstructor}.
64+
*
65+
* @param property must not be {@literal null}.
66+
* @return
67+
*/
68+
public boolean isConstructorParameter(PersistentProperty<?> property) {
69+
70+
Assert.notNull(property);
71+
72+
for (MappedConstructor.MappedParameter parameter : parameters) {
73+
if (parameter.maps(property)) {
74+
return true;
75+
}
76+
}
77+
78+
return false;
79+
}
80+
81+
/**
82+
* Returns the {@link MappedParameter} for the given {@link Parameter}.
83+
*
84+
* @param parameter must not be {@literal null}.
85+
* @return
86+
*/
87+
public MappedParameter getFor(Parameter<?> parameter) {
88+
89+
for (MappedParameter mappedParameter : parameters) {
90+
if (mappedParameter.parameter.equals(parameter)) {
91+
return mappedParameter;
92+
}
93+
}
94+
95+
throw new IllegalStateException(String.format("Didn't find a MappedParameter for %s!", parameter.toString()));
96+
}
97+
98+
/**
99+
* Abstraction of a {@link Parameter} alongside mapping information.
100+
*
101+
* @author Oliver Gierke
102+
*/
103+
static class MappedParameter {
104+
105+
private final MongoPersistentProperty property;
106+
private final Parameter<?> parameter;
107+
108+
/**
109+
* Creates a new {@link MappedParameter} for the given {@link Parameter}, {@link MongoPersistentProperty} and
110+
* {@link MappingContext}.
111+
*
112+
* @param parameter must not be {@literal null}.
113+
* @param entity must not be {@literal null}.
114+
* @param context must not be {@literal null}.
115+
*/
116+
public MappedParameter(Parameter<?> parameter, MongoPersistentEntity<?> entity,
117+
MappingContext<? extends MongoPersistentEntity<?>, ? extends MongoPersistentProperty> context) {
118+
119+
Assert.notNull(parameter);
120+
Assert.notNull(entity);
121+
Assert.notNull(context);
122+
123+
this.parameter = parameter;
124+
125+
PropertyPath propertyPath = PropertyPath.from(parameter.getName(), entity.getType());
126+
PersistentPropertyPath<? extends MongoPersistentProperty> path = context.getPersistentPropertyPath(propertyPath);
127+
this.property = path == null ? null : path.getLeafProperty();
128+
}
129+
130+
/**
131+
* Returns whether there is a SpEL expression configured for this parameter.
132+
*
133+
* @return
134+
*/
135+
public boolean hasSpELExpression() {
136+
return parameter.getKey() != null;
137+
}
138+
139+
/**
140+
* Returns the field name to be used to lookup the value which in turn shall be converted into the constructor
141+
* parameter.
142+
*
143+
* @return
144+
*/
145+
public String getFieldName() {
146+
return property.getFieldName();
147+
}
148+
149+
/**
150+
* Returns the type of the property backing the {@link Parameter}.
151+
*
152+
* @return
153+
*/
154+
public TypeInformation<?> getPropertyTypeInformation() {
155+
return property.getTypeInformation();
156+
}
157+
158+
/**
159+
* Returns whether the given {@link PersistentProperty} is mapped by the parameter.
160+
*
161+
* @param property
162+
* @return
163+
*/
164+
public boolean maps(PersistentProperty<?> property) {
165+
return this.property.equals(property);
166+
}
167+
}
168+
}

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java

+60-44
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import org.springframework.data.convert.TypeMapper;
3838
import org.springframework.data.mapping.Association;
3939
import org.springframework.data.mapping.AssociationHandler;
40-
import org.springframework.data.mapping.PreferredConstructor;
40+
import org.springframework.data.mapping.PreferredConstructor.Parameter;
4141
import org.springframework.data.mapping.PropertyHandler;
4242
import org.springframework.data.mapping.context.MappingContext;
4343
import org.springframework.data.mapping.model.BeanWrapper;
@@ -189,60 +189,25 @@ protected <S extends Object> S read(TypeInformation<S> type, DBObject dbo) {
189189

190190
private <S extends Object> S read(final MongoPersistentEntity<S> entity, final DBObject dbo) {
191191

192-
final StandardEvaluationContext spelCtx = new StandardEvaluationContext();
193-
if (null != applicationContext) {
192+
final StandardEvaluationContext spelCtx = new StandardEvaluationContext(dbo);
193+
spelCtx.addPropertyAccessor(DBObjectPropertyAccessor.INSTANCE);
194+
195+
if (applicationContext != null) {
194196
spelCtx.setBeanResolver(new BeanFactoryResolver(applicationContext));
195197
}
196-
if (!(dbo instanceof BasicDBList)) {
197-
String[] keySet = dbo.keySet().toArray(new String[dbo.keySet().size()]);
198-
for (String key : keySet) {
199-
spelCtx.setVariable(key, dbo.get(key));
200-
}
201-
}
202-
203-
final List<String> ctorParamNames = new ArrayList<String>();
204-
final MongoPersistentProperty idProperty = entity.getIdProperty();
205-
206-
ParameterValueProvider provider = new SpELAwareParameterValueProvider(spelExpressionParser, spelCtx) {
207-
@Override
208-
@SuppressWarnings("unchecked")
209-
public <T> T getParameterValue(PreferredConstructor.Parameter<T> parameter) {
210-
211-
if (parameter.getKey() != null) {
212-
return super.getParameterValue(parameter);
213-
}
214198

215-
String name = parameter.getName();
216-
TypeInformation<T> type = parameter.getType();
217-
Class<T> rawType = parameter.getRawType();
218-
String key = idProperty == null ? name : idProperty.getName().equals(name) ? idProperty.getFieldName() : name;
219-
Object obj = dbo.get(key);
220-
221-
ctorParamNames.add(name);
222-
if (obj instanceof DBRef) {
223-
return read(type, ((DBRef) obj).fetch());
224-
} else if (obj instanceof BasicDBList) {
225-
BasicDBList objAsDbList = (BasicDBList) obj;
226-
return conversionService.convert(readCollectionOrArray(type, objAsDbList), rawType);
227-
} else if (obj instanceof DBObject) {
228-
return read(type, ((DBObject) obj));
229-
} else if (null != obj && obj.getClass().isAssignableFrom(rawType)) {
230-
return (T) obj;
231-
} else if (null != obj) {
232-
return conversionService.convert(obj, rawType);
233-
}
199+
final MappedConstructor constructor = new MappedConstructor(entity, mappingContext);
234200

235-
return null;
236-
}
237-
};
201+
SpELAwareParameterValueProvider delegate = new SpELAwareParameterValueProvider(spelExpressionParser, spelCtx);
202+
ParameterValueProvider provider = new DelegatingParameterValueProvider(constructor, dbo, delegate);
238203

239204
final BeanWrapper<MongoPersistentEntity<S>, S> wrapper = BeanWrapper.create(entity, provider, conversionService);
240205

241206
// Set properties not already set in the constructor
242207
entity.doWithProperties(new PropertyHandler<MongoPersistentProperty>() {
243208
public void doWithPersistentProperty(MongoPersistentProperty prop) {
244209

245-
boolean isConstructorProperty = ctorParamNames.contains(prop.getName());
210+
boolean isConstructorProperty = constructor.isConstructorParameter(prop);
246211
boolean hasValueForProperty = dbo.containsField(prop.getFieldName());
247212

248213
if (!hasValueForProperty || isConstructorProperty) {
@@ -892,4 +857,55 @@ private Object removeTypeInfoRecursively(Object object) {
892857

893858
return dbObject;
894859
}
860+
861+
private class DelegatingParameterValueProvider implements ParameterValueProvider {
862+
863+
private final DBObject source;
864+
private final ParameterValueProvider delegate;
865+
private final MappedConstructor constructor;
866+
867+
/**
868+
* {@link ParameterValueProvider} to delegate source object lookup to a {@link SpELAwareParameterValueProvider} in
869+
* case a MappCon
870+
*
871+
* @param constructor must not be {@literal null}.
872+
* @param source must not be {@literal null}.
873+
* @param delegate must not be {@literal null}.
874+
*/
875+
public DelegatingParameterValueProvider(MappedConstructor constructor, DBObject source,
876+
SpELAwareParameterValueProvider delegate) {
877+
878+
Assert.notNull(constructor);
879+
Assert.notNull(source);
880+
Assert.notNull(delegate);
881+
882+
this.constructor = constructor;
883+
this.source = source;
884+
this.delegate = delegate;
885+
}
886+
887+
/*
888+
* (non-Javadoc)
889+
* @see org.springframework.data.mapping.model.ParameterValueProvider#getParameterValue(org.springframework.data.mapping.PreferredConstructor.Parameter)
890+
*/
891+
@SuppressWarnings("unchecked")
892+
public <T> T getParameterValue(Parameter<T> parameter) {
893+
894+
MappedConstructor.MappedParameter mappedParameter = constructor.getFor(parameter);
895+
Object value = mappedParameter.hasSpELExpression() ? delegate.getParameterValue(parameter) : source
896+
.get(mappedParameter.getFieldName());
897+
898+
TypeInformation<?> type = mappedParameter.getPropertyTypeInformation();
899+
900+
if (value instanceof DBRef) {
901+
return (T) read(type, ((DBRef) value).fetch());
902+
} else if (value instanceof BasicDBList) {
903+
return (T) getPotentiallyConvertedSimpleRead(readCollectionOrArray(type, (BasicDBList) value), type.getType());
904+
} else if (value instanceof DBObject) {
905+
return (T) read(type, (DBObject) value);
906+
} else {
907+
return (T) getPotentiallyConvertedSimpleRead(value, type.getType());
908+
}
909+
}
910+
}
895911
}

spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java

+6-10
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ public void returnsEntityWhenQueryingForDateTime() {
10411041
List<TestClass> testClassList = mappingTemplate.find(new Query(Criteria.where("myDate").is(dateTime.toDate())),
10421042
TestClass.class);
10431043
assertThat(testClassList.size(), is(1));
1044-
assertThat(testClassList.get(0).getMyDate(), is(testClass.getMyDate()));
1044+
assertThat(testClassList.get(0).myDate, is(testClass.myDate));
10451045
}
10461046

10471047
/**
@@ -1080,23 +1080,19 @@ public void removesEntityWithAnnotatedIdIfIdNeedsMassaging() {
10801080
assertThat(template.findOne(query(where("id").is(id)), Sample.class), is(nullValue()));
10811081
}
10821082

1083-
public class Sample {
1083+
public static class Sample {
10841084

10851085
@Id
10861086
String id;
10871087
}
10881088

1089-
public class TestClass {
1089+
static class TestClass {
10901090

1091-
private DateTime myDate;
1091+
DateTime myDate;
10921092

10931093
@PersistenceConstructor
1094-
public TestClass(DateTime date) {
1095-
this.myDate = date;
1096-
}
1097-
1098-
public DateTime getMyDate() {
1099-
return myDate;
1094+
TestClass(DateTime myDate) {
1095+
this.myDate = myDate;
11001096
}
11011097
}
11021098

0 commit comments

Comments
 (0)