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();