From cd789f97228d3136ce100e5056fbb7eb2b8871f5 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 24 Apr 2017 14:50:38 +0200 Subject: [PATCH] DATAREDIS-634 - Adapt to moved CustomConversions to Spring Data Commons. Introduce RedisCustomConversions extending o.s.d.convert.CustomConversions. Remove o.s.d.mongo.core.convert.CustomConversions implementation code and utility classes and let it extend RedisCustomConversions. Replace references to o.s.d.r.c.c.CustomConversions with o.s.d.convert.CustomConversions. Adapt tests. Introduce constructors for ObjectHashMapper and RedisKeyValueAdapter requiring o.s.d.convert.CustomConversions and deprecate constructors accepting o.s.d.r.c.c.CustomConversions. Related ticket: DATACMNS-1035. --- .../reference/redis-repositories.adoc | 2 +- .../data/redis/core/RedisKeyValueAdapter.java | 37 +- .../redis/core/convert/CustomConversions.java | 428 +----------------- .../core/convert/MappingRedisConverter.java | 35 +- .../core/convert/RedisCustomConversions.java | 73 +++ .../core/mapping/RedisMappingContext.java | 20 +- .../data/redis/hash/ObjectHashMapper.java | 19 +- ...RedisRepositoryConfigurationExtension.java | 7 +- .../MappingRedisConverterUnitTests.java | 69 +-- 9 files changed, 193 insertions(+), 497 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java diff --git a/src/main/asciidoc/reference/redis-repositories.adoc b/src/main/asciidoc/reference/redis-repositories.adoc index 6db528106..43851cc5c 100644 --- a/src/main/asciidoc/reference/redis-repositories.adoc +++ b/src/main/asciidoc/reference/redis-repositories.adoc @@ -158,7 +158,7 @@ of Complex Type addresses.[work].city = "... |=== -Mapping behavior can be customized by registering the according `Converter` in `CustomConversions`. Those converters can take care of converting from/to a single `byte[]` as well as `Map` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash. Writing objects to a Redis hash will delete the content from the hash and re-create the whole hash, so not mapped data will be lost. +Mapping behavior can be customized by registering the according `Converter` in `RedisCustomConversions`. Those converters can take care of converting from/to a single `byte[]` as well as `Map` whereas the first one is suitable for eg. converting one complex type to eg. a binary JSON representation that still uses the default mappings hash structure. The second option offers full control over the resulting hash. Writing objects to a Redis hash will delete the content from the hash and re-create the whole hash, so not mapped data will be lost. .Sample byte[] Converters ==== diff --git a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java index c75fb6501..e5ff6f99a 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java +++ b/src/main/java/org/springframework/data/redis/core/RedisKeyValueAdapter.java @@ -28,8 +28,6 @@ import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; @@ -42,7 +40,6 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter; import org.springframework.data.keyvalue.core.KeyValueAdapter; import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty; -import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; @@ -55,6 +52,7 @@ import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; import org.springframework.data.redis.core.convert.PathIndexResolver; import org.springframework.data.redis.core.convert.RedisConverter; +import org.springframework.data.redis.core.convert.RedisCustomConversions; import org.springframework.data.redis.core.convert.RedisData; import org.springframework.data.redis.core.convert.ReferenceResolverImpl; import org.springframework.data.redis.core.mapping.RedisMappingContext; @@ -66,7 +64,6 @@ import org.springframework.data.redis.util.ByteUtils; import org.springframework.data.util.CloseableIterator; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; /** * Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis. Objects @@ -117,7 +114,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter /** * Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default - * {@link CustomConversions}. + * {@link RedisCustomConversions}. * * @param redisOps must not be {@literal null}. */ @@ -126,13 +123,13 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter } /** - * Creates new {@link RedisKeyValueAdapter} with default {@link CustomConversions}. + * Creates new {@link RedisKeyValueAdapter} with default {@link RedisCustomConversions}. * * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. */ public RedisKeyValueAdapter(RedisOperations redisOps, RedisMappingContext mappingContext) { - this(redisOps, mappingContext, new CustomConversions()); + this(redisOps, mappingContext, new RedisCustomConversions()); } /** @@ -141,9 +138,25 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter * @param redisOps must not be {@literal null}. * @param mappingContext must not be {@literal null}. * @param customConversions can be {@literal null}. + * @deprecated since 2.0, use + * {@link #RedisKeyValueAdapter(RedisOperations, RedisMappingContext, org.springframework.data.convert.CustomConversions)}. */ + @Deprecated public RedisKeyValueAdapter(RedisOperations redisOps, RedisMappingContext mappingContext, CustomConversions customConversions) { + this(redisOps, mappingContext, (org.springframework.data.convert.CustomConversions) customConversions); + } + + /** + * Creates new {@link RedisKeyValueAdapter}. + * + * @param redisOps must not be {@literal null}. + * @param mappingContext must not be {@literal null}. + * @param customConversions can be {@literal null}. + * @since 2.0 + */ + public RedisKeyValueAdapter(RedisOperations redisOps, RedisMappingContext mappingContext, + org.springframework.data.convert.CustomConversions customConversions) { super(new RedisQueryEngine()); @@ -152,7 +165,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, new PathIndexResolver(mappingContext), new ReferenceResolverImpl(redisOps)); - mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); + mappingConverter.setCustomConversions(customConversions == null ? new RedisCustomConversions() : customConversions); mappingConverter.afterPropertiesSet(); this.converter = mappingConverter; @@ -374,7 +387,7 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter offset = Math.max(0, offset); if (offset >= 0 && rows > 0) { - keys = keys.subList((int)offset, Math.min((int)offset + rows, keys.size())); + keys = keys.subList((int) offset, Math.min((int) offset + rows, keys.size())); } for (byte[] key : keys) { @@ -428,7 +441,8 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter public void update(final PartialUpdate update) { - final RedisPersistentEntity entity = this.converter.getMappingContext().getPersistentEntity(update.getTarget()).get(); + final RedisPersistentEntity entity = this.converter.getMappingContext().getPersistentEntity(update.getTarget()) + .get(); final String keyspace = entity.getKeySpace(); final Object id = update.getId(); @@ -622,11 +636,10 @@ public class RedisKeyValueAdapter extends AbstractKeyValueAdapter if (entity.hasExplictTimeToLiveProperty()) { Optional ttlProperty = entity.getExplicitTimeToLiveProperty(); - if(!ttlProperty.isPresent()) { + if (!ttlProperty.isPresent()) { return target; } - final Optional ttl = ttlProperty.get().findAnnotation(TimeToLive.class); Long timeout = redisOps.execute(new RedisCallback() { diff --git a/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java b/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java index a55f41ce0..88638ea9f 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/redis/core/convert/CustomConversions.java @@ -15,445 +15,33 @@ */ package org.springframework.data.redis.core.convert; -import java.util.ArrayList; -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 java.util.function.Supplier; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -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.convert.ReadingConverter; -import org.springframework.data.convert.WritingConverter; -import org.springframework.data.mapping.model.SimpleTypeHolder; -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. - * + * * @author Olivyer Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Mark Paluch * @since 1.7 + * @deprecated since 2.0, use {@link RedisCustomConversions}. */ -public class CustomConversions { - - private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class); - private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Redis 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 Redis supported type! You might wanna check you annotation setup at the converter implementation."; - - 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; +@Deprecated +public class CustomConversions extends RedisCustomConversions { /** * Creates an empty {@link CustomConversions} object. */ - public CustomConversions() { - this(new ArrayList()); - } + public CustomConversions() {} /** * Creates a new {@link CustomConversions} instance registering the given converters. - * + * * @param converters */ public CustomConversions(List converters) { - - Assert.notNull(converters,"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, Optional>>(); - - List toRegister = new ArrayList(); - - // Add user provided converters to make sure they can override the defaults - toRegister.addAll(converters); - toRegister.add(new BinaryConverters.StringToBytesConverter()); - toRegister.add(new BinaryConverters.BytesToStringConverter()); - toRegister.add(new BinaryConverters.NumberToBytesConverter()); - toRegister.add(new BinaryConverters.BytesToNumberConverterFactory()); - toRegister.add(new BinaryConverters.EnumToBytesConverter()); - toRegister.add(new BinaryConverters.BytesToEnumConverterFactory()); - toRegister.add(new BinaryConverters.BooleanToBytesConverter()); - toRegister.add(new BinaryConverters.BytesToBooleanConverter()); - toRegister.add(new BinaryConverters.DateToBytesConverter()); - toRegister.add(new BinaryConverters.BytesToDateConverter()); - - toRegister.addAll(Jsr310Converters.getConvertersToRegister()); - - for (Object c : toRegister) { - registerConversion(c); - } - - Collections.reverse(toRegister); - - this.converters = Collections.unmodifiableList(toRegister); - 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. 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) { - 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) { - - Class type = converter.getClass(); - boolean isWriting = type.isAnnotationPresent(WritingConverter.class); - boolean isReading = type.isAnnotationPresent(ReadingConverter.class); - - if (converter instanceof GenericConverter) { - GenericConverter genericConverter = (GenericConverter) converter; - for (ConvertiblePair pair : genericConverter.getConvertibleTypes()) { - register(new ConverterRegistration(pair, isReading, isWriting)); - } - } else if (converter instanceof Converter) { - Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class); - register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting)); - } else if (converter instanceof ConverterFactory) { - - Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), ConverterFactory.class); - register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting)); - } - - else { - throw new IllegalArgumentException("Unsupported Converter type!"); - } - } - - /** - * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic - * Redis types. - * - * @param converterRegistration - */ - private void register(ConverterRegistration converterRegistration) { - - 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 Redis native one. - * - * @param sourceType must not be {@literal null} - * @return - */ - public Class getCustomWriteTarget(final Class sourceType) { - - return getOrCreateAndCache(sourceType, rawWriteTargetTypes, - () -> getCustomTarget(sourceType, null, writingPairs).isPresent() ? (Class) getCustomTarget(sourceType, null, writingPairs).get() : null); - } - - /** - * 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 - * @return - */ - public Class getCustomWriteTarget(final Class sourceType, final Class requestedTargetType) { - - if (requestedTargetType == null) { - return getCustomWriteTarget(sourceType); - } - - return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customWriteTargetTypes, - () -> (Class) getCustomTarget(sourceType, requestedTargetType, writingPairs).get()); - } - - /** - * Returns whether we have a custom conversion registered to readTargetWriteLocl into a Redis 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) { - return hasCustomWriteTarget(sourceType, null); - } - - /** - * Returns whether we have a custom conversion registered to readTargetWriteLocl an object of the given source type - * into an object of the given Redis native target type. - * - * @param sourceType must not be {@literal null}. - * @param requestedTargetType - * @return - */ - public boolean hasCustomWriteTarget(Class sourceType, Class requestedTargetType) { - return getCustomWriteTarget(sourceType, requestedTargetType) != null; - } - - /** - * 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 requestedTargetType must not be {@literal null} - * @return - */ - public boolean hasCustomReadTarget(Class sourceType, Class requestedTargetType) { - return getCustomReadTarget(sourceType, requestedTargetType) != null; - } - - /** - * 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 requestedTargetType can be {@literal null}. - * @return - */ - private Class getCustomReadTarget(final Class sourceType, final Class requestedTargetType) { - - if (requestedTargetType == null) { - return null; - } - - return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customReadTargetTypes, - () -> getCustomTarget(sourceType, requestedTargetType, readingPairs).isPresent() ? ((Class) getCustomTarget(sourceType, requestedTargetType, readingPairs).get()) : null); - } - - /** - * 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 requestedTargetType can be {@literal null}. - * @param pairs must not be {@literal null}. - * @return - */ - private static Optional> getCustomTarget(Class sourceType, Class requestedTargetType, - Collection pairs) { - - Assert.notNull(sourceType, "SourceType must not be null!"); - Assert.notNull(pairs, "Convertible pairs must not be null!"); - - if (requestedTargetType != null && pairs.contains(new ConvertiblePair(sourceType, requestedTargetType))) { - return Optional.of(requestedTargetType); - } - - for (ConvertiblePair typePair : pairs) { - if (typePair.getSourceType().isAssignableFrom(sourceType)) { - Class targetType = typePair.getTargetType(); - if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) { - return Optional.of(targetType); - } - } - } - - return Optional.empty(); - } - - /** - * Will try to find a value for the given key in the given cache or produce one using the given {@link Supplier} and - * store it in the cache. - * - * @param key the key to lookup a potentially existing value, must not be {@literal null}. - * @param cache the cache to find the value in, must not be {@literal null}. - * @param producer the {@link Supplier} to create values to cache, must not be {@literal null}. - * @return - */ - private static Class getOrCreateAndCache(T key, Map>> cache, - Supplier> producer) { - Optional> foo = cache.computeIfAbsent(key, t -> Optional.ofNullable(producer.get())); - return foo.isPresent() ? foo.get() : null; - } - - /** - * Conversion registration information. - * - * @author Oliver Gierke - */ - static class ConverterRegistration { - - private final ConvertiblePair convertiblePair; - private final boolean reading; - private final boolean writing; - - /** - * Creates a new {@link ConverterRegistration}. - * - * @param convertiblePair must not be {@literal null}. - * @param isReading whether to force to consider the converter for reading. - * @param isWriting whether to force to consider the converter for reading. - */ - public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) { - - Assert.notNull(convertiblePair, "Convertible pair must not be null!"); - - this.convertiblePair = convertiblePair; - this.reading = isReading; - this.writing = isWriting; - } - - /** - * Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags. - * - * @param source the source type to be converted from, must not be {@literal null}. - * @param target the target type to be converted to, must not be {@literal null}. - * @param isReading whether to force to consider the converter for reading. - * @param isWriting whether to force to consider the converter for writing. - */ - public ConverterRegistration(Class source, Class target, boolean isReading, boolean isWriting) { - this(new ConvertiblePair(source, target), isReading, isWriting); - } - - /** - * 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 Redis simple one. - * - * @return - */ - public boolean isSimpleSourceType() { - return isRedisBasicType(convertiblePair.getSourceType()); - } - - /** - * Returns whether the target type is a Redis simple one. - * - * @return - */ - public boolean isSimpleTargetType() { - return isRedisBasicType(convertiblePair.getTargetType()); - } - - /** - * Returns whether the given type is a type that Redis can handle basically. - * - * @param type - * @return - */ - private static boolean isRedisBasicType(Class type) { - return (byte[].class.equals(type) || Map.class.equals(type)); - } + super(converters); } } diff --git a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java index 549769a4c..9d4de2e1d 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java +++ b/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-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. @@ -16,17 +16,8 @@ package org.springframework.data.redis.core.convert; import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Optional; -import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,6 +27,7 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.DefaultTypeMapper; import org.springframework.data.convert.EntityInstantiator; import org.springframework.data.convert.EntityInstantiators; @@ -152,7 +144,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { entityInstantiators = new EntityInstantiators(); this.conversionService = new DefaultConversionService(); - this.customConversions = new CustomConversions(); + this.customConversions = new RedisCustomConversions(); typeMapper = new DefaultTypeMapper(new RedisTypeAliasAccessor(this.conversionService)); @@ -232,7 +224,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { if (persistentProperty.isMap()) { Map targetValue = null; - Class mapValueType = persistentProperty.getMapValueType().orElseThrow(()-> new IllegalArgumentException("Unable to retrieve MapValueType!")); + Class mapValueType = persistentProperty.getMapValueType() + .orElseThrow(() -> new IllegalArgumentException("Unable to retrieve MapValueType!")); if (conversionService.canConvert(byte[].class, mapValueType)) { targetValue = readMapOfSimpleTypes(currentPath, persistentProperty.getType(), @@ -502,7 +495,8 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { pUpdate.getPropertyPath(), pUpdate.getValue())); } - writeMap(entity.getKeySpace(), pUpdate.getPropertyPath(), targetProperty.getMapValueType().orElse(Object.class), map, sink); + writeMap(entity.getKeySpace(), pUpdate.getPropertyPath(), targetProperty.getMapValueType().orElse(Object.class), + map, sink); } else { writeInternal(entity.getKeySpace(), pUpdate.getPropertyPath(), pUpdate.getValue(), @@ -735,22 +729,23 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { if (customConversions.hasCustomWriteTarget(value.getClass())) { - Class targetType = customConversions.getCustomWriteTarget(value.getClass()); + Optional> targetType = customConversions.getCustomWriteTarget(value.getClass()); - if (!ClassUtils.isAssignable(Map.class, targetType) && customConversions.isSimpleType(value.getClass()) + if (!targetType.filter(it -> ClassUtils.isAssignable(Map.class, it)).isPresent() + && customConversions.isSimpleType(value.getClass()) && value.getClass() != propertyType) { sink.getBucket().put((!path.isEmpty() ? path + "." + TYPE_HINT_ALIAS : TYPE_HINT_ALIAS), toBytes(value.getClass().getName())); } - if (ClassUtils.isAssignable(Map.class, targetType)) { + if (targetType.filter(it -> ClassUtils.isAssignable(Map.class, it)).isPresent()) { - Map map = (Map) conversionService.convert(value, targetType); + Map map = (Map) conversionService.convert(value, targetType.get()); for (Map.Entry entry : map.entrySet()) { sink.getBucket().put(path + (StringUtils.hasText(path) ? "." : "") + entry.getKey(), toBytes(entry.getValue())); } - } else if (ClassUtils.isAssignable(byte[].class, targetType)) { + } else if (targetType.filter(it -> ClassUtils.isAssignable(byte[].class, it)).isPresent()) { sink.getBucket().put(path, toBytes(value)); } else { throw new IllegalArgumentException( @@ -986,7 +981,7 @@ public class MappingRedisConverter implements RedisConverter, InitializingBean { * @param customConversions */ public void setCustomConversions(CustomConversions customConversions) { - this.customConversions = customConversions != null ? customConversions : new CustomConversions(); + this.customConversions = customConversions != null ? customConversions : new RedisCustomConversions(); } public void setReferenceResolver(ReferenceResolver referenceResolver) { diff --git a/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java b/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java new file mode 100644 index 000000000..c356bfe6d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/convert/RedisCustomConversions.java @@ -0,0 +1,73 @@ +/* + * Copyright 2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.convert; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.springframework.data.mapping.model.SimpleTypeHolder; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. + * + * @author Mark Paluch + * @since 2.0 + * @see org.springframework.data.convert.CustomConversions + * @see SimpleTypeHolder + */ +public class RedisCustomConversions extends org.springframework.data.convert.CustomConversions { + + private static final StoreConversions STORE_CONVERSIONS; + private static final List STORE_CONVERTERS; + + static { + + List converters = new ArrayList<>(); + + converters.add(new BinaryConverters.StringToBytesConverter()); + converters.add(new BinaryConverters.BytesToStringConverter()); + converters.add(new BinaryConverters.NumberToBytesConverter()); + converters.add(new BinaryConverters.BytesToNumberConverterFactory()); + converters.add(new BinaryConverters.EnumToBytesConverter()); + converters.add(new BinaryConverters.BytesToEnumConverterFactory()); + converters.add(new BinaryConverters.BooleanToBytesConverter()); + converters.add(new BinaryConverters.BytesToBooleanConverter()); + converters.add(new BinaryConverters.DateToBytesConverter()); + converters.add(new BinaryConverters.BytesToDateConverter()); + converters.addAll(Jsr310Converters.getConvertersToRegister()); + + STORE_CONVERTERS = Collections.unmodifiableList(converters); + STORE_CONVERSIONS = StoreConversions.of(SimpleTypeHolder.DEFAULT, STORE_CONVERTERS); + } + + /** + * Creates an empty {@link RedisCustomConversions} object. + */ + public RedisCustomConversions() { + this(Collections.emptyList()); + } + + /** + * Creates a new {@link RedisCustomConversions} instance registering the given converters. + * + * @param converters + */ + public RedisCustomConversions(List converters) { + super(STORE_CONVERSIONS, converters); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java index 52433c9a2..83b61d59d 100644 --- a/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java +++ b/src/main/java/org/springframework/data/redis/core/mapping/RedisMappingContext.java @@ -39,6 +39,7 @@ import org.springframework.data.redis.core.TimeToLiveAccessor; import org.springframework.data.redis.core.convert.KeyspaceConfiguration; import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; import org.springframework.data.redis.core.convert.MappingConfiguration; +import org.springframework.data.redis.core.convert.RedisCustomConversions; import org.springframework.data.redis.core.index.IndexConfiguration; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -51,13 +52,15 @@ import org.springframework.util.StringUtils; /** * Redis specific {@link MappingContext}. - * + * * @author Christoph Strobl * @author Oliver Gierke * @since 1.7 */ public class RedisMappingContext extends KeyValueMappingContext, RedisPersistentProperty> { + private static final SimpleTypeHolder SIMPLE_TYPE_HOLDER = new RedisCustomConversions().getSimpleTypeHolder(); + private final MappingConfiguration mappingConfiguration; private final TimeToLiveAccessor timeToLiveAccessor; @@ -72,7 +75,7 @@ public class RedisMappingContext extends KeyValueMappingContext { * Creates new {@link ObjectHashMapper}. */ public ObjectHashMapper() { - this(new CustomConversions()); + this(new RedisCustomConversions()); } /** * Creates new {@link ObjectHashMapper}. * * @param customConversions can be {@literal null}. + * @deprecated since 2.0, use {@link #ObjectHashMapper(org.springframework.data.convert.CustomConversions)}. */ + @Deprecated public ObjectHashMapper(CustomConversions customConversions) { + this((org.springframework.data.convert.CustomConversions) customConversions); + } + + /** + * Creates new {@link ObjectHashMapper}. + * + * @param customConversions can be {@literal null}. + * @since 2.0 + */ + public ObjectHashMapper(org.springframework.data.convert.CustomConversions customConversions) { MappingRedisConverter mappingConverter = new MappingRedisConverter(new RedisMappingContext(), new NoOpIndexResolver(), new NoOpReferenceResolver()); - mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions); + mappingConverter.setCustomConversions(customConversions == null ? new RedisCustomConversions() : customConversions); mappingConverter.afterPropertiesSet(); converter = mappingConverter; diff --git a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java index 70ca08e83..27ff66b16 100644 --- a/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java +++ b/src/main/java/org/springframework/data/redis/repository/configuration/RedisRepositoryConfigurationExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2016 the original author or authors. + * Copyright 2015-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. @@ -33,9 +33,9 @@ import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryCon import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.RedisKeyValueAdapter; import org.springframework.data.redis.core.RedisKeyValueTemplate; -import org.springframework.data.redis.core.convert.CustomConversions; import org.springframework.data.redis.core.convert.MappingConfiguration; import org.springframework.data.redis.core.convert.MappingRedisConverter; +import org.springframework.data.redis.core.convert.RedisCustomConversions; import org.springframework.data.redis.core.mapping.RedisMappingContext; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.RepositoryConfigurationSource; @@ -45,6 +45,7 @@ import org.springframework.util.StringUtils; * {@link RepositoryConfigurationExtension} for Redis. * * @author Christoph Strobl + * @author Mark Paluch * @since 1.7 */ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension { @@ -92,7 +93,7 @@ public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryCon registerIfNotAlreadyRegistered(mappingContextDefinition, registry, MAPPING_CONTEXT_BEAN_NAME, configurationSource); // register coustom conversions - RootBeanDefinition customConversions = new RootBeanDefinition(CustomConversions.class); + RootBeanDefinition customConversions = new RootBeanDefinition(RedisCustomConversions.class); registerIfNotAlreadyRegistered(customConversions, registry, REDIS_CUSTOM_CONVERSIONS_BEAN_NAME, configurationSource); diff --git a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java index edc558767..f0751ca5a 100644 --- a/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java +++ b/src/test/java/org/springframework/data/redis/core/convert/MappingRedisConverterUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core.convert; import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.contains; import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.data.redis.core.convert.ConversionTestEntities.*; @@ -31,16 +32,7 @@ import java.time.LocalTime; import java.time.Period; import java.time.ZoneId; import java.time.ZonedDateTime; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import org.hamcrest.core.IsEqual; import org.junit.Before; @@ -880,12 +872,17 @@ public class MappingRedisConverterUnitTests { assertThat(write(address).getTimeToLive(), is(5L)); } - @Test // DATAREDIS-425 + @Test // DATAREDIS-425, DATAREDIS-634 public void writeShouldHonorCustomConversionOnRootType() { - this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + RedisCustomConversions customConversions = new RedisCustomConversions( + Collections.singletonList(new AddressToBytesConverter())); + + RedisMappingContext mappingContext = new RedisMappingContext(); + mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + + this.converter = new MappingRedisConverter(mappingContext, null, resolverMock); + this.converter.setCustomConversions(customConversions); this.converter.afterPropertiesSet(); Address address = new Address(); @@ -896,12 +893,17 @@ public class MappingRedisConverterUnitTests { isBucket().containingUtf8String("_raw", "{\"city\":\"unknown\",\"country\":\"Tel'aran'rhiod\"}")); } - @Test // DATAREDIS-425 + @Test // DATAREDIS-425, DATAREDIS-634 public void writeShouldHonorCustomConversionOnNestedType() { - this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + RedisCustomConversions customConversions = new RedisCustomConversions( + Collections.singletonList(new AddressToBytesConverter())); + + RedisMappingContext mappingContext = new RedisMappingContext(); + mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder()); + + this.converter = new MappingRedisConverter(mappingContext, null, resolverMock); + this.converter.setCustomConversions(customConversions); this.converter.afterPropertiesSet(); Address address = new Address(); @@ -918,7 +920,7 @@ public class MappingRedisConverterUnitTests { this.converter = new MappingRedisConverter(null, null, resolverMock); this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new AddressToBytesConverter()))); this.converter.afterPropertiesSet(); Address address = new Address(); @@ -934,7 +936,7 @@ public class MappingRedisConverterUnitTests { this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new AddressToBytesConverter()))); this.converter.afterPropertiesSet(); Address address = new Address(); @@ -950,7 +952,7 @@ public class MappingRedisConverterUnitTests { this.converter = new MappingRedisConverter(null, null, resolverMock); this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new BytesToAddressConverter()))); + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new BytesToAddressConverter()))); this.converter.afterPropertiesSet(); Map map = new LinkedHashMap(); @@ -967,7 +969,7 @@ public class MappingRedisConverterUnitTests { this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new BytesToAddressConverter()))); + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new BytesToAddressConverter()))); this.converter.afterPropertiesSet(); Map map = new LinkedHashMap(); @@ -1003,7 +1005,8 @@ public class MappingRedisConverterUnitTests { public void writeShouldConsiderMapConvertersForRootType() { this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); this.converter.afterPropertiesSet(); Species myrddraal = new Species(); @@ -1018,7 +1021,8 @@ public class MappingRedisConverterUnitTests { public void writeShouldConsiderMapConvertersForNestedType() { this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); this.converter.afterPropertiesSet(); rand.species = new Species(); @@ -1031,7 +1035,8 @@ public class MappingRedisConverterUnitTests { public void readShouldConsiderMapConvertersForRootType() { this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); this.converter.afterPropertiesSet(); Map map = new LinkedHashMap(); map.put("species-name", "trolloc"); @@ -1046,7 +1051,8 @@ public class MappingRedisConverterUnitTests { public void readShouldConsiderMapConvertersForNestedType() { this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); this.converter.afterPropertiesSet(); Map map = new LinkedHashMap(); @@ -1062,7 +1068,8 @@ public class MappingRedisConverterUnitTests { public void writeShouldConsiderMapConvertersInsideLists() { this.converter = new MappingRedisConverter(new RedisMappingContext(), null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new SpeciesToMapConverter()))); this.converter.afterPropertiesSet(); TheWheelOfTime twot = new TheWheelOfTime(); @@ -1081,7 +1088,8 @@ public class MappingRedisConverterUnitTests { public void readShouldConsiderMapConvertersForValuesInList() { this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new MapToSpeciesConverter()))); this.converter.afterPropertiesSet(); Map map = new LinkedHashMap(); @@ -1099,7 +1107,8 @@ public class MappingRedisConverterUnitTests { public void writeHandlesArraysProperly() { this.converter = new MappingRedisConverter(null, null, resolverMock); - this.converter.setCustomConversions(new CustomConversions(Collections.singletonList(new ListToByteConverter()))); + this.converter + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new ListToByteConverter()))); this.converter.afterPropertiesSet(); Map innerMap = new LinkedHashMap(); @@ -1534,7 +1543,7 @@ public class MappingRedisConverterUnitTests { this.converter = new MappingRedisConverter(null, null, resolverMock); this.converter - .setCustomConversions(new CustomConversions(Collections.singletonList(new AddressToBytesConverter()))); + .setCustomConversions(new RedisCustomConversions(Collections.singletonList(new AddressToBytesConverter()))); this.converter.afterPropertiesSet(); Address address = new Address();