diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java new file mode 100644 index 000000000..d8d1cc750 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -0,0 +1,500 @@ +/* + * Copyright 2011-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.convert; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.GenericTypeResolver; +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.model.SimpleTypeHolder; +import org.springframework.data.util.Optionals; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. The converters build up two sets of types which store-specific basic 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 + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.0 + */ +@Slf4j +public class CustomConversions { + + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a store-supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String NOT_A_CONVERTER = "Converter %s is neither a Spring Converter, GenericConverter or ConverterFactory!"; + private static final List DEFAULT_CONVERTERS; + + static { + + List defaults = new ArrayList<>(); + + defaults.addAll(JodaTimeConverters.getConvertersToRegister()); + defaults.addAll(Jsr310Converters.getConvertersToRegister()); + defaults.addAll(ThreeTenBackPortConverters.getConvertersToRegister()); + + DEFAULT_CONVERTERS = Collections.unmodifiableList(defaults); + } + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + private final Map>> customReadTargetTypes; + private final Map>> customWriteTargetTypes; + private final Map, Optional>> rawWriteTargetTypes; + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param converters + */ + public CustomConversions(StoreConversions storeConversions, List converters) { + + Assert.notNull(converters, "List of converters must not be null!"); + + this.readingPairs = new LinkedHashSet<>(); + this.writingPairs = new LinkedHashSet<>(); + this.customSimpleTypes = new HashSet<>(); + this.customReadTargetTypes = new ConcurrentHashMap<>(); + this.customWriteTargetTypes = new ConcurrentHashMap<>(); + this.rawWriteTargetTypes = new ConcurrentHashMap<>(); + + List toRegister = new ArrayList(); + + // Add user provided converters to make sure they can override the defaults + toRegister.addAll(converters); + toRegister.addAll(storeConversions.getStoreConverters()); + toRegister.addAll(DEFAULT_CONVERTERS); + + toRegister.stream()// + .flatMap(it -> storeConversions.getRegistrationsFor(it).stream())// + .forEach(this::register); + + Collections.reverse(toRegister); + + this.converters = Collections.unmodifiableList(toRegister); + this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, storeConversions.getStoreTypeHolder()); + } + + /** + * Returns the underlying {@link SimpleTypeHolder}. + * + * @return + */ + public SimpleTypeHolder getSimpleTypeHolder() { + return simpleTypeHolder; + } + + /** + * Returns whether the given type is considered to be simple. That means it's either a general simple type or we have + * a writing {@link Converter} registered for a particular type. + * + * @see SimpleTypeHolder#isSimpleType(Class) + * @param type + * @return + */ + public boolean isSimpleType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return simpleTypeHolder.isSimpleType(type); + } + + /** + * Populates the given {@link GenericConversionService} with the converters registered. + * + * @param conversionService + */ + public void registerConvertersIn(GenericConversionService conversionService) { + + Assert.notNull(conversionService, "ConversionService must not be null!"); + + converters.forEach(it -> { + + boolean added = false; + + if (it instanceof Converter) { + conversionService.addConverter(Converter.class.cast(it)); + added = true; + } + + if (it instanceof ConverterFactory) { + conversionService.addConverterFactory(ConverterFactory.class.cast(it)); + added = true; + } + + if (it instanceof GenericConverter) { + conversionService.addConverter(GenericConverter.class.cast(it)); + added = true; + } + + if (!added) { + throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, it)); + } + }); + } + + /** + * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic + * Mongo types. + * + * @param pair + */ + private void register(ConverterRegistration converterRegistration) { + + Assert.notNull(converterRegistration, "Converter registration must not be null!"); + + ConvertiblePair pair = converterRegistration.getConvertiblePair(); + + if (converterRegistration.isReading()) { + + readingPairs.add(pair); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleSourceType()) { + LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + + if (converterRegistration.isWriting()) { + + writingPairs.add(pair); + customSimpleTypes.add(pair.getSourceType()); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleTargetType()) { + LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + } + + /** + * Returns the target type to convert to in case we have a custom conversion registered to convert the given source + * type into a Mongo native one. + * + * @param sourceType must not be {@literal null} + * @return + */ + public Optional> getCustomWriteTarget(Class sourceType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + + return rawWriteTargetTypes.computeIfAbsent(sourceType, + it -> getCustomTarget(sourceType, Optional.empty(), writingPairs)); + } + + /** + * Returns the target type we can readTargetWriteLocl an inject of the given source type to. The returned type might + * be a subclass of the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply + * return the first target type matching or {@literal null} if no conversion can be found. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType must not be {@literal null}. + * @return + */ + public Optional> getCustomWriteTarget(Class sourceType, Class requestedTargetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(requestedTargetType, "Target type must not be null!"); + + return customWriteTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, requestedTargetType), + it -> getCustomTarget(sourceType, Optional.of(requestedTargetType), writingPairs)); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl into a Mongo native type. The + * returned type might be a subclass of the given expected type though. + * + * @param sourceType must not be {@literal null} + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + + return getCustomWriteTarget(sourceType).isPresent(); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl an object of the given source type + * into an object of the given Mongo native target type. + * + * @param sourceType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType, Class targetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(targetType, "Target type must not be null!"); + + return getCustomWriteTarget(sourceType, targetType).isPresent(); + } + + /** + * Returns whether we have a custom conversion registered to readTargetReadLock the given source into the given target + * type. + * + * @param sourceType must not be {@literal null} + * @param targetType must not be {@literal null} + * @return + */ + public boolean hasCustomReadTarget(Class sourceType, Class targetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(targetType, "Target type must not be null!"); + + return getCustomReadTarget(sourceType, targetType).isPresent(); + } + + /** + * Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the + * returned {@link Class} could be an assignable type to the given {@code requestedTargetType}. + * + * @param sourceType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + private Optional> getCustomReadTarget(Class sourceType, Class targetType) { + + return customReadTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, targetType), + it -> getCustomTarget(sourceType, Optional.of(targetType), readingPairs)); + } + + /** + * Inspects the given {@link ConvertiblePair}s for ones that have a source compatible type as source. Additionally + * checks assignability of the target type if one is given. + * + * @param sourceType must not be {@literal null}. + * @param targetType can be {@literal null}. + * @param pairs must not be {@literal null}. + * @return + */ + private static Optional> getCustomTarget(Class sourceType, Optional> targetType, + Collection pairs) { + + Assert.notNull(sourceType, "Source Class must not be null!"); + Assert.notNull(pairs, "Collection of ConvertiblePair must not be null!"); + + return Optionals.firstNonEmpty(// + () -> targetType.filter(it -> pairs.contains(new ConvertiblePair(sourceType, it))), // + () -> pairs.stream()// + .filter(it -> hasAssignableSourceType(it, sourceType)) // + .> map(ConvertiblePair::getTargetType)// + .filter(it -> requestTargetTypeIsAssignable(targetType, it))// + .findFirst()); + } + + private static boolean hasAssignableSourceType(ConvertiblePair pair, Class sourceType) { + return pair.getSourceType().isAssignableFrom(sourceType); + } + + private static boolean requestTargetTypeIsAssignable(Optional> requestedTargetType, Class targetType) { + + return !requestedTargetType.isPresent() // + ? true // + : requestedTargetType.map(it -> targetType.isAssignableFrom(it)).orElse(false); + } + + /** + * Conversion registration information. + * + * @author Oliver Gierke + * @author Mark Paluch + */ + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static class ConverterRegistration { + + private final @NonNull ConvertiblePair convertiblePair; + private final @NonNull StoreConversions storeConversions; + private final boolean reading; + private final boolean writing; + + /** + * Returns whether the converter shall be used for writing. + * + * @return + */ + public boolean isWriting() { + return writing == true || (!reading && isSimpleTargetType()); + } + + /** + * Returns whether the converter shall be used for reading. + * + * @return + */ + public boolean isReading() { + return reading == true || (!writing && isSimpleSourceType()); + } + + /** + * Returns the actual conversion pair. + * + * @return + */ + public ConvertiblePair getConvertiblePair() { + return convertiblePair; + } + + /** + * Returns whether the source type is a Mongo simple one. + * + * @return + */ + public boolean isSimpleSourceType() { + return storeConversions.isStoreSimpleType(convertiblePair.getSourceType()); + } + + /** + * Returns whether the target type is a Mongo simple one. + * + * @return + */ + public boolean isSimpleTargetType() { + return storeConversions.isStoreSimpleType(convertiblePair.getTargetType()); + } + } + + /** + * Value type to capture store-specific extensions to the {@link CustomConversions}. Allows to forward store specific + * default conversions and a set of types that are supposed to be considered simple. + * + * @author Oliver Gierke + */ + @Value + @Getter(AccessLevel.PACKAGE) + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class StoreConversions { + + public static final StoreConversions NONE = StoreConversions.of(SimpleTypeHolder.DEFAULT, Collections.emptyList()); + + SimpleTypeHolder storeTypeHolder; + Collection storeConverters; + + /** + * Creates a new {@link StoreConversions} for the given store-specific {@link SimpleTypeHolder} and the given + * converters. + * + * @param storeTypeHolder must not be {@literal null}. + * @param converters must not be {@literal null}. + * @return + */ + public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Object... converters) { + + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); + Assert.notNull(converters, "Converters must not be null!"); + + return new StoreConversions(storeTypeHolder, Arrays.asList(converters)); + } + + /** + * Creates a new {@link StoreConversions} for the given store-specific {@link SimpleTypeHolder} and the given + * converters. + * + * @param storeTypeHolder must not be {@literal null}. + * @param converters must not be {@literal null}. + * @return + */ + public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Collection converters) { + + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); + Assert.notNull(converters, "Converters must not be null!"); + + return new StoreConversions(storeTypeHolder, converters); + } + + /** + * Returns {@link ConverterRegistration}s for the given converter. + * + * @param converter must not be {@literal null}. + * @return + */ + public Streamable getRegistrationsFor(Object converter) { + + Assert.notNull(converter, "Converter must not be null!"); + + Class type = converter.getClass(); + boolean isWriting = type.isAnnotationPresent(WritingConverter.class); + boolean isReading = type.isAnnotationPresent(ReadingConverter.class); + + if (converter instanceof GenericConverter) { + + GenericConverter genericConverter = (GenericConverter) converter; + return Streamable.of(genericConverter.getConvertibleTypes()).map(it -> register(it, isReading, isWriting)); + + } else if (converter instanceof ConverterFactory) { + + return getRegistrationFor(converter, ConverterFactory.class, isReading, isWriting); + + } else if (converter instanceof Converter) { + + return getRegistrationFor(converter, Converter.class, isReading, isWriting); + + } else { + throw new IllegalArgumentException("Unsupported converter type!"); + } + } + + private Streamable getRegistrationFor(Object converter, Class type, boolean isReading, + boolean isWriting) { + + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), type); + return Streamable.of(register(arguments[0], arguments[1], isReading, isWriting)); + } + + private ConverterRegistration register(Class source, Class target, boolean isReading, boolean isWriting) { + return register(new ConvertiblePair(source, target), isReading, isWriting); + } + + private ConverterRegistration register(ConvertiblePair pair, boolean isReading, boolean isWriting) { + return new ConverterRegistration(pair, this, isReading, isWriting); + } + + private boolean isStoreSimpleType(Class type) { + return storeTypeHolder.isSimpleType(type); + } + } +} diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index a962e86ec..749b80af5 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -89,7 +89,7 @@ public abstract class AbstractMappingContext> initialEntitySet = new HashSet<>(); private boolean strict = false; - private SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(); + private SimpleTypeHolder simpleTypeHolder = SimpleTypeHolder.DEFAULT; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock read = lock.readLock(); diff --git a/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java b/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java index 3437d28d5..5a27b0024 100644 --- a/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java @@ -32,39 +32,43 @@ import org.springframework.util.Assert; */ public class SimpleTypeHolder { - private static final Set> DEFAULTS = new HashSet<>(); + private static final Set> DEFAULTS = new HashSet>() { - static { - DEFAULTS.add(boolean.class); - DEFAULTS.add(boolean[].class); - DEFAULTS.add(long.class); - DEFAULTS.add(long[].class); - DEFAULTS.add(short.class); - DEFAULTS.add(short[].class); - DEFAULTS.add(int.class); - DEFAULTS.add(int[].class); - DEFAULTS.add(byte.class); - DEFAULTS.add(byte[].class); - DEFAULTS.add(float.class); - DEFAULTS.add(float[].class); - DEFAULTS.add(double.class); - DEFAULTS.add(double[].class); - DEFAULTS.add(char.class); - DEFAULTS.add(char[].class); - DEFAULTS.add(Boolean.class); - DEFAULTS.add(Long.class); - DEFAULTS.add(Short.class); - DEFAULTS.add(Integer.class); - DEFAULTS.add(Byte.class); - DEFAULTS.add(Float.class); - DEFAULTS.add(Double.class); - DEFAULTS.add(Character.class); - DEFAULTS.add(String.class); - DEFAULTS.add(Date.class); - DEFAULTS.add(Locale.class); - DEFAULTS.add(Class.class); - DEFAULTS.add(Enum.class); - } + private static final long serialVersionUID = -1738594126505221500L; + + { + add(boolean.class); + add(boolean[].class); + add(long.class); + add(long[].class); + add(short.class); + add(short[].class); + add(int.class); + add(int[].class); + add(byte.class); + add(byte[].class); + add(float.class); + add(float[].class); + add(double.class); + add(double[].class); + add(char.class); + add(char[].class); + add(Boolean.class); + add(Long.class); + add(Short.class); + add(Integer.class); + add(Byte.class); + add(Float.class); + add(Double.class); + add(Character.class); + add(String.class); + add(Date.class); + add(Locale.class); + add(Class.class); + add(Enum.class); + } + }; + public static final SimpleTypeHolder DEFAULT = new SimpleTypeHolder(); private final Set> simpleTypes; @@ -73,8 +77,7 @@ public class SimpleTypeHolder { * * @see #SimpleTypeHolder(Set, boolean) */ - @SuppressWarnings("unchecked") - public SimpleTypeHolder() { + protected SimpleTypeHolder() { this(Collections.emptySet(), true); } @@ -128,13 +131,8 @@ public class SimpleTypeHolder { return true; } - for (Class clazz : simpleTypes) { - if (clazz.isAssignableFrom(type)) { - simpleTypes.add(type); - return true; - } - } - - return false; + return simpleTypes.stream()// + .filter(it -> it.isAssignableFrom(type))// + .peek(it -> simpleTypes.add(type)).findFirst().isPresent(); } } diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java new file mode 100644 index 000000000..371824cb0 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -0,0 +1,277 @@ +/* + * Copyright 2011-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.convert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.junit.Test; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions.StoreConversions; +import org.threeten.bp.LocalDateTime; + +/** + * Unit tests for {@link MongoCustomConversions}. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 2.0 + */ +public class CustomConversionsUnitTests { + + @Test // DATACMNS-1035 + public void findsBasicReadAndWriteConversions() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(FormatToStringConverter.INSTANCE, StringToFormatConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Format.class)).hasValue(String.class); + assertThat(conversions.getCustomWriteTarget(String.class)).isNotPresent(); + + assertThat(conversions.hasCustomReadTarget(String.class, Format.class)).isTrue(); + assertThat(conversions.hasCustomReadTarget(String.class, Locale.class)).isFalse(); + } + + @Test // DATACMNS-1035 + public void considersSubtypesCorrectly() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(NumberToStringConverter.INSTANCE, StringToNumberConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Long.class)).hasValue(String.class); + assertThat(conversions.hasCustomReadTarget(String.class, Long.class)).isTrue(); + } + + @Test // DATACMNS-1035 + public void populatesConversionServiceCorrectly() { + + GenericConversionService conversionService = new DefaultConversionService(); + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToFormatConverter.INSTANCE)); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.canConvert(String.class, Format.class), is(true)); + } + + @Test // DATAMONGO-259, DATACMNS-1035 + public void doesNotConsiderTypeSimpleIfOnlyReadConverterIsRegistered() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToFormatConverter.INSTANCE)); + assertThat(conversions.isSimpleType(Format.class), is(false)); + } + + @Test // DATAMONGO-298, DATACMNS-1035 + public void discoversConvertersForSubtypesOfMongoTypes() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToIntegerConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(String.class, Integer.class), is(true)); + assertThat(conversions.hasCustomWriteTarget(String.class, Integer.class), is(true)); + } + + @Test // DATAMONGO-795, DATACMNS-1035 + public void favorsCustomConverterForIndeterminedTargetType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(DateTimeToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(DateTime.class)).hasValue(String.class); + } + + @Test // DATAMONGO-881, DATACMNS-1035 + public void customConverterOverridesDefault() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(CustomDateTimeConverter.INSTANCE)); + GenericConversionService conversionService = new DefaultConversionService(); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.convert(new DateTime(), Date.class)).isEqualTo(new Date(0)); + } + + @Test // DATAMONGO-1001, DATACMNS-1035 + public void shouldSelectPropertCustomWriteTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(FormatToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(createProxyTypeFor(Format.class))).hasValue(String.class); + } + + @Test // DATAMONGO-1001, DATACMNS-1035 + public void shouldSelectPropertCustomReadTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(CustomObjectToStringConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(createProxyTypeFor(Object.class), String.class)).isTrue(); + } + + @Test // DATAMONGO-1131, DATACMNS-1035 + public void registersConvertersForJsr310() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(customConversions.hasCustomWriteTarget(java.time.LocalDateTime.class)).isTrue(); + } + + @Test // DATAMONGO-1131, DATACMNS-1035 + public void registersConvertersForThreeTenBackPort() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(customConversions.hasCustomWriteTarget(LocalDateTime.class)).isTrue(); + } + + @Test // DATAMONGO-1302, DATACMNS-1035 + public void registersConverterFactoryCorrectly() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, + Collections.singletonList(new FormatConverterFactory())); + + assertThat(customConversions.getCustomWriteTarget(String.class, SimpleDateFormat.class)).isPresent(); + } + + private static Class createProxyTypeFor(Class type) { + + ProxyFactory factory = new ProxyFactory(); + factory.setProxyTargetClass(true); + factory.setTargetClass(type); + + return factory.getProxy().getClass(); + } + + enum FormatToStringConverter implements Converter { + + INSTANCE; + + public String convert(Format source) { + return source.toString(); + } + } + + enum StringToFormatConverter implements Converter { + + INSTANCE; + + public Format convert(String source) { + return DateFormat.getInstance(); + } + } + + 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; + } + } + + enum StringToIntegerConverter implements Converter { + + INSTANCE; + + public Integer convert(String source) { + return 0; + } + } + + enum DateTimeToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(DateTime source) { + return ""; + } + } + + enum CustomDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public Date convert(DateTime source) { + return new Date(0); + } + } + + enum CustomObjectToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Object source) { + return source != null ? source.toString() : null; + } + + } + + @WritingConverter + static class FormatConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new StringToFormat(targetType); + } + + private static final class StringToFormat implements Converter { + + private final Class targetType; + + public StringToFormat(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(String source) { + + if (source.length() == 0) { + return null; + } + + try { + return targetType.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + } + } +} diff --git a/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java b/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java index 0c0cdaefa..90d1ed770 100755 --- a/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java @@ -44,14 +44,14 @@ public class SimpleTypeHolderUnitTests { @Test(expected = IllegalArgumentException.class) // DATACMNS-31 public void rejectsNullTypeForIsSimpleTypeCall() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; holder.isSimpleType(null); } @Test public void addsDefaultTypes() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(String.class)).isTrue(); } @@ -87,28 +87,28 @@ public class SimpleTypeHolderUnitTests { @Test public void considersObjectToBeSimpleType() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(Object.class)).isTrue(); } @Test public void considersSimpleEnumAsSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(SimpleEnum.FOO.getClass())).isTrue(); } @Test public void considersComplexEnumAsSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(ComplexEnum.FOO.getClass())).isTrue(); } @Test // DATACMNS-1006 public void considersJavaLangTypesSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(Type.class)).isTrue(); } diff --git a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java index 504eadb34..57014caa3 100755 --- a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java @@ -48,7 +48,7 @@ import org.springframework.util.StringUtils; */ public class AbstractMappingContextUnitTests { - final SimpleTypeHolder holder = new SimpleTypeHolder(); + final SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; SampleMappingContext context; @Before @@ -153,7 +153,9 @@ public class AbstractMappingContextUnitTests { PersistentEntity entity = mappingContext .getRequiredPersistentEntity(Sample.class); - assertThat(entity.getPersistentProperty("persons")).hasValueSatisfying(it -> assertThat(mappingContext.getPersistentEntity(it)).hasValueSatisfying(inner -> assertThat(inner.getType()).isEqualTo(Person.class))); + assertThat(entity.getPersistentProperty("persons")) + .hasValueSatisfying(it -> assertThat(mappingContext.getPersistentEntity(it)) + .hasValueSatisfying(inner -> assertThat(inner.getType()).isEqualTo(Person.class))); } @Test // DATACMNS-380