DATAMONGO-1703 - Convert resolved DBRef's from source that do not match the requested property type.

We now check if already resolved DBRef's are assignable to the target property type. If not, we perform conversion again to prevent ClassCastException when trying to assign non matching types.

Remove non applicable public modifiers in ObjectPath.

Original pull request: #478.
This commit is contained in:
Christoph Strobl
2017-07-04 13:59:42 +02:00
committed by Mark Paluch
parent 1f2d0da5ed
commit 1681bcd15b
4 changed files with 310 additions and 56 deletions

View File

@@ -144,7 +144,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
*/ */
public void setTypeMapper(MongoTypeMapper typeMapper) { public void setTypeMapper(MongoTypeMapper typeMapper) {
this.typeMapper = typeMapper == null this.typeMapper = typeMapper == null
? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext) : typeMapper; ? new DefaultMongoTypeMapper(DefaultMongoTypeMapper.DEFAULT_TYPE_KEY, mappingContext)
: typeMapper;
} }
/* /*
@@ -520,7 +521,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
addCustomTypeKeyIfNecessary(ClassTypeInformation.from(prop.getRawType()), obj, propDbObj); addCustomTypeKeyIfNecessary(ClassTypeInformation.from(prop.getRawType()), obj, propDbObj);
MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass()) MongoPersistentEntity<?> entity = isSubtype(prop.getType(), obj.getClass())
? mappingContext.getPersistentEntity(obj.getClass()) : mappingContext.getPersistentEntity(type); ? mappingContext.getPersistentEntity(obj.getClass())
: mappingContext.getPersistentEntity(type);
writeInternal(obj, propDbObj, entity); writeInternal(obj, propDbObj, entity);
accessor.put(prop, propDbObj); accessor.put(prop, propDbObj);
@@ -731,7 +733,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
return conversions.hasCustomWriteTarget(key.getClass(), String.class) return conversions.hasCustomWriteTarget(key.getClass(), String.class)
? (String) getPotentiallyConvertedSimpleWrite(key) : key.toString(); ? (String) getPotentiallyConvertedSimpleWrite(key)
: key.toString();
} }
/** /**
@@ -1031,7 +1034,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
for (Entry<Object, Object> entry : ((Map<Object, Object>) obj).entrySet()) { for (Entry<Object, Object> entry : ((Map<Object, Object>) obj).entrySet()) {
TypeInformation<? extends Object> valueTypeHint = typeHint != null && typeHint.getMapValueType() != null TypeInformation<? extends Object> valueTypeHint = typeHint != null && typeHint.getMapValueType() != null
? typeHint.getMapValueType() : typeHint; ? typeHint.getMapValueType()
: typeHint;
converted.put(getPotentiallyConvertedSimpleWrite(entry.getKey()).toString(), converted.put(getPotentiallyConvertedSimpleWrite(entry.getKey()).toString(),
convertToMongoType(entry.getValue(), valueTypeHint)); convertToMongoType(entry.getValue(), valueTypeHint));
@@ -1228,11 +1232,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
return (T) dbref; return (T) dbref;
} }
Object object = dbref == null ? null : path.getPathItem(dbref.getId(), dbref.getCollectionName()); T object = dbref == null ? null : path.getPathItem(dbref.getId(), dbref.getCollectionName(), (Class<T>) rawType);
return (T) (object != null ? object : readAndConvertDBRef(dbref, type, path, rawType)); return object != null ? object : (T) readAndConvertDBRef(dbref, type, path, rawType);
} }
private <T> T readAndConvertDBRef(DBRef dbref, TypeInformation<?> type, ObjectPath path, final Class<?> rawType) { private <T> T readAndConvertDBRef(DBRef dbref, TypeInformation<?> type, ObjectPath path, Class<?> rawType) {
List<T> result = bulkReadAndConvertDBRefs(Collections.singletonList(dbref), type, path, rawType); List<T> result = bulkReadAndConvertDBRefs(Collections.singletonList(dbref), type, path, rawType);
return CollectionUtils.isEmpty(result) ? null : result.iterator().next(); return CollectionUtils.isEmpty(result) ? null : result.iterator().next();
@@ -1262,7 +1266,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App
} }
List<DBObject> referencedRawDocuments = dbrefs.size() == 1 List<DBObject> referencedRawDocuments = dbrefs.size() == 1
? Collections.singletonList(readRef(dbrefs.iterator().next())) : bulkReadRefs(dbrefs); ? Collections.singletonList(readRef(dbrefs.iterator().next()))
: bulkReadRefs(dbrefs);
String collectionName = dbrefs.iterator().next().getCollectionName(); String collectionName = dbrefs.iterator().next().getCollectionName();
List<T> targeList = new ArrayList<T>(dbrefs.size()); List<T> targeList = new ArrayList<T>(dbrefs.size());

View File

@@ -21,6 +21,8 @@ import java.util.List;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import com.mongodb.DBObject; import com.mongodb.DBObject;
@@ -35,11 +37,13 @@ import com.mongodb.DBObject;
* *
* @author Thomas Darimont * @author Thomas Darimont
* @author Oliver Gierke * @author Oliver Gierke
* @author Mark Paluch
* @author Christoph Strobl
* @since 1.6 * @since 1.6
*/ */
class ObjectPath { class ObjectPath {
public static final ObjectPath ROOT = new ObjectPath(); static final ObjectPath ROOT = new ObjectPath();
private final List<ObjectPathItem> items; private final List<ObjectPathItem> items;
@@ -68,9 +72,9 @@ class ObjectPath {
* @param object must not be {@literal null}. * @param object must not be {@literal null}.
* @param entity must not be {@literal null}. * @param entity must not be {@literal null}.
* @param id must not be {@literal null}. * @param id must not be {@literal null}.
* @return * @return new instance of {@link ObjectPath}.
*/ */
public ObjectPath push(Object object, MongoPersistentEntity<?> entity, Object id) { ObjectPath push(Object object, MongoPersistentEntity<?> entity, Object id) {
Assert.notNull(object, "Object must not be null!"); Assert.notNull(object, "Object must not be null!");
Assert.notNull(entity, "MongoPersistentEntity must not be null!"); Assert.notNull(entity, "MongoPersistentEntity must not be null!");
@@ -80,14 +84,15 @@ class ObjectPath {
} }
/** /**
* Returns the object with the given id and stored in the given collection if it's contained in the {@link ObjectPath} * Returns the object with the given id and stored in the given collection if it's contained in the* {@link ObjectPath}.
* .
* *
* @param id must not be {@literal null}. * @param id must not be {@literal null}.
* @param collection must not be {@literal null} or empty. * @param collection must not be {@literal null} or empty.
* @return * @return
* @deprecated use {@link #getPathItem(Object, String, Class)}.
*/ */
public Object getPathItem(Object id, String collection) { @Deprecated
Object getPathItem(Object id, String collection) {
Assert.notNull(id, "Id must not be null!"); Assert.notNull(id, "Id must not be null!");
Assert.hasText(collection, "Collection name must not be null!"); Assert.hasText(collection, "Collection name must not be null!");
@@ -96,11 +101,7 @@ class ObjectPath {
Object object = item.getObject(); Object object = item.getObject();
if (object == null) { if (object == null || item.getIdValue() == null) {
continue;
}
if (item.getIdValue() == null) {
continue; continue;
} }
@@ -112,12 +113,45 @@ class ObjectPath {
return null; return null;
} }
/**
* Get the object with given {@literal id}, stored in the {@literal collection} that is assignable to the given
* {@literal type} or {@literal null} if no match found.
*
* @param id must not be {@literal null}.
* @param collection must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @return {@literal null} when no match found.
* @since 2.0
*/
<T> T getPathItem(Object id, String collection, Class<T> type) {
Assert.notNull(id, "Id must not be null!");
Assert.hasText(collection, "Collection name must not be null!");
Assert.notNull(type, "Type must not be null!");
for (ObjectPathItem item : items) {
Object object = item.getObject();
if (object == null || item.getIdValue() == null) {
continue;
}
if (collection.equals(item.getCollection()) && id.equals(item.getIdValue())
&& ClassUtils.isAssignable(type, object.getClass())) {
return (T) object;
}
}
return null;
}
/** /**
* Returns the current object of the {@link ObjectPath} or {@literal null} if the path is empty. * Returns the current object of the {@link ObjectPath} or {@literal null} if the path is empty.
* *
* @return * @return
*/ */
public Object getCurrentObject() { Object getCurrentObject() {
return items.isEmpty() ? null : items.get(items.size() - 1).getObject(); return items.isEmpty() ? null : items.get(items.size() - 1).getObject();
} }
@@ -135,7 +169,7 @@ class ObjectPath {
List<String> strings = new ArrayList<String>(items.size()); List<String> strings = new ArrayList<String>(items.size());
for (ObjectPathItem item : items) { for (ObjectPathItem item : items) {
strings.add(item.object.toString()); strings.add(ObjectUtils.nullSafeToString(item.object));
} }
return StringUtils.collectionToDelimitedString(strings, " -> "); return StringUtils.collectionToDelimitedString(strings, " -> ");
@@ -167,15 +201,15 @@ class ObjectPath {
this.collection = collection; this.collection = collection;
} }
public Object getObject() { Object getObject() {
return object; return object;
} }
public Object getIdValue() { Object getIdValue() {
return idValue; return idValue;
} }
public String getCollection() { String getCollection() {
return collection; return collection;
} }
} }

View File

@@ -0,0 +1,111 @@
/*
* Copyright 2017 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;
import static org.hamcrest.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import static org.springframework.data.mongodb.core.query.Criteria.*;
import static org.springframework.data.mongodb.core.query.Query.*;
import lombok.Data;
import java.net.UnknownHostException;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.DBRef;
import org.springframework.data.mongodb.core.mapping.Document;
import com.mongodb.MongoClient;
/**
* {@link org.springframework.data.mongodb.core.mapping.DBRef} related integration tests for
* {@link org.springframework.data.mongodb.core.MongoTemplate}.
*
* @author Christoph Strobl
*/
public class MongoTemplateDbRefTests {
MongoTemplate template;
@Before
public void setUp() throws UnknownHostException {
template = new MongoTemplate(new MongoClient(), "mongo-template-dbref-tests");
template.dropCollection(RefCycleLoadingIntoDifferentTypeRoot.class);
template.dropCollection(RefCycleLoadingIntoDifferentTypeIntermediate.class);
template.dropCollection(RefCycleLoadingIntoDifferentTypeRootView.class);
}
@Test // DATAMONGO-1703
public void shouldLoadRefIntoDifferentTypeCorrectly() {
// init root
RefCycleLoadingIntoDifferentTypeRoot root = new RefCycleLoadingIntoDifferentTypeRoot();
root.id = "root-1";
root.content = "jon snow";
template.save(root);
// init one and set view id ref to root.id
RefCycleLoadingIntoDifferentTypeIntermediate intermediate = new RefCycleLoadingIntoDifferentTypeIntermediate();
intermediate.id = "one-1";
intermediate.refToRootView = new RefCycleLoadingIntoDifferentTypeRootView();
intermediate.refToRootView.id = root.id;
template.save(intermediate);
// add one ref to root
root.refToIntermediate = intermediate;
template.save(root);
RefCycleLoadingIntoDifferentTypeRoot loaded = template.findOne(query(where("id").is(root.id)),
RefCycleLoadingIntoDifferentTypeRoot.class);
assertThat(loaded.content, is(equalTo("jon snow")));
assertThat(loaded.getRefToIntermediate(), is(instanceOf(RefCycleLoadingIntoDifferentTypeIntermediate.class)));
assertThat(loaded.getRefToIntermediate().getRefToRootView(),
is(instanceOf(RefCycleLoadingIntoDifferentTypeRootView.class)));
assertThat(loaded.getRefToIntermediate().getRefToRootView().getContent(), is(equalTo("jon snow")));
}
@Data
@Document(collection = "cycle-with-different-type-root")
static class RefCycleLoadingIntoDifferentTypeRoot {
@Id String id;
String content;
@DBRef RefCycleLoadingIntoDifferentTypeIntermediate refToIntermediate;
}
@Data
@Document(collection = "cycle-with-different-type-intermediate")
static class RefCycleLoadingIntoDifferentTypeIntermediate {
@Id String id;
@DBRef RefCycleLoadingIntoDifferentTypeRootView refToRootView;
}
@Data
@Document(collection = "cycle-with-different-type-root")
static class RefCycleLoadingIntoDifferentTypeRootView {
@Id String id;
String content;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2017 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.MatcherAssert.*;
import static org.hamcrest.Matchers.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.util.ClassTypeInformation;
/**
* Unit tests for {@link ObjectPath}.
*
* @author Christoph Strobl
*/
public class ObjectPathUnitTests {
MongoPersistentEntity<EntityOne> one;
MongoPersistentEntity<EntityTwo> two;
MongoPersistentEntity<EntityThree> three;
@Before
public void setUp() {
one = new BasicMongoPersistentEntity(ClassTypeInformation.from(EntityOne.class));
two = new BasicMongoPersistentEntity(ClassTypeInformation.from(EntityTwo.class));
three = new BasicMongoPersistentEntity(ClassTypeInformation.from(EntityThree.class));
}
@Test // DATAMONGO-1703
public void getPathItemShouldReturnMatch() {
ObjectPath path = ObjectPath.ROOT.push(new EntityOne(), one, "id-1");
assertThat(path.getPathItem("id-1", "one", EntityOne.class), is(notNullValue()));
}
@Test // DATAMONGO-1703
public void getPathItemShouldReturnNullWhenNoTypeMatchFound() {
ObjectPath path = ObjectPath.ROOT.push(new EntityOne(), one, "id-1");
assertThat(path.getPathItem("id-1", "one", EntityThree.class), is(nullValue()));
}
@Test // DATAMONGO-1703
public void getPathItemShouldReturnCachedItemWhenIdAndCollectionMatchAndIsAssignable() {
ObjectPath path = ObjectPath.ROOT.push(new EntityTwo(), one, "id-1");
assertThat(path.getPathItem("id-1", "one", EntityOne.class), is(notNullValue()));
}
@Test // DATAMONGO-1703
public void getPathItemShouldReturnNullWhenIdAndCollectionMatchButNotAssignable() {
ObjectPath path = ObjectPath.ROOT.push(new EntityOne(), one, "id-1");
assertThat(path.getPathItem("id-1", "one", EntityTwo.class), is(nullValue()));
}
@Test // DATAMONGO-1703
public void getPathItemShouldReturnNullWhenIdAndCollectionMatchAndAssignableToInterface() {
ObjectPath path = ObjectPath.ROOT.push(new EntityThree(), one, "id-1");
assertThat(path.getPathItem("id-1", "one", ValueInterface.class), is(notNullValue()));
}
@Document(collection = "one")
static class EntityOne {
}
static class EntityTwo extends EntityOne {
}
interface ValueInterface {
}
@Document(collection = "three")
static class EntityThree implements ValueInterface {
}
}