diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/config/MappingMongoConverterParser.java index 17e1800ab..8b6defefd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/config/MappingMongoConverterParser.java @@ -30,6 +30,7 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; @@ -37,6 +38,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.data.annotation.Persistent; +import org.springframework.data.document.mongodb.convert.CustomConversions; import org.springframework.data.document.mongodb.convert.MappingMongoConverter; import org.springframework.data.document.mongodb.mapping.Document; import org.springframework.data.document.mongodb.mapping.MongoMappingContext; @@ -64,20 +66,9 @@ public class MappingMongoConverterParser extends AbstractBeanDefinitionParser { @Override protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { BeanDefinitionRegistry registry = parserContext.getRegistry(); - - String ctxRef = element.getAttribute("mapping-context-ref"); - if (!StringUtils.hasText(ctxRef)) { - BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder - .genericBeanDefinition(MongoMappingContext.class); - - Set classesToAdd = getInititalEntityClasses(element, mappingContextBuilder); - if (classesToAdd != null) { - mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd); - } - - registry.registerBeanDefinition(MAPPING_CONTEXT, mappingContextBuilder.getBeanDefinition()); - ctxRef = MAPPING_CONTEXT; - } + + BeanDefinition conversionsDefinition = getCustomConversions(element, parserContext); + String ctxRef = potentiallyCreateMappingContext(element, parserContext, conversionsDefinition); try { registry.getBeanDefinition(POST_PROCESSOR); @@ -93,12 +84,15 @@ public class MappingMongoConverterParser extends AbstractBeanDefinitionParser { if (!StringUtils.hasText(dbFactoryRef)) { dbFactoryRef = DB_FACTORY; } - - + + // Converter BeanDefinitionBuilder converterBuilder = BeanDefinitionBuilder.genericBeanDefinition(MappingMongoConverter.class); converterBuilder.addConstructorArgReference(dbFactoryRef); converterBuilder.addConstructorArgReference(ctxRef); + if (conversionsDefinition != null) { + converterBuilder.addPropertyValue("customConversions", conversionsDefinition); + } try { registry.getBeanDefinition(INDEX_HELPER); @@ -112,20 +106,62 @@ public class MappingMongoConverterParser extends AbstractBeanDefinitionParser { registry.registerBeanDefinition(INDEX_HELPER, indexHelperBuilder.getBeanDefinition()); } + return converterBuilder.getBeanDefinition(); + } + + private String potentiallyCreateMappingContext(Element element, ParserContext parserContext, BeanDefinition conversionsDefinition) { + + String ctxRef = element.getAttribute("mapping-context-ref"); + if (!StringUtils.hasText(ctxRef)) { + BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder + .genericBeanDefinition(MongoMappingContext.class); + + Set classesToAdd = getInititalEntityClasses(element, mappingContextBuilder); + if (classesToAdd != null) { + mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd); + } + + if (conversionsDefinition != null) { + AbstractBeanDefinition simpleTypesDefinition = new GenericBeanDefinition(); + simpleTypesDefinition.setFactoryBeanName("customConversions"); + simpleTypesDefinition.setFactoryMethodName("getSimpleTypeHolder"); + + mappingContextBuilder.addPropertyValue("simpleTypeHolder", simpleTypesDefinition); + } + + parserContext.getRegistry().registerBeanDefinition(MAPPING_CONTEXT, mappingContextBuilder.getBeanDefinition()); + ctxRef = MAPPING_CONTEXT; + } + + return ctxRef; + } + + private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { + List customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters"); + if (customConvertersElements.size() == 1) { Element customerConvertersElement = customConvertersElements.get(0); ManagedList converterBeans = new ManagedList(); - List listenerElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter"); - if (listenerElements != null) { - for (Element listenerElement : listenerElements) { + List converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter"); + if (converterElements != null) { + for (Element listenerElement : converterElements) { converterBeans.add(parseConverter(listenerElement, parserContext)); } } - converterBuilder.addPropertyValue("customConverters", converterBeans); + + BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(CustomConversions.class); + conversionsBuilder.addConstructorArgValue(converterBeans); + + AbstractBeanDefinition conversionsBean = conversionsBuilder.getBeanDefinition(); + conversionsBean.setSource(parserContext.extractSource(element)); + + parserContext.getRegistry().registerBeanDefinition("customConversions", conversionsBean); + + return conversionsBean; } - - return converterBuilder.getBeanDefinition(); + + return null; } public Set getInititalEntityClasses(Element element, BeanDefinitionBuilder builder) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/AbstractMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/AbstractMongoConverter.java index 4919efbc7..7b3a8c5bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/AbstractMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/AbstractMongoConverter.java @@ -16,72 +16,55 @@ package org.springframework.data.document.mongodb.convert; -import static org.springframework.data.mapping.MappingBeanHelper.isSimpleType; - import java.math.BigInteger; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; -import java.util.Locale; import java.util.Map; -import java.util.Set; - import org.bson.types.ObjectId; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.GenericTypeResolver; import org.springframework.core.convert.ConversionService; -import org.springframework.core.convert.TypeDescriptor; -import org.springframework.core.convert.converter.Converter; -import org.springframework.core.convert.converter.ConverterFactory; -import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.core.convert.support.GenericConversionService; import org.springframework.data.document.mongodb.convert.ObjectIdConverters.BigIntegerToObjectIdConverter; import org.springframework.data.document.mongodb.convert.ObjectIdConverters.ObjectIdToBigIntegerConverter; import org.springframework.data.document.mongodb.convert.ObjectIdConverters.ObjectIdToStringConverter; import org.springframework.data.document.mongodb.convert.ObjectIdConverters.StringToObjectIdConverter; -import org.springframework.data.mapping.MappingBeanHelper; - import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DBObject; /** + * Base class for {@link MongoConverter} implementations. Sets up a {@link GenericConversionService} and populates basic + * converters. Allows registering {@link CustomConversions}. + * * @author Jon Brisbin + * @author Oliver Gierke ogierke@vmware.com */ public abstract class AbstractMongoConverter implements MongoConverter, InitializingBean { - @SuppressWarnings({ "unchecked" }) - private static final List> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class, - DBObject.class); - protected final GenericConversionService conversionService; - private final Set customTypeMapping = new HashSet(); + protected CustomConversions conversions = new CustomConversions(); + /** + * Creates a new {@link AbstractMongoConverter} using the given {@link GenericConversionService}. + * + * @param conversionService + */ public AbstractMongoConverter(GenericConversionService conversionService) { this.conversionService = conversionService == null ? ConversionServiceFactory.createDefaultConversionService() : conversionService; this.conversionService.removeConvertible(Object.class, String.class); - registerConverter(CustomToStringConverter.INSTANCE); } /** - * Add custom {@link Converter} or {@link ConverterFactory} instances to be used that will take presidence over - * metadata driven conversion between of objects to/from DBObject + * Registers the given custom conversions with the converter. * - * @param converters + * @param conversions */ - public void setCustomConverters(Set converters) { - if (null != converters) { - for (Object c : converters) { - registerConverter(c); - } - } + public void setCustomConversions(CustomConversions conversions) { + this.conversions = conversions; } /** @@ -102,61 +85,10 @@ public abstract class AbstractMongoConverter implements MongoConverter, Initiali if (!conversionService.canConvert(BigInteger.class, ObjectId.class)) { conversionService.addConverter(BigIntegerToObjectIdConverter.INSTANCE); } - } - - /** - * Inspects the given {@link Converter} for the types it can convert and registers the pair for custom type conversion - * in case the target type is a Mongo basic type. - * - * @param converter - */ - private void registerConverter(Object converter) { - if (converter instanceof GenericConverter) { - customTypeMapping.addAll(((GenericConverter) converter).getConvertibleTypes()); - } else { - Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class); - if (MONGO_TYPES.contains(arguments[1]) || MONGO_TYPES.contains(arguments[0])) { - customTypeMapping.add(new ConvertiblePair(arguments[0], arguments[1])); - } - } - - boolean added = false; - - if (converter instanceof Converter) { - this.conversionService.addConverter((Converter) converter); - added = true; - } - - if (converter instanceof ConverterFactory) { - this.conversionService.addConverterFactory((ConverterFactory) converter); - added = true; - } - - if (converter instanceof GenericConverter) { - this.conversionService.addConverter((GenericConverter) converter); - added = true; - } - - if (!added) { - throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!"); - } + conversions.registerConvertersIn(conversionService); } - protected Class getCustomTarget(Class source, Class expectedTargetType) { - for (ConvertiblePair typePair : customTypeMapping) { - if (typePair.getSourceType().isAssignableFrom(source)) { - - Class targetType = typePair.getTargetType(); - - if (targetType.equals(expectedTargetType) || expectedTargetType == null) { - return targetType; - } - } - } - - return null; - } /* * (non-Javadoc) @@ -179,7 +111,7 @@ public abstract class AbstractMongoConverter implements MongoConverter, Initiali return ((Enum) obj).name(); } - if (null != obj && isSimpleType(obj.getClass())) { + if (null != obj && conversions.isSimpleType(obj.getClass())) { // Doesn't need conversion return obj; } @@ -240,19 +172,4 @@ public abstract class AbstractMongoConverter implements MongoConverter, Initiali } return newDbl; } - - - private enum CustomToStringConverter implements GenericConverter { - INSTANCE; - - public Set getConvertibleTypes() { - ConvertiblePair localeToString = new ConvertiblePair(Locale.class, String.class); - ConvertiblePair booleanToString = new ConvertiblePair(Character.class, String.class); - return new HashSet(Arrays.asList(localeToString, booleanToString)); - } - - public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { - return source.toString(); - } - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/CustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/CustomConversions.java new file mode 100644 index 000000000..6c8010857 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/CustomConversions.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2011 by the original author(s). + * + * 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.document.mongodb.convert; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.mapping.SimpleTypeHolder; +import org.springframework.util.Assert; + +import com.mongodb.DBObject; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. The converters are pretty much builds up two sets of types which Mongo basic types {@see #MONGO_TYPES} + * can be converted into and from. These types will be considered simple ones (which means they neither need deeper + * inspection nor nested conversion. Thus the {@link CustomConversions} also act as factory for {@link SimpleTypeHolder}. + * + * @author Oliver Gierke + */ +public class CustomConversions { + + @SuppressWarnings({ "unchecked" }) + private static final List> MONGO_TYPES = Arrays.asList(Number.class, Date.class, String.class, + DBObject.class); + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + /** + * Creates an empty {@link CustomConversions} object. + */ + CustomConversions() { + this(new ArrayList()); + } + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param converters + */ + public CustomConversions(List converters) { + + Assert.notNull(converters); + + this.readingPairs = new HashSet(); + this.writingPairs = new HashSet(); + this.customSimpleTypes = new HashSet>(); + + registerConversion(CustomToStringConverter.INSTANCE); + + for (Object c : converters) { + registerConversion(c); + } + + this.converters = new ArrayList(); + this.converters.add(CustomToStringConverter.INSTANCE); + this.converters.addAll(converters); + + this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, true); + } + + /** + * Returns the underlying {@link SimpleTypeHolder}. + * + * @return + */ + public SimpleTypeHolder getSimpleTypeHolder() { + return simpleTypeHolder; + } + + /** + * Returns whether the given type is considered to be simple. + * + * @param type + * @return + */ + public boolean isSimpleType(Class type) { + return simpleTypeHolder.isSimpleType(type); + } + + /** + * Populates the given {@link GenericConversionService} with the convertes registered. + * + * @param conversionService + */ + public void registerConvertersIn(GenericConversionService conversionService) { + + for (Object converter : converters) { + + boolean added = false; + + if (converter instanceof Converter) { + conversionService.addConverter((Converter) converter); + added = true; + } + + if (converter instanceof ConverterFactory) { + conversionService.addConverterFactory((ConverterFactory) converter); + added = true; + } + + if (converter instanceof GenericConverter) { + conversionService.addConverter((GenericConverter) converter); + added = true; + } + + if (!added) { + throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!"); + } + } + } + + /** + * Registers a conversion for the given converter. Inspects either generics or the {@link ConvertiblePair}s returned + * by a {@link GenericConverter}. + * + * @param converter + */ + private void registerConversion(Object converter) { + + if (converter instanceof GenericConverter) { + GenericConverter genericConverter = (GenericConverter) converter; + for (ConvertiblePair pair : genericConverter.getConvertibleTypes()) { + register(pair); + } + } else if (converter instanceof Converter){ + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class); + register(new ConvertiblePair(arguments[0], arguments[1])); + } else { + throw new IllegalArgumentException("Unsupported Converter type!"); + } + } + + /** + * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic + * Mongo types. + * + * @param pair + */ + private void register(ConvertiblePair pair) { + + if (isMongoBasicType(pair.getSourceType())) { + readingPairs.add(pair); + customSimpleTypes.add(pair.getTargetType()); + } + + if (isMongoBasicType(pair.getTargetType())) { + writingPairs.add(pair); + customSimpleTypes.add(pair.getSourceType()); + } + } + + /** + * Returns the target type we can write an onject of the given source type to. The returned type might be a subclass + * oth the given expected type though. If {@code expexctedTargetType} is {@literal null} we will simply return the + * first target type matching or {@literal null} if noe conversion can be found. + * + * @param source must not be {@literal null} + * @param expectedTargetType + * @return + */ + public Class getCustomWriteTarget(Class source, Class expectedTargetType) { + Assert.notNull(source); + return getCustomTarget(source, expectedTargetType, writingPairs); + } + + /** + * Returns whether we have a custom conversion registered to read the given source into the given target type. + * + * @param source must not be {@literal null} + * @param expectedTargetType must not be {@literal null} + * @return + */ + public boolean hasCustomReadTarget(Class source, Class expectedTargetType) { + Assert.notNull(source); + Assert.notNull(expectedTargetType); + return getCustomTarget(source, expectedTargetType, readingPairs) != null; + } + + /** + * Inspects the given {@link ConvertiblePair} for ones that have a source compatible type as source. Additionally + * checks assignabilty of the target type if one is given. + * + * @param source must not be {@literal null} + * @param expectedTargetType + * @param pairs must not be {@literal null} + * @return + */ + private static Class getCustomTarget(Class source, Class expectedTargetType, Iterable pairs) { + + Assert.notNull(source); + Assert.notNull(pairs); + + for (ConvertiblePair typePair : pairs) { + if (typePair.getSourceType().isAssignableFrom(source)) { + Class targetType = typePair.getTargetType(); + if (expectedTargetType == null || targetType.isAssignableFrom(expectedTargetType)) { + return targetType; + } + } + } + + return null; + } + + /** + * Returns whether the given type is a type that Mongo can handle basically. + * + * @param type + * @return + */ + private static boolean isMongoBasicType(Class type) { + return MONGO_TYPES.contains(type); + } + + + private enum CustomToStringConverter implements GenericConverter { + INSTANCE; + + public Set getConvertibleTypes() { + ConvertiblePair localeToString = new ConvertiblePair(Locale.class, String.class); + ConvertiblePair booleanToString = new ConvertiblePair(Character.class, String.class); + return new HashSet(Arrays.asList(localeToString, booleanToString)); + } + + public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + return source.toString(); + } + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java index bbe3c4e3d..c2c9a0a0c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/convert/MappingMongoConverter.java @@ -16,8 +16,6 @@ package org.springframework.data.document.mongodb.convert; -import static org.springframework.data.mapping.MappingBeanHelper.*; - import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.math.BigInteger; @@ -138,9 +136,8 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App TypeInformation typeToUse = getMoreConcreteTargetType(dbo, type); Class rawType = typeToUse.getType(); - Class customTarget = getCustomTarget(rawType, DBObject.class); - if (customTarget != null) { + if (conversions.hasCustomReadTarget(DBObject.class, rawType)) { return conversionService.convert(dbo, rawType); } @@ -279,7 +276,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return; } - boolean handledByCustomConverter = getCustomTarget(obj.getClass(), DBObject.class) != null; + boolean handledByCustomConverter = conversions.getCustomWriteTarget(obj.getClass(), DBObject.class) != null; if (!handledByCustomConverter) { dbo.put(CUSTOM_TYPE_KEY, obj.getClass().getName()); @@ -301,7 +298,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return; } - Class customTarget = getCustomTarget(obj.getClass(), DBObject.class); + Class customTarget = conversions.getCustomWriteTarget(obj.getClass(), DBObject.class); if (customTarget != null) { DBObject result = conversionService.convert(obj, DBObject.class); @@ -374,7 +371,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App throw new MappingException(e.getMessage(), e); } if (null != propertyObj) { - if (!isSimpleType(propertyObj.getClass())) { + if (!conversions.isSimpleType(propertyObj.getClass())) { writePropertyInternal(prop, propertyObj, dbo); } else { writeSimpleInternal(prop.getFieldName(), propertyObj, dbo); @@ -437,7 +434,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App } // Lookup potential custom target type - Class basicTargetType = getCustomTarget(obj.getClass(), null); + Class basicTargetType = conversions.getCustomWriteTarget(obj.getClass(), null); if (basicTargetType != null) { dbo.put(name, conversionService.convert(obj, basicTargetType)); @@ -516,7 +513,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App Class elementType = element.getClass(); - if (isSimpleType(elementType)) { + if (conversions.isSimpleType(elementType)) { dbList.add(element); } else if (element instanceof Collection || elementType.isArray()) { dbList.add(createCollectionDBObject(componentType, asCollection(element))); @@ -535,11 +532,11 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App for (Map.Entry entry : obj.entrySet()) { Object key = entry.getKey(); Object val = entry.getValue(); - if (isSimpleType(key.getClass())) { + if (conversions.isSimpleType(key.getClass())) { // Don't use conversion service here as removal of ObjectToString converter results in some primitive types not // being convertable String simpleKey = key.toString(); - if (isSimpleType(val.getClass())) { + if (conversions.isSimpleType(val.getClass())) { writeSimpleInternal(simpleKey, val, dbo); } else { DBObject newDbo = new BasicDBObject(); @@ -602,7 +599,7 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App */ private void writeSimpleInternal(String key, Object value, DBObject dbObject) { - Class customTarget = getCustomTarget(value.getClass(), null); + Class customTarget = conversions.getCustomWriteTarget(value.getClass(), null); Object valueToSet = null; if (customTarget != null) { @@ -652,30 +649,29 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App o = x.getValue(ctx); } else { - Object dbObj = dbo.get(prop.getFieldName()); + Object sourceValue = dbo.get(prop.getFieldName()); - if (dbObj == null) { + if (sourceValue == null) { return null; } Class propertyType = prop.getType(); - Class customTarget = getCustomTarget(dbObj.getClass(), propertyType); - if (customTarget != null) { - return conversionService.convert(dbObj, propertyType); + if (conversions.hasCustomReadTarget(sourceValue.getClass(), propertyType)) { + return conversionService.convert(sourceValue, propertyType); } - if (dbObj instanceof DBRef) { - dbObj = ((DBRef) dbObj).fetch(); + if (sourceValue instanceof DBRef) { + sourceValue = ((DBRef) sourceValue).fetch(); } - if (dbObj instanceof DBObject) { + if (sourceValue instanceof DBObject) { if (prop.isMap()) { - return readMap(prop.getTypeInformation(), (DBObject) dbObj); - } else if (prop.isArray() && dbObj instanceof BasicDBObject && ((DBObject) dbObj).keySet().size() == 0) { + return readMap(prop.getTypeInformation(), (DBObject) sourceValue); + } else if (prop.isArray() && sourceValue instanceof BasicDBObject && ((DBObject) sourceValue).keySet().size() == 0) { // It's empty return Array.newInstance(prop.getComponentType(), 0); - } else if (prop.isCollection() && dbObj instanceof BasicDBList) { - BasicDBList dbObjList = (BasicDBList) dbObj; + } else if (prop.isCollection() && sourceValue instanceof BasicDBList) { + BasicDBList dbObjList = (BasicDBList) sourceValue; List items = new LinkedList(); for (int i = 0; i < dbObjList.size(); i++) { Object dbObjItem = dbObjList.get(i); @@ -694,17 +690,17 @@ public class MappingMongoConverter extends AbstractMongoConverter implements App return itemsToReturn; } - Class toType = findTypeToBeUsed((DBObject) dbObj); + Class toType = findTypeToBeUsed((DBObject) sourceValue); // It's a complex object, have to read it in if (toType != null) { dbo.removeField(CUSTOM_TYPE_KEY); - o = read(toType, (DBObject) dbObj); + o = read(toType, (DBObject) sourceValue); } else { - o = read(mappingContext.getPersistentEntity(prop.getTypeInformation()), (DBObject) dbObj); + o = read(mappingContext.getPersistentEntity(prop.getTypeInformation()), (DBObject) sourceValue); } } else { - o = dbObj; + o = sourceValue; } } return o; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentProperty.java index b9c1ba8ea..039691121 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentProperty.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.types.ObjectId; import org.springframework.data.mapping.AnnotationBasedPersistentProperty; +import org.springframework.data.mapping.SimpleTypeHolder; import org.springframework.data.mapping.model.Association; /** @@ -56,9 +57,11 @@ public class BasicMongoPersistentProperty extends AnnotationBasedPersistentPrope * @param field * @param propertyDescriptor * @param owner + * @param simpleTypeHolder */ - public BasicMongoPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, MongoPersistentEntity owner) { - super(field, propertyDescriptor, owner); + public BasicMongoPersistentProperty(Field field, PropertyDescriptor propertyDescriptor, + MongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); if (isIdProperty() && field.isAnnotationPresent(FieldName.class)) { LOG.warn(String.format("Invalid usage of %s on id property. Field name will not be considered!", FieldName.class)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/MongoMappingContext.java index 6caf07b57..3a2820156 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/MongoMappingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/MongoMappingContext.java @@ -18,12 +18,13 @@ package org.springframework.data.document.mongodb.mapping; import java.beans.PropertyDescriptor; import java.lang.reflect.Field; +import java.util.HashSet; import java.util.Set; import org.bson.types.CodeWScope; import org.bson.types.ObjectId; import org.springframework.data.mapping.AbstractMappingContext; -import org.springframework.data.mapping.MappingBeanHelper; +import org.springframework.data.mapping.SimpleTypeHolder; import org.springframework.data.util.TypeInformation; /** @@ -31,23 +32,33 @@ import org.springframework.data.util.TypeInformation; * @author Oliver Gierke ogierke@vmware.com */ public class MongoMappingContext extends AbstractMappingContext, MongoPersistentProperty> { - public MongoMappingContext() { - augmentSimpleTypes(); + + private static final Set> MONGO_SIMPLE_TYPES = new HashSet>(); + + static { + MONGO_SIMPLE_TYPES.add(com.mongodb.DBRef.class); + MONGO_SIMPLE_TYPES.add(ObjectId.class); + MONGO_SIMPLE_TYPES.add(CodeWScope.class); + MONGO_SIMPLE_TYPES.add(Character.class); } - protected void augmentSimpleTypes() { - // Augment simpleTypes with MongoDB-specific classes - Set> simpleTypes = MappingBeanHelper.getSimpleTypes(); - simpleTypes.add(com.mongodb.DBRef.class); - simpleTypes.add(ObjectId.class); - simpleTypes.add(CodeWScope.class); - simpleTypes.add(Character.class); + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.AbstractMappingContext#setSimpleTypeHolder(org.springframework.data.mapping.SimpleTypeHolder) + */ + @Override + public void setSimpleTypeHolder(SimpleTypeHolder simpleTypes) { + super.setSimpleTypeHolder(new SimpleTypeHolder(MONGO_SIMPLE_TYPES, simpleTypes)); } + /* + * (non-Javadoc) + * @see org.springframework.data.mapping.AbstractMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.mapping.MutablePersistentEntity, org.springframework.data.mapping.SimpleTypeHolder) + */ @Override public MongoPersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, - BasicMongoPersistentEntity owner) { - return new BasicMongoPersistentProperty(field, descriptor, owner); + BasicMongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new BasicMongoPersistentProperty(field, descriptor, owner, simpleTypeHolder); } /* (non-Javadoc) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/SimpleMongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/SimpleMongoMappingContext.java index 474cb6509..c8eb58717 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/SimpleMongoMappingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/document/mongodb/mapping/SimpleMongoMappingContext.java @@ -24,6 +24,7 @@ import org.springframework.data.document.mongodb.MongoCollectionUtils; import org.springframework.data.mapping.AbstractMappingContext; import org.springframework.data.mapping.BasicPersistentEntity; import org.springframework.data.mapping.AbstractPersistentProperty; +import org.springframework.data.mapping.SimpleTypeHolder; import org.springframework.data.mapping.model.Association; import org.springframework.data.util.TypeInformation; @@ -47,8 +48,8 @@ public class SimpleMongoMappingContext extends */ @Override protected SimplePersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor, - SimpleMongoPersistentEntity owner) { - return new SimplePersistentProperty(field, descriptor, owner); + SimpleMongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + return new SimplePersistentProperty(field, descriptor, owner, simpleTypeHolder); } static class SimplePersistentProperty extends AbstractPersistentProperty implements @@ -63,8 +64,8 @@ public class SimpleMongoMappingContext extends * @param propertyDescriptor * @param information */ - public SimplePersistentProperty(Field field, PropertyDescriptor propertyDescriptor, MongoPersistentEntity owner) { - super(field, propertyDescriptor, owner); + public SimplePersistentProperty(Field field, PropertyDescriptor propertyDescriptor, MongoPersistentEntity owner, SimpleTypeHolder simpleTypeHolder) { + super(field, propertyDescriptor, owner, simpleTypeHolder); } /* (non-Javadoc) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java index 6ec056a61..4815e1be2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/MongoOperationsUnitTests.java @@ -20,8 +20,6 @@ import static org.junit.Assert.*; import java.util.Arrays; import java.util.List; -import java.util.Set; - import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import org.bson.types.ObjectId; @@ -83,10 +81,6 @@ public abstract class MongoOperationsUnitTests { public MappingContext, MongoPersistentProperty> getMappingContext() { return null; } - - @Override - public void setCustomConverters(Set converters) { - } }; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/SimpleMongoConverterTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/SimpleMongoConverterTests.java index 10cc6cd8b..9aad7bb3e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/SimpleMongoConverterTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/SimpleMongoConverterTests.java @@ -29,11 +29,8 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; - import org.hamcrest.CoreMatchers; import org.joda.time.LocalDate; import org.junit.Before; @@ -41,6 +38,7 @@ import org.junit.Test; import org.springframework.core.convert.converter.Converter; import org.springframework.data.document.mongodb.SomeEnumTest.NumberEnum; import org.springframework.data.document.mongodb.SomeEnumTest.StringEnum; +import org.springframework.data.document.mongodb.convert.CustomConversions; import org.springframework.data.document.mongodb.convert.SimpleMongoConverter; import org.springframework.util.ReflectionUtils; @@ -48,6 +46,7 @@ import com.mongodb.BasicDBObject; import com.mongodb.DBObject; import com.mongodb.util.JSON; +@SuppressWarnings("deprecation") public class SimpleMongoConverterTests { static final String SIMPLE_JSON = "{ \"map\" : { \"foo\" : 3 , \"bar\" : 4}, \"number\" : 15 }"; @@ -329,11 +328,12 @@ public class SimpleMongoConverterTests { @Test public void convertsJodaTimeTypesCorrectly() { - Set> converters = new HashSet>(); + List> converters = new ArrayList>(); converters.add(new LocalDateToDateConverter()); converters.add(new DateToLocalDateConverter()); - converter.setCustomConverters(converters); + converter.setCustomConversions(new CustomConversions(converters)); + converter.afterPropertiesSet(); AnotherPerson person = new AnotherPerson(); person.birthDate = new LocalDate(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/TestMongoConfiguration.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/TestMongoConfiguration.java index a48b14bc4..8ac9acf52 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/TestMongoConfiguration.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/TestMongoConfiguration.java @@ -1,12 +1,12 @@ package org.springframework.data.document.mongodb; -import java.util.HashSet; -import java.util.Set; - +import java.util.ArrayList; +import java.util.List; import com.mongodb.Mongo; import org.springframework.context.annotation.Bean; import org.springframework.core.convert.converter.Converter; import org.springframework.data.document.mongodb.config.AbstractMongoConfiguration; +import org.springframework.data.document.mongodb.convert.CustomConversions; import org.springframework.data.document.mongodb.convert.MappingMongoConverter; public class TestMongoConfiguration extends AbstractMongoConfiguration { @@ -16,6 +16,7 @@ public class TestMongoConfiguration extends AbstractMongoConfiguration { return "database"; } + @Override @Bean public Mongo mongo() throws Exception { return new Mongo("localhost", 27017); @@ -28,10 +29,10 @@ public class TestMongoConfiguration extends AbstractMongoConfiguration { @Override protected void afterMappingMongoConverterCreation(MappingMongoConverter converter) { - Set> converterList = new HashSet>(); - converterList.add(new org.springframework.data.document.mongodb.PersonReadConverter()); - converterList.add(new org.springframework.data.document.mongodb.PersonWriteConverter()); - converter.setCustomConverters(converterList); + List> converters = new ArrayList>(); + converters.add(new org.springframework.data.document.mongodb.PersonReadConverter()); + converters.add(new org.springframework.data.document.mongodb.PersonWriteConverter()); + converter.setCustomConversions(new CustomConversions(converters)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConversionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConversionsUnitTests.java new file mode 100644 index 000000000..1cb424a8b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConversionsUnitTests.java @@ -0,0 +1,95 @@ +package org.springframework.data.document.mongodb.convert; + +import static org.junit.Assert.*; +import static org.hamcrest.Matchers.*; + +import java.util.Arrays; +import java.util.Locale; +import java.util.UUID; + +import org.junit.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.ConversionServiceFactory; +import org.springframework.core.convert.support.GenericConversionService; + +/** + * Unit tests for {@link CustomConversions}. + * + * @author Oliver Gierke + */ +public class CustomConversionsUnitTests { + + @Test + @SuppressWarnings("unchecked") + public void findsBasicReadAndWriteConversions() { + + CustomConversions conversions = new CustomConversions(Arrays.asList(UuidToStringConverter.INSTANCE, + StringToUUIDConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(UUID.class, null), is(typeCompatibleWith(String.class))); + assertThat(conversions.getCustomWriteTarget(String.class, null), is(nullValue())); + + assertThat(conversions.hasCustomReadTarget(String.class, UUID.class), is(true)); + assertThat(conversions.hasCustomReadTarget(String.class, Locale.class), is(false)); + } + + @Test + @SuppressWarnings("unchecked") + public void considersSubtypesCorrectly() { + + CustomConversions conversions = new CustomConversions(Arrays.asList( + NumberToStringConverter.INSTANCE, StringToNumberConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Long.class, null), is(typeCompatibleWith(String.class))); + assertThat(conversions.hasCustomReadTarget(String.class, Long.class), is(true)); + } + + @Test + public void considersTypesWeRegisteredConvertersForAsSimple() { + + CustomConversions conversions = new CustomConversions( Arrays.asList(UuidToStringConverter.INSTANCE)); + assertThat(conversions.isSimpleType(UUID.class), is(true)); + } + + @Test + public void populatesConversionServiceCorrectly() { + + GenericConversionService conversionService = ConversionServiceFactory.createDefaultConversionService(); + assertThat(conversionService.canConvert(String.class, UUID.class), is(false)); + + CustomConversions conversions = new CustomConversions( + Arrays.asList(StringToUUIDConverter.INSTANCE)); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.canConvert(String.class, UUID.class), is(true)); + } + + enum UuidToStringConverter implements Converter { + INSTANCE; + + public String convert(UUID source) { + return source.toString(); + } + } + + enum StringToUUIDConverter implements Converter { + INSTANCE; + public UUID convert(String source) { + return UUID.fromString(source); + } + } + + enum NumberToStringConverter implements Converter { + INSTANCE; + public String convert(Number source) { + return source.toString(); + } + } + + enum StringToNumberConverter implements Converter { + INSTANCE; + public Number convert(String source) { + return 0L; + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConvertersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConvertersUnitTests.java index f68a93a06..483fe9f20 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConvertersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/convert/CustomConvertersUnitTests.java @@ -61,15 +61,19 @@ public class CustomConvertersUnitTests { @SuppressWarnings("unchecked") public void setUp() throws Exception { + when(barToDBObjectConverter.convert(any(Bar.class))).thenReturn(new BasicDBObject()); + when(dbObjectToBarConverter.convert(any(DBObject.class))).thenReturn(new Bar()); + + CustomConversions conversions = new CustomConversions(Arrays.asList(barToDBObjectConverter, dbObjectToBarConverter)); + context = new MongoMappingContext(); context.setInitialEntitySet(new HashSet>(Arrays.asList(Foo.class, Bar.class))); + context.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); context.afterPropertiesSet(); - when(barToDBObjectConverter.convert(any(Bar.class))).thenReturn(new BasicDBObject()); - when(dbObjectToBarConverter.convert(any(DBObject.class))).thenReturn(new Bar()); - converter = new MappingMongoConverter(mongoDbFactory, context); - converter.setCustomConverters(new HashSet(Arrays.asList(barToDBObjectConverter, dbObjectToBarConverter))); + converter.setCustomConversions(conversions); + converter.afterPropertiesSet(); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentPropertyUnitTests.java index be1e0d0a5..7b9a6b020 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentPropertyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/BasicMongoPersistentPropertyUnitTests.java @@ -22,6 +22,7 @@ import java.lang.reflect.Field; import org.junit.Before; import org.junit.Test; import org.springframework.data.annotation.Id; +import org.springframework.data.mapping.SimpleTypeHolder; import org.springframework.data.util.ClassTypeInformation; import org.springframework.util.ReflectionUtils; @@ -62,7 +63,7 @@ public class BasicMongoPersistentPropertyUnitTests { } private MongoPersistentProperty getPropertyFor(Field field) { - return new BasicMongoPersistentProperty(field, null, entity); + return new BasicMongoPersistentProperty(field, null, entity, new SimpleTypeHolder()); } class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java index 26c4452d2..7b86b5c9e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/document/mongodb/mapping/MappingMongoConverterUnitTests.java @@ -25,13 +25,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; - import org.joda.time.LocalDate; import org.junit.Before; import org.junit.Test; @@ -40,6 +37,7 @@ import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.core.convert.converter.Converter; import org.springframework.data.document.mongodb.MongoDbFactory; +import org.springframework.data.document.mongodb.convert.CustomConversions; import org.springframework.data.document.mongodb.convert.MappingMongoConverter; import com.mongodb.BasicDBList; @@ -61,8 +59,12 @@ public class MappingMongoConverterUnitTests { @Before public void setUp() { + mappingContext = new MongoMappingContext(); + mappingContext.afterPropertiesSet(); + converter = new MappingMongoConverter(factory, mappingContext); + converter.afterPropertiesSet(); } @Test @@ -83,16 +85,15 @@ public class MappingMongoConverterUnitTests { @Test public void convertsJodaTimeTypesCorrectly() { - Set> converters = new HashSet>(); + List> converters = new ArrayList>(); converters.add(new LocalDateToDateConverter()); converters.add(new DateToLocalDateConverter()); - - List> customSimpleTypes = new ArrayList>(); - customSimpleTypes.add(LocalDate.class); - mappingContext.setCustomSimpleTypes(customSimpleTypes); + + CustomConversions conversions = new CustomConversions(converters); + mappingContext.setSimpleTypeHolder(conversions.getSimpleTypeHolder()); converter = new MappingMongoConverter(factory, mappingContext); - converter.setCustomConverters(converters); + converter.setCustomConversions(conversions); converter.afterPropertiesSet(); Person person = new Person(); @@ -343,6 +344,7 @@ public class MappingMongoConverterUnitTests { } @Test + @SuppressWarnings("unchecked") public void writesNestedCollectionsCorrectly() { CollectionWrapper wrapper = new CollectionWrapper(); diff --git a/spring-data-mongodb/src/test/resources/template-mapping.xml b/spring-data-mongodb/src/test/resources/template-mapping.xml index de2726a28..d201f40cc 100644 --- a/spring-data-mongodb/src/test/resources/template-mapping.xml +++ b/spring-data-mongodb/src/test/resources/template-mapping.xml @@ -12,15 +12,23 @@ - - - - - - + - + + + + + + + + + + + + + +