From ce5046c35f519b6f51a180a013c250cd05e92e72 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 26 Aug 2011 20:20:38 +0200 Subject: [PATCH] DATADOC-63 - Added TypeMapper abstraction to customize how type information is written to a DBObject and retrieved from it. Added TypeMapper abstraction to allow plugging in a custom implementation. Current type handling logic is now in DefaultTypeMapper which additionally allows customizing the key the type information its stored under or disabling writing type info by setting the key to null. Added ConfigurableTypeMapper implementation that gets a Map configured to define Strings to be used to store type information. --- .../core/convert/ConfigurableTypeMapper.java | 98 ++++++++++++ .../core/convert/DefaultTypeMapper.java | 147 ++++++++++++++++++ .../core/convert/MappingMongoConverter.java | 65 ++++---- .../data/mongodb/core/convert/TypeMapper.java | 60 +++++++ .../core/convert/TypeMapperProvider.java | 31 ++++ .../ConvertingParameterAccessor.java | 34 ++-- .../ConfigurableTypeMapperUnitTests.java | 109 +++++++++++++ .../convert/DefaultTypeMapperUnitTests.java | 132 ++++++++++++++++ .../MappingMongoConverterUnitTests.java | 12 +- .../StringBasedMongoQueryUnitTests.java | 5 +- 10 files changed, 644 insertions(+), 49 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapper.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapper.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapper.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapperProvider.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapperUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapperUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapper.java new file mode 100644 index 000000000..4a914a5cc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapper.java @@ -0,0 +1,98 @@ +/* + * Copyright 2011 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.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; + +/** + * {@link TypeMapper} allowing to configure a {@link Map} containing {@link String} to {@link Class} mappings that will + * be used to map the values found under the configured type key (see {@link DefaultTypeMapper#setTypeKey(String)}. This + * allows declarative type mapping in a Spring config file for example. + * + * @author Oliver Gierke + */ +public class ConfigurableTypeMapper extends DefaultTypeMapper { + + private final Map, String> typeMap; + private boolean handleUnmappedClasses = false; + + /** + * Creates a new {@link ConfigurableTypeMapper} for the given type map. + * + * @param sourceTypeMap must not be {@literal null}. + */ + public ConfigurableTypeMapper(Map, String> sourceTypeMap) { + + Assert.notNull(sourceTypeMap); + + this.typeMap = new HashMap, String>(sourceTypeMap.size()); + + for (Entry, String> entry : sourceTypeMap.entrySet()) { + TypeInformation key = ClassTypeInformation.from(entry.getKey()); + String value = entry.getValue(); + + if (typeMap.containsValue(value)) { + throw new IllegalArgumentException(String.format( + "Detected mapping ambiguity! String %s cannot be mapped to more than one type!", value)); + } + + this.typeMap.put(key, value); + } + } + + /** + * Configures whether to try to handle unmapped classes by simply writing the class' name or loading the class as + * specified in the superclass. Defaults to {@literal false}. + * + * @param handleUnmappedClasses the handleUnmappedClasses to set + */ + public void setHandleUnmappedClasses(boolean handleUnmappedClasses) { + this.handleUnmappedClasses = handleUnmappedClasses; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.DefaultTypeMapper#getTypeInformation(java.lang.String) + */ + @Override + protected TypeInformation getTypeInformation(String value) { + + for (Entry, String> entry : typeMap.entrySet()) { + if (entry.getValue().equals(value)) { + return entry.getKey(); + } + } + + return handleUnmappedClasses ? super.getTypeInformation(value) : null; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.DefaultTypeMapper#getTypeString(org.springframework.data.util.TypeInformation) + */ + @Override + protected String getTypeString(TypeInformation typeInformation) { + + String key = typeMap.get(typeInformation); + return key != null ? key : handleUnmappedClasses ? super.getTypeString(typeInformation) : null; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapper.java new file mode 100644 index 000000000..d2b640430 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapper.java @@ -0,0 +1,147 @@ +/* + * Copyright 2011 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.List; + +import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.BasicDBList; +import com.mongodb.DBObject; + +/** + * Default implementation of {@link TypeMapper} allowing configuration of the key to lookup and store type information + * in {@link DBObject}. The key defaults to {@link #DEFAULT_TYPE_KEY}. Actual type-to-{@link String} conversion and back + * is done in {@link #getTypeString(TypeInformation)} or {@link #getTypeInformation(String)} respectively. + * + * @author Oliver Gierke + */ +public class DefaultTypeMapper implements TypeMapper { + + public static final String DEFAULT_TYPE_KEY = "_class"; + @SuppressWarnings("rawtypes") + private static final TypeInformation LIST_TYPE_INFORMATION = ClassTypeInformation.from(List.class); + + private String typeKey = DEFAULT_TYPE_KEY; + + /** + * Sets the key to store the type information under. If set to {@literal null} no type information will be stored in + * the document. + * + * @param typeKey the typeKey to set + */ + public void setTypeKey(String typeKey) { + this.typeKey = typeKey; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.TypeMapper#isTypeKey(java.lang.String) + */ + public boolean isTypeKey(String key) { + return typeKey == null ? false : typeKey.equals(key); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.TypeMapper#readType(com.mongodb.DBObject) + */ + public TypeInformation readType(DBObject dbObject) { + + if (dbObject instanceof BasicDBList) { + return LIST_TYPE_INFORMATION; + } + + if (typeKey == null) { + return null; + } + + Object classToBeUsed = dbObject.get(typeKey); + + if (classToBeUsed == null) { + return null; + } + + return getTypeInformation(classToBeUsed.toString()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.TypeMapper#writeType(java.lang.Class, com.mongodb.DBObject) + */ + public void writeType(Class type, DBObject dbObject) { + writeType(ClassTypeInformation.from(type), dbObject); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.TypeMapper#writeType(java.lang.Class, com.mongodb.DBObject) + */ + public void writeType(TypeInformation info, DBObject dbObject) { + + Assert.notNull(info); + + if (typeKey == null) { + return; + } + + String string = getTypeString(info); + + if (string != null) { + dbObject.put(typeKey, getTypeString(info)); + } + } + + /** + * Turn the given type information into the String representation that shall be stored inside the {@link DBObject}. If + * the returned String is {@literal null} no type information will be stored. Default implementation simply returns + * the fully-qualified class name. + * + * @param typeInformation must not be {@literal null}. + * @return the String representation to be stored or {@literal null} if no type information shall be stored. + */ + protected String getTypeString(TypeInformation typeInformation) { + return typeInformation.getType().getName(); + } + + /** + * Returns the {@link TypeInformation} that shall be used when the given {@link String} value is found as type hint. + * The default implementation will simply interpret the given value as fully-qualified class name and try to load the + * class. Will return {@literal null} in case the given {@link String} is empty. Will not be called in case no + * {@link String} was found for the configured type key at all. + * + * @param value the type to load, must not be {@literal null}. + * @return the type to be used for the given {@link String} representation or {@literal null} if nothing found or the + * class cannot be loaded. + */ + protected TypeInformation getTypeInformation(String value) { + + if (!StringUtils.hasText(value)) { + return null; + } + + try { + return ClassTypeInformation.from(ClassUtils.forName(value, null)); + } catch (ClassNotFoundException e) { + 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 36f062841..56491ff7c 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 @@ -62,7 +62,6 @@ import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -73,9 +72,7 @@ import org.springframework.util.StringUtils; * @author Jon Brisbin * @author Oliver Gierke */ -public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware { - - public static final String CUSTOM_TYPE_KEY = "_class"; +public class MappingMongoConverter extends AbstractMongoConverter implements ApplicationContextAware, TypeMapperProvider { @SuppressWarnings("rawtypes") private static final TypeInformation MAP_TYPE_INFORMATION = ClassTypeInformation.from(Map.class); @@ -89,6 +86,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App protected final MongoDbFactory mongoDbFactory; protected ApplicationContext applicationContext; protected boolean useFieldAccessOnly = true; + protected TypeMapper typeMapper = new DefaultTypeMapper(); /** * Creates a new {@link MappingMongoConverter} given the new {@link MongoDbFactory} and {@link MappingContext}. @@ -108,6 +106,25 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App this.mappingContext = mappingContext; } + /** + * Configures the {@link TypeMapper} to be used to add type information to {@link DBObject}s created by the converter + * and how to lookup type information from {@link DBObject}s when reading them. Uses a {@link DefaultTypeMapper} by + * default. Setting this to {@literal null} will reset the {@link TypeMapper} to the default one. + * + * @param typeMapper the typeMapper to set + */ + public void setTypeMapper(TypeMapper typeMapper) { + this.typeMapper = typeMapper == null ? new DefaultTypeMapper() : typeMapper; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.convert.MongoConverter#getTypeMapper() + */ + public TypeMapper getTypeMapper() { + return this.typeMapper; + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.core.convert.MongoConverter#getMappingContext() @@ -126,7 +143,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App public void setUseFieldAccessOnly(boolean useFieldAccessOnly) { this.useFieldAccessOnly = useFieldAccessOnly; } - + /* * (non-Javadoc) * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext) @@ -281,7 +298,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App boolean handledByCustomConverter = conversions.getCustomWriteTarget(obj.getClass(), DBObject.class) != null; if (!handledByCustomConverter) { - dbo.put(CUSTOM_TYPE_KEY, obj.getClass().getName()); + typeMapper.writeType(ClassTypeInformation.from(obj.getClass()), dbo); } writeInternal(obj, dbo); @@ -566,8 +583,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * the one given. This is usually the case if you store a subtype of the actual declared type of the property. * * @param type - * @param value - * @param dbObject + * @param value must not be {@literal null}. + * @param dbObject must not be {@literal null}. */ protected void addCustomTypeKeyIfNecessary(TypeInformation type, Object value, DBObject dbObject) { @@ -579,7 +596,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App boolean notTheSameClass = !value.getClass().equals(reference); if (notTheSameClass) { - dbObject.put(CUSTOM_TYPE_KEY, value.getClass().getName()); + typeMapper.writeType(value.getClass(), dbObject); } } @@ -711,11 +728,12 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App (BasicDBList) sourceValue); } - Class toType = findTypeToBeUsed((DBObject) sourceValue); + TypeInformation toType = findTypeToBeUsed((DBObject) sourceValue); // It's a complex object, have to read it in if (toType != null) { - dbo.removeField(CUSTOM_TYPE_KEY); + // TODO: why do we remove the type? + // dbo.removeField(CUSTOM_TYPE_KEY); o = read(toType, (DBObject) sourceValue); } else { o = read(mappingContext.getPersistentEntity(prop.getTypeInformation()), (DBObject) sourceValue); @@ -773,7 +791,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Map sourceMap = dbObject.toMap(); for (Entry entry : sourceMap.entrySet()) { - if (entry.getKey().equals(CUSTOM_TYPE_KEY)) { + if (typeMapper.isTypeKey(entry.getKey())) { continue; } @@ -808,30 +826,15 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App * @return the type to be used for converting the given {@link DBObject} into or {@literal null} if there's no type * found. */ - protected Class findTypeToBeUsed(DBObject dbObject) { - - if (dbObject instanceof BasicDBList) { - return List.class; - } - - Object classToBeUsed = dbObject.get(CUSTOM_TYPE_KEY); - - if (classToBeUsed == null) { - return null; - } - - try { - return ClassUtils.forName(classToBeUsed.toString(), null); - } catch (ClassNotFoundException e) { - return null; - } + protected TypeInformation findTypeToBeUsed(DBObject dbObject) { + return typeMapper.readType(dbObject); } private Class getDefaultedTypeToBeUsed(DBObject dbObject) { - Class result = findTypeToBeUsed(dbObject); + TypeInformation result = findTypeToBeUsed(dbObject); if (result != null) { - return result; + return result.getType(); } return dbObject instanceof BasicDBList ? List.class : Map.class; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapper.java new file mode 100644 index 000000000..90723817b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2011 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 org.springframework.data.util.TypeInformation; + +import com.mongodb.DBObject; + +/** + * Interface to define strategies how to store type information in a {@link DBObject}. + * + * @author Oliver Gierke + */ +public interface TypeMapper { + + /** + * Returns whether the given key is the key being used as type key. + * + * @param key + * @return + */ + boolean isTypeKey(String key); + + /** + * Reads the {@link TypeInformation} from the given {@link DBObject}. + * + * @param dbObject must not be {@literal null}. + * @return + */ + TypeInformation readType(DBObject dbObject); + + /** + * Writes type information for the given type into the given {@link DBObject}. + * + * @param type must not be {@literal null}. + * @param dbObject must not be {@literal null}. + */ + void writeType(Class type, DBObject dbObject); + + /** + * Writes type information for the given {@link TypeInformation} into the given {@link DBObject}. + * + * @param type must not be {@literal null}. + * @param dbObject must not be {@literal null}. + */ + void writeType(TypeInformation type, DBObject dbObject); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapperProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapperProvider.java new file mode 100644 index 000000000..23a8130d8 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/TypeMapperProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2011 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; + +/** + * Interfaces for components being able to provide a {@link TypeMapper}. + * + * @author Oliver Gierke + */ +public interface TypeMapperProvider { + + /** + * Returns the {@link TypeMapper}. + * + * @return the {@link TypeMapper} or {@literal null} if none available. + */ + TypeMapper getTypeMapper(); +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java index 0a7b5a009..0c4e00760 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/ConvertingParameterAccessor.java @@ -19,8 +19,9 @@ import java.util.Iterator; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.convert.TypeMapper; +import org.springframework.data.mongodb.core.convert.TypeMapperProvider; import org.springframework.data.mongodb.core.geo.Distance; import org.springframework.data.repository.query.ParameterAccessor; @@ -34,7 +35,7 @@ import com.mongodb.DBObject; */ public class ConvertingParameterAccessor implements MongoParameterAccessor { - private final MongoWriter writer; + private final MongoWriter writer; private final MongoParameterAccessor delegate; /** @@ -42,7 +43,7 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { * * @param writer */ - public ConvertingParameterAccessor(MongoWriter writer, MongoParameterAccessor delegate) { + public ConvertingParameterAccessor(MongoWriter writer, MongoParameterAccessor delegate) { this.writer = writer; this.delegate = delegate; } @@ -98,7 +99,12 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { */ private Object getConvertedValue(Object value) { - return removeTypeInfoRecursively(writer.convertToMongoType(value)); + if (!(writer instanceof TypeMapperProvider)) { + return value; + } + + TypeMapper mapper = ((TypeMapperProvider) writer).getTypeMapper(); + return removeTypeInfoRecursively(writer.convertToMongoType(value), mapper); } /** @@ -107,26 +113,34 @@ public class ConvertingParameterAccessor implements MongoParameterAccessor { * @param object * @return */ - private Object removeTypeInfoRecursively(Object object) { + private Object removeTypeInfoRecursively(Object object, TypeMapper mapper) { - if (!(object instanceof DBObject)) { + if (!(object instanceof DBObject) || mapper == null) { return object; } DBObject dbObject = (DBObject) object; - - dbObject.removeField(MappingMongoConverter.CUSTOM_TYPE_KEY); + String keyToRemove = null; for (String key : dbObject.keySet()) { + + if (mapper.isTypeKey(key)) { + keyToRemove = key; + } + Object value = dbObject.get(key); if (value instanceof BasicDBList) { for (Object element : (BasicDBList) value) { - removeTypeInfoRecursively(element); + removeTypeInfoRecursively(element, mapper); } } else { - removeTypeInfoRecursively(value); + removeTypeInfoRecursively(value, mapper); } } + if (keyToRemove != null) { + dbObject.removeField(keyToRemove); + } + return dbObject; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapperUnitTests.java new file mode 100644 index 000000000..0ae5a2897 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ConfigurableTypeMapperUnitTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2011 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.Matchers.*; +import static org.junit.Assert.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.util.TypeInformation; + +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link ConfigurableTypeMapper}. + * + * @author Oliver Gierke + */ +public class ConfigurableTypeMapperUnitTests { + + ConfigurableTypeMapper mapper; + + @Before + public void setUp() { + mapper = new ConfigurableTypeMapper(Collections.singletonMap(String.class, "1")); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNullTypeMap() { + new ConfigurableTypeMapper(null); + } + + @Test(expected = IllegalArgumentException.class) + public void rejectsNonBijectionalMap() { + Map, String> map = new HashMap, String>(); + map.put(String.class, "1"); + map.put(Object.class, "1"); + + new ConfigurableTypeMapper(map); + } + + @Test + public void writesMapKeyForType() { + writesTypeToField(new BasicDBObject(), String.class, "1"); + writesTypeToField(new BasicDBObject(), Object.class, null); + } + + @Test + public void writesClassNamesForUnmappedValuesIfConfigured() { + mapper.setHandleUnmappedClasses(true); + writesTypeToField(new BasicDBObject(), String.class, "1"); + writesTypeToField(new BasicDBObject(), Object.class, Object.class.getName()); + } + + @Test + public void readsTypeForMapKey() { + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, "1"), String.class); + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, "unmapped"), null); + } + + @Test + public void readsTypeLoadingClassesForUnmappedTypesIfConfigured() { + mapper.setHandleUnmappedClasses(true); + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, "1"), String.class); + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, Object.class.getName()), Object.class); + } + + private void readsTypeFromField(DBObject dbObject, Class type) { + + TypeInformation typeInfo = mapper.readType(dbObject); + + if (type != null) { + assertThat(typeInfo, is(notNullValue())); + assertThat(typeInfo.getType(), is(typeCompatibleWith(type))); + } else { + assertThat(typeInfo, is(nullValue())); + } + } + + private void writesTypeToField(DBObject dbObject, Class type, Object value) { + + mapper.writeType(type, dbObject); + + if (value == null) { + assertThat(dbObject.keySet().isEmpty(), is(true)); + } else { + assertThat(dbObject.containsField(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(true)); + assertThat(dbObject.get(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(value)); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapperUnitTests.java new file mode 100644 index 000000000..5b9c6c5c7 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DefaultTypeMapperUnitTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2011 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.junit.Assert.*; +import static org.hamcrest.Matchers.*; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.util.TypeInformation; + +import com.mongodb.BasicDBList; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; + +/** + * Unit tests for {@link DefaultTypeMapper}. + * + * @author Oliver Gierke + */ +public class DefaultTypeMapperUnitTests { + + DefaultTypeMapper mapper; + + @Before + public void setUp() { + mapper = new DefaultTypeMapper(); + } + + @Test + public void addsFullyQualifiedClassNameUnderDefaultKeyByDefault() { + writesTypeToField(DefaultTypeMapper.DEFAULT_TYPE_KEY, new BasicDBObject(), String.class); + } + + @Test + public void writesTypeToCustomFieldIfConfigured() { + mapper.setTypeKey("_custom"); + writesTypeToField("_custom", new BasicDBObject(), String.class); + } + + @Test + public void doesNotWriteTypeInformationInCaseKeyIsSetToNull() { + mapper.setTypeKey(null); + writesTypeToField(null, new BasicDBObject(), String.class); + } + + @Test + public void readsTypeFromDefaultKeyByDefault() { + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, String.class.getName()), String.class); + } + + @Test + public void readsTypeFromCustomFieldConfigured() { + mapper.setTypeKey("_custom"); + readsTypeFromField(new BasicDBObject("_custom", String.class.getName()), String.class); + } + + @Test + public void returnsListForBasicDBLists() { + readsTypeFromField(new BasicDBList(), List.class); + } + + @Test + public void returnsNullIfNoTypeInfoInDBObject() { + readsTypeFromField(new BasicDBObject(), null); + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, ""), null); + } + + @Test + public void returnsNullIfClassCannotBeLoaded() { + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, "fooBar"), null); + } + + @Test + public void returnsNullIfTypeKeySetToNull() { + mapper.setTypeKey(null); + readsTypeFromField(new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, String.class), null); + } + + @Test + public void returnsCorrectTypeKey() { + + assertThat(mapper.isTypeKey(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(true)); + + mapper.setTypeKey("_custom"); + assertThat(mapper.isTypeKey("_custom"), is(true)); + assertThat(mapper.isTypeKey(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(false)); + + mapper.setTypeKey(null); + assertThat(mapper.isTypeKey("_custom"), is(false)); + assertThat(mapper.isTypeKey(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(false)); + } + + private void readsTypeFromField(DBObject dbObject, Class type) { + + TypeInformation typeInfo = mapper.readType(dbObject); + + if (type != null) { + assertThat(typeInfo, is(notNullValue())); + assertThat(typeInfo.getType(), is(typeCompatibleWith(type))); + } else { + assertThat(typeInfo, is(nullValue())); + } + } + + private void writesTypeToField(String field, DBObject dbObject, Class type) { + + mapper.writeType(type, dbObject); + + if (field == null) { + assertThat(dbObject.keySet().isEmpty(), is(true)); + } else { + assertThat(dbObject.containsField(field), is(true)); + assertThat(dbObject.get(field), is((Object) type.getName())); + } + } +} 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 8d404919e..c578f4eff 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 @@ -153,7 +153,7 @@ public class MappingMongoConverterUnitTests { DBObject dbObject = new BasicDBObject(); dbObject.put("birthDate", new LocalDate()); - dbObject.put(MappingMongoConverter.CUSTOM_TYPE_KEY, Person.class.getName()); + dbObject.put(DefaultTypeMapper.DEFAULT_TYPE_KEY, Person.class.getName()); assertThat(converter.read(Contact.class, dbObject), is(Person.class)); } @@ -166,7 +166,7 @@ public class MappingMongoConverterUnitTests { DBObject dbObject = new BasicDBObject(); dbObject.put("birthDate", new LocalDate()); - dbObject.put(MappingMongoConverter.CUSTOM_TYPE_KEY, Person.class.getName()); + dbObject.put(DefaultTypeMapper.DEFAULT_TYPE_KEY, Person.class.getName()); assertThat(converter.read(BirthDateContainer.class, dbObject), is(BirthDateContainer.class)); } @@ -180,8 +180,8 @@ public class MappingMongoConverterUnitTests { DBObject result = new BasicDBObject(); converter.write(person, result); - assertThat(result.containsField(MappingMongoConverter.CUSTOM_TYPE_KEY), is(true)); - assertThat(result.get(MappingMongoConverter.CUSTOM_TYPE_KEY).toString(), is(Person.class.getName())); + assertThat(result.containsField(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(true)); + assertThat(result.get(DefaultTypeMapper.DEFAULT_TYPE_KEY).toString(), is(Person.class.getName())); } /** @@ -295,7 +295,7 @@ public class MappingMongoConverterUnitTests { BasicDBList contacts = (BasicDBList) result; DBObject personDbObject = (DBObject) contacts.get(0); assertThat(personDbObject.get("foo").toString(), is("Oliver")); - assertThat((String) personDbObject.get(MappingMongoConverter.CUSTOM_TYPE_KEY), is(Person.class.getName())); + assertThat((String) personDbObject.get(DefaultTypeMapper.DEFAULT_TYPE_KEY), is(Person.class.getName())); } /** @@ -304,7 +304,7 @@ public class MappingMongoConverterUnitTests { @Test public void readsCollectionWithInterfaceCorrectly() { - BasicDBObject person = new BasicDBObject(MappingMongoConverter.CUSTOM_TYPE_KEY, Person.class.getName()); + BasicDBObject person = new BasicDBObject(DefaultTypeMapper.DEFAULT_TYPE_KEY, Person.class.getName()); person.put("foo", "Oliver"); BasicDBList contacts = new BasicDBList(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StringBasedMongoQueryUnitTests.java index 92bf3536b..5eb6c18cd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/StringBasedMongoQueryUnitTests.java @@ -28,6 +28,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.data.mongodb.MongoDbFactory; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.DefaultTypeMapper; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -93,7 +94,7 @@ public class StringBasedMongoQueryUnitTests { DBObject dbObject = new BasicDBObject(); converter.write(address, dbObject); - dbObject.removeField(MappingMongoConverter.CUSTOM_TYPE_KEY); + dbObject.removeField(DefaultTypeMapper.DEFAULT_TYPE_KEY); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor); BasicDBObject queryObject = new BasicDBObject("address", dbObject); @@ -114,7 +115,7 @@ public class StringBasedMongoQueryUnitTests { DBObject addressDbObject = new BasicDBObject(); converter.write(address, addressDbObject); - addressDbObject.removeField(MappingMongoConverter.CUSTOM_TYPE_KEY); + addressDbObject.removeField(DefaultTypeMapper.DEFAULT_TYPE_KEY); DBObject reference = new BasicDBObject("address", addressDbObject); reference.put("lastname", "Matthews");