/*
* 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.
* 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.lang.reflect.Array;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.CollectionFactory;
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;
import org.springframework.data.convert.TypeAliasAccessor;
import org.springframework.data.convert.TypeMapper;
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
import org.springframework.data.mapping.Alias;
import org.springframework.data.mapping.Association;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.mapping.context.PersistentPropertyPath;
import org.springframework.data.mapping.model.MappingException;
import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider;
import org.springframework.data.mapping.model.PropertyValueProvider;
import org.springframework.data.redis.core.PartialUpdate;
import org.springframework.data.redis.core.PartialUpdate.PropertyUpdate;
import org.springframework.data.redis.core.PartialUpdate.UpdateCommand;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.mapping.RedisMappingContext;
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
import org.springframework.data.redis.core.mapping.RedisPersistentProperty;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.comparator.NullSafeComparator;
/**
* {@link RedisConverter} implementation creating flat binary map structure out of a given domain type. Considers
* {@link Indexed} annotation for enabling helper structures for finder operations. <br />
* <br />
* <strong>NOTE</strong> {@link MappingRedisConverter} is an {@link InitializingBean} and requires
* {@link MappingRedisConverter#afterPropertiesSet()} to be called.
*
* <pre>
* <code>
* @RedisHash("persons")
* class Person {
*
* @Id String id;
* String firstname;
*
* List<String> nicknames;
* List<Person> coworkers;
*
* Address address;
* @Reference Country nationality;
* }
* </code>
* </pre>
*
* The above is represented as:
*
* <pre>
* <code>
* _class=org.example.Person
* id=1
* firstname=rand
* lastname=al'thor
* coworkers.[0].firstname=mat
* coworkers.[0].nicknames.[0]=prince of the ravens
* coworkers.[1].firstname=perrin
* coworkers.[1].address.city=two rivers
* nationality=nationality:andora
* </code>
* </pre>
*
* @author Christoph Strobl
* @author Greg Turnquist
* @author Mark Paluch
* @since 1.7
*/
public class MappingRedisConverter implements RedisConverter, InitializingBean {
private static final String TYPE_HINT_ALIAS = "_class";
private static final String INVALID_TYPE_ASSIGNMENT = "Value of type %s cannot be assigned to property %s of type %s.";
private final RedisMappingContext mappingContext;
private final GenericConversionService conversionService;
private final EntityInstantiators entityInstantiators;
private final TypeMapper<RedisData> typeMapper;
private final Comparator<String> listKeyComparator = new NullSafeComparator<String>(
NaturalOrderingKeyComparator.INSTANCE, true);
private ReferenceResolver referenceResolver;
private IndexResolver indexResolver;
private CustomConversions customConversions;
/**
* Creates new {@link MappingRedisConverter}.
*
* @param context can be {@literal null}.
*/
MappingRedisConverter(RedisMappingContext context) {
this(context, null, null);
}
/**
* Creates new {@link MappingRedisConverter} and defaults {@link RedisMappingContext} when {@literal null}.
*
* @param mappingContext can be {@literal null}.
* @param indexResolver can be {@literal null}.
* @param referenceResolver must not be {@literal null}.
*/
public MappingRedisConverter(RedisMappingContext mappingContext, IndexResolver indexResolver,
ReferenceResolver referenceResolver) {
this.mappingContext = mappingContext != null ? mappingContext : new RedisMappingContext();
entityInstantiators = new EntityInstantiators();
this.conversionService = new DefaultConversionService();
this.customConversions = new RedisCustomConversions();
typeMapper = new DefaultTypeMapper<RedisData>(new RedisTypeAliasAccessor(this.conversionService));
this.referenceResolver = referenceResolver;
this.indexResolver = indexResolver != null ? indexResolver : new PathIndexResolver(this.mappingContext);
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityReader#read(java.lang.Class, java.lang.Object)
*/
@Override
public <R> R read(Class<R> type, final RedisData source) {
return readInternal("", type, source);
}
@SuppressWarnings("unchecked")
private <R> R readInternal(final String path, Class<R> type, final RedisData source) {
if (source.getBucket() == null || source.getBucket().isEmpty()) {
return null;
}
TypeInformation<?> readType = typeMapper.readType(source).orElse(ClassTypeInformation.from(type));
final Optional<RedisPersistentEntity<?>> entity = mappingContext.getPersistentEntity(readType);
if (customConversions.hasCustomReadTarget(Map.class, readType.getType())) {
Map<String, byte[]> partial = new HashMap<String, byte[]>();
if (!path.isEmpty()) {
for (Entry<String, byte[]> entry : source.getBucket().extract(path + ".").entrySet()) {
partial.put(entry.getKey().substring(path.length() + 1), entry.getValue());
}
} else {
partial.putAll(source.getBucket().asMap());
}
R instance = (R) conversionService.convert(partial, readType.getType());
if (entity.isPresent() && entity.get().hasIdProperty()) {
entity.get().getPropertyAccessor(instance).setProperty(entity.get().getIdProperty().get(),
Optional.ofNullable(source.getId()));
}
return instance;
}
if (conversionService.canConvert(byte[].class, readType.getType())) {
return (R) conversionService.convert(source.getBucket().get(StringUtils.hasText(path) ? path : "_raw"),
readType.getType());
}
EntityInstantiator instantiator = entityInstantiators.getInstantiatorFor(entity.get());
Object instance = instantiator.createInstance((RedisPersistentEntity<RedisPersistentProperty>) entity.get(),
new PersistentEntityParameterValueProvider<RedisPersistentProperty>(entity.get(),
new ConverterAwareParameterValueProvider(path, source, conversionService),
Optional.of(this.conversionService)));
final PersistentPropertyAccessor accessor = entity.get().getPropertyAccessor(instance);
entity.get().doWithProperties(new PropertyHandler<RedisPersistentProperty>() {
@Override
public void doWithPersistentProperty(RedisPersistentProperty persistentProperty) {
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
PreferredConstructor<?, RedisPersistentProperty> constructor = entity.get().getPersistenceConstructor().get();
if (constructor.isConstructorParameter(persistentProperty)) {
return;
}
if (persistentProperty.isMap()) {
Map<?, ?> targetValue = null;
Class<?> mapValueType = persistentProperty.getMapValueType()
.orElseThrow(() -> new IllegalArgumentException("Unable to retrieve MapValueType!"));
if (conversionService.canConvert(byte[].class, mapValueType)) {
targetValue = readMapOfSimpleTypes(currentPath, persistentProperty.getType(),
persistentProperty.getComponentType().get(), mapValueType, source);
} else {
targetValue = readMapOfComplexTypes(currentPath, persistentProperty.getType(),
persistentProperty.getComponentType().get(), mapValueType, source);
}
if (targetValue != null) {
accessor.setProperty(persistentProperty, Optional.ofNullable(targetValue));
}
}
else if (persistentProperty.isCollectionLike()) {
Object targetValue = readCollectionOrArray(currentPath, persistentProperty.getType(),
persistentProperty.getTypeInformation().getComponentType().get().getActualType().getType(),
source.getBucket());
if (targetValue != null) {
accessor.setProperty(persistentProperty, Optional.ofNullable(targetValue));
}
} else if (persistentProperty.isEntity() && !conversionService.canConvert(byte[].class,
persistentProperty.getTypeInformation().getActualType().getType())) {
Class<R> targetType = (Class<R>) persistentProperty.getTypeInformation().getActualType().getType();
Bucket bucket = source.getBucket().extract(currentPath + ".");
RedisData newBucket = new RedisData(bucket);
byte[] type = bucket.get(currentPath + "." + TYPE_HINT_ALIAS);
if (type != null && type.length > 0) {
newBucket.getBucket().put(TYPE_HINT_ALIAS, type);
}
R val = readInternal(currentPath, targetType, newBucket);
accessor.setProperty(persistentProperty, Optional.ofNullable(val));
} else {
if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) {
if (source.getBucket().get(currentPath) == null) {
accessor.setProperty(persistentProperty,
Optional.of(fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType())));
} else {
accessor.setProperty(persistentProperty, Optional.ofNullable(source.getId()));
}
}
Class<?> typeToUse = getTypeHint(currentPath, source.getBucket(), persistentProperty.getActualType());
accessor.setProperty(persistentProperty,
Optional.ofNullable(fromBytes(source.getBucket().get(currentPath), typeToUse)));
}
}
});
readAssociation(path, source, entity.get(), accessor);
return (R) instance;
}
private void readAssociation(final String path, final RedisData source, final RedisPersistentEntity<?> entity,
final PersistentPropertyAccessor accessor) {
entity.doWithAssociations(new AssociationHandler<RedisPersistentProperty>() {
@Override
public void doWithAssociation(Association<RedisPersistentProperty> association) {
String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName()
: association.getInverse().getName();
if (association.getInverse().isCollectionLike()) {
Bucket bucket = source.getBucket().extract(currentPath + ".[");
Collection<Object> target = CollectionFactory.createCollection(association.getInverse().getType(),
association.getInverse().getComponentType().orElse(Object.class), bucket.size());
for (Entry<String, byte[]> entry : bucket.entrySet()) {
String referenceKey = fromBytes(entry.getValue(), String.class);
String[] args = referenceKey.split(":");
Map<byte[], byte[]> rawHash = referenceResolver.resolveReference(args[1], args[0]);
if (!CollectionUtils.isEmpty(rawHash)) {
target.add(read(association.getInverse().getActualType(), new RedisData(rawHash)));
}
}
accessor.setProperty(association.getInverse(), Optional.ofNullable(target));
} else {
byte[] binKey = source.getBucket().get(currentPath);
if (binKey == null || binKey.length == 0) {
return;
}
String key = fromBytes(binKey, String.class);
String[] args = key.split(":");
Map<byte[], byte[]> rawHash = referenceResolver.resolveReference(args[1], args[0]);
if (!CollectionUtils.isEmpty(rawHash)) {
accessor.setProperty(association.getInverse(),
Optional.ofNullable(read(association.getInverse().getActualType(), new RedisData(rawHash))));
}
}
}
});
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object)
*/
@Override
@SuppressWarnings({ "rawtypes" })
public void write(Object source, final RedisData sink) {
if (source == null) {
return;
}
if (source instanceof PartialUpdate) {
writePartialUpdate((PartialUpdate) source, sink);
return;
}
final Optional<RedisPersistentEntity<?>> entity = mappingContext.getPersistentEntity(source.getClass());
if (!customConversions.hasCustomWriteTarget(source.getClass())) {
typeMapper.writeType(ClassUtils.getUserClass(source), sink);
}
if (!entity.isPresent()) {
typeMapper.writeType(ClassUtils.getUserClass(source), sink);
sink.getBucket().put("_raw", conversionService.convert(source, byte[].class));
return;
}
sink.setKeyspace(entity.get().getKeySpace());
writeInternal(entity.get().getKeySpace(), "", source, entity.get().getTypeInformation(), sink);
Optional<Object> identifyer = entity.get().getIdentifierAccessor(source).getIdentifier();
if (identifyer.isPresent()) {
sink.setId(getConversionService().convert(identifyer.get(), String.class));
}
Long ttl = entity.get().getTimeToLiveAccessor().getTimeToLive(source);
if (ttl != null && ttl > 0) {
sink.setTimeToLive(ttl);
}
for (IndexedData indexedData : indexResolver.resolveIndexesFor(entity.get().getTypeInformation(), source)) {
sink.addIndexedData(indexedData);
}
}
protected void writePartialUpdate(PartialUpdate<?> update, RedisData sink) {
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(update.getTarget()).get();
write(update.getValue(), sink);
if (sink.getBucket().keySet().contains(TYPE_HINT_ALIAS)) {
sink.getBucket().put(TYPE_HINT_ALIAS, null); // overwrite stuff in here
}
if (update.isRefreshTtl() && !update.getPropertyUpdates().isEmpty()) {
Long ttl = entity.getTimeToLiveAccessor().getTimeToLive(update);
if (ttl != null && ttl > 0) {
sink.setTimeToLive(ttl);
}
}
for (PropertyUpdate pUpdate : update.getPropertyUpdates()) {
String path = pUpdate.getPropertyPath();
if (UpdateCommand.SET.equals(pUpdate.getCmd())) {
writePartialPropertyUpdate(update, pUpdate, sink, entity, path);
}
}
}
/**
* @param update
* @param pUpdate
* @param sink
* @param entity
* @param path
*/
private void writePartialPropertyUpdate(PartialUpdate<?> update, PropertyUpdate pUpdate, RedisData sink,
RedisPersistentEntity<?> entity, String path) {
RedisPersistentProperty targetProperty = getTargetPropertyOrNullForPath(path, update.getTarget());
if (targetProperty == null) {
targetProperty = getTargetPropertyOrNullForPath(path.replaceAll("\\.\\[.*\\]", ""), update.getTarget());
TypeInformation<?> ti = targetProperty == null ? ClassTypeInformation.OBJECT
: (targetProperty.isMap()
? (targetProperty.getTypeInformation().getMapValueType() != null
? targetProperty.getTypeInformation().getMapValueType().get() : ClassTypeInformation.OBJECT)
: targetProperty.getTypeInformation().getActualType());
writeInternal(entity.getKeySpace(), pUpdate.getPropertyPath(), pUpdate.getValue(), ti, sink);
return;
}
if (targetProperty.isAssociation()) {
if (targetProperty.isCollectionLike()) {
RedisPersistentEntity<?> ref = mappingContext.getPersistentEntity(targetProperty.getAssociation().get()
.getInverse().getTypeInformation().getComponentType().get().getActualType()).get();
int i = 0;
for (Object o : (Collection<?>) pUpdate.getValue()) {
Optional<Object> refId = ref.getPropertyAccessor(o).getProperty(ref.getIdProperty().get());
if (refId.isPresent()) {
sink.getBucket().put(pUpdate.getPropertyPath() + ".[" + i + "]",
toBytes(ref.getKeySpace() + ":" + refId.get()));
i++;
}
}
} else {
RedisPersistentEntity<?> ref = mappingContext
.getPersistentEntity(targetProperty.getAssociation().get().getInverse().getTypeInformation()).get();
Optional<Object> refId = ref.getPropertyAccessor(pUpdate.getValue()).getProperty(ref.getIdProperty().get());
if (refId.isPresent()) {
sink.getBucket().put(pUpdate.getPropertyPath(), toBytes(ref.getKeySpace() + ":" + refId.get()));
}
}
} else if (targetProperty.isCollectionLike()) {
Collection<?> collection = pUpdate.getValue() instanceof Collection ? (Collection<?>) pUpdate.getValue()
: Collections.<Object> singleton(pUpdate.getValue());
writeCollection(entity.getKeySpace(), pUpdate.getPropertyPath(), collection,
targetProperty.getTypeInformation().getActualType(), sink);
} else if (targetProperty.isMap()) {
Map<Object, Object> map = new HashMap<Object, Object>();
if (pUpdate.getValue() instanceof Map) {
map.putAll((Map<?, ?>) pUpdate.getValue());
} else if (pUpdate.getValue() instanceof Entry) {
map.put(((Entry<?, ?>) pUpdate.getValue()).getKey(), ((Entry<?, ?>) pUpdate.getValue()).getValue());
} else {
throw new MappingException(
String.format("Cannot set update value for map property '%s' to '%s'. Please use a Map or Map.Entry.",
pUpdate.getPropertyPath(), pUpdate.getValue()));
}
writeMap(entity.getKeySpace(), pUpdate.getPropertyPath(), targetProperty.getMapValueType().orElse(Object.class),
map, sink);
} else {
writeInternal(entity.getKeySpace(), pUpdate.getPropertyPath(), pUpdate.getValue(),
targetProperty.getTypeInformation(), sink);
Set<IndexedData> data = indexResolver.resolveIndexesFor(entity.getKeySpace(), pUpdate.getPropertyPath(),
targetProperty.getTypeInformation(), pUpdate.getValue());
if (data.isEmpty()) {
data = indexResolver.resolveIndexesFor(entity.getKeySpace(), pUpdate.getPropertyPath(),
targetProperty.getOwner().getTypeInformation(), pUpdate.getValue());
}
sink.addIndexedData(data);
}
}
RedisPersistentProperty getTargetPropertyOrNullForPath(String path, Class<?> type) {
try {
PersistentPropertyPath<RedisPersistentProperty> persistentPropertyPath = mappingContext
.getPersistentPropertyPath(path, type);
return persistentPropertyPath.getLeafProperty();
} catch (Exception e) {
// that's just fine
}
return null;
}
/**
* @param keyspace
* @param path
* @param value
* @param typeHint
* @param sink
*/
private void writeInternal(final String keyspace, final String path, final Object value, TypeInformation<?> typeHint,
final RedisData sink) {
if (value == null) {
return;
}
if (customConversions.hasCustomWriteTarget(value.getClass())) {
if (!StringUtils.hasText(path) && customConversions.getCustomWriteTarget(value.getClass()).equals(byte[].class)) {
sink.getBucket().put(StringUtils.hasText(path) ? path : "_raw", conversionService.convert(value, byte[].class));
} else {
if (!ClassUtils.isAssignable(typeHint.getType(), value.getClass())) {
throw new MappingException(
String.format(INVALID_TYPE_ASSIGNMENT, value.getClass(), path, typeHint.getType()));
}
writeToBucket(path, value, sink, typeHint.getType());
}
return;
}
if (value.getClass() != typeHint.getType()) {
sink.getBucket().put((!path.isEmpty() ? path + "." + TYPE_HINT_ALIAS : TYPE_HINT_ALIAS),
toBytes(value.getClass().getName()));
}
final RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(value.getClass()).get();
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
entity.doWithProperties(new PropertyHandler<RedisPersistentProperty>() {
@Override
public void doWithPersistentProperty(RedisPersistentProperty persistentProperty) {
String propertyStringPath = (!path.isEmpty() ? path + "." : "") + persistentProperty.getName();
if (persistentProperty.isIdProperty()) {
if (accessor.getProperty(persistentProperty).isPresent()) {
sink.getBucket().put(propertyStringPath, toBytes(accessor.getProperty(persistentProperty).get()));
}
return;
}
if (persistentProperty.isMap()) {
if (accessor.getProperty(persistentProperty).isPresent()) {
writeMap(keyspace, propertyStringPath, persistentProperty.getMapValueType().orElse(Object.class),
(Map<?, ?>) accessor.getProperty(persistentProperty).get(), sink);
}
} else if (persistentProperty.isCollectionLike()) {
final Optional<Object> propertyValue = accessor.getProperty(persistentProperty);
if (!propertyValue.isPresent()) {
writeCollection(keyspace, propertyStringPath, (Iterable<?>) null,
persistentProperty.getTypeInformation().getComponentType().get(), sink);
} else {
if (Iterable.class.isAssignableFrom(propertyValue.get().getClass())) {
writeCollection(keyspace, propertyStringPath, (Iterable<?>) propertyValue.get(),
persistentProperty.getTypeInformation().getComponentType().get(), sink);
} else if (propertyValue.isPresent() && propertyValue.get().getClass().isArray()) {
writeCollection(keyspace, propertyStringPath, CollectionUtils.arrayToList(propertyValue.get()),
persistentProperty.getTypeInformation().getComponentType().get(), sink);
} else {
throw new RuntimeException("Don't know how to handle " + propertyValue.getClass() + " type collection");
}
}
} else if (persistentProperty.isEntity()) {
Optional<Object> propertyValue = accessor.getProperty(persistentProperty);
if (propertyValue.isPresent()) {
writeInternal(keyspace, propertyStringPath, propertyValue.get(),
persistentProperty.getTypeInformation().getActualType(), sink);
}
} else {
Optional<Object> propertyValue = accessor.getProperty(persistentProperty);
if (propertyValue.isPresent()) {
writeToBucket(propertyStringPath, propertyValue.get(), sink, persistentProperty.getType());
}
}
}
});
writeAssociation(path, entity, value, sink);
}
private void writeAssociation(final String path, final RedisPersistentEntity<?> entity, final Object value,
final RedisData sink) {
if (value == null) {
return;
}
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
entity.doWithAssociations(new AssociationHandler<RedisPersistentProperty>() {
@Override
public void doWithAssociation(Association<RedisPersistentProperty> association) {
Optional<Object> refObject = accessor.getProperty(association.getInverse());
if (!refObject.isPresent()) {
return;
}
if (association.getInverse().isCollectionLike()) {
RedisPersistentEntity<?> ref = mappingContext.getPersistentEntity(
association.getInverse().getTypeInformation().getComponentType().get().getActualType()).get();
String keyspace = ref.getKeySpace();
String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName();
int i = 0;
for (Object o : (Collection<?>) refObject.get()) {
Optional<Object> refId = ref.getPropertyAccessor(o).getProperty(ref.getIdProperty().get());
if (refId.isPresent()) {
sink.getBucket().put(propertyStringPath + ".[" + i + "]", toBytes(keyspace + ":" + refId.get()));
i++;
}
}
} else {
RedisPersistentEntity<?> ref = mappingContext
.getPersistentEntity(association.getInverse().getTypeInformation()).get();
String keyspace = ref.getKeySpace();
Optional<Object> refId = ref.getPropertyAccessor(refObject.get()).getProperty(ref.getIdProperty().get());
if (refId.isPresent()) {
String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName();
sink.getBucket().put(propertyStringPath, toBytes(keyspace + ":" + refId.get()));
}
}
}
});
}
/**
* @param keyspace
* @param path
* @param values
* @param typeHint
* @param sink
*/
private void writeCollection(String keyspace, String path, Iterable<?> values, TypeInformation<?> typeHint,
RedisData sink) {
if (values == null) {
return;
}
int i = 0;
for (Object value : values) {
if (value == null) {
break;
}
String currentPath = path + ".[" + i + "]";
if (!ClassUtils.isAssignable(typeHint.getType(), value.getClass())) {
throw new MappingException(
String.format(INVALID_TYPE_ASSIGNMENT, value.getClass(), currentPath, typeHint.getType()));
}
if (customConversions.hasCustomWriteTarget(value.getClass())) {
writeToBucket(currentPath, value, sink, typeHint.getType());
} else {
writeInternal(keyspace, currentPath, value, typeHint, sink);
}
i++;
}
}
private void writeToBucket(String path, Object value, RedisData sink, Class<?> propertyType) {
if (value == null || (value instanceof Optional && !((Optional<?>) value).isPresent())) {
return;
}
if (customConversions.hasCustomWriteTarget(value.getClass())) {
Optional<Class<?>> targetType = customConversions.getCustomWriteTarget(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 (targetType.filter(it -> ClassUtils.isAssignable(Map.class, it)).isPresent()) {
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 (targetType.filter(it -> ClassUtils.isAssignable(byte[].class, it)).isPresent()) {
sink.getBucket().put(path, toBytes(value));
} else {
throw new IllegalArgumentException(
String.format("Cannot convert value '%s' of type %s to bytes.", value, value.getClass()));
}
}
}
private Object readCollectionOrArray(String path, Class<?> collectionType, Class<?> valueType, Bucket bucket) {
List<String> keys = new ArrayList<String>(bucket.extractAllKeysFor(path));
Collections.sort(keys, listKeyComparator);
boolean isArray = collectionType.isArray();
Class<?> collectionTypeToUse = isArray ? ArrayList.class : collectionType;
Collection<Object> target = CollectionFactory.createCollection(collectionTypeToUse, valueType, keys.size());
for (String key : keys) {
if (key.endsWith(TYPE_HINT_ALIAS)) {
continue;
}
Bucket elementData = bucket.extract(key);
byte[] typeInfo = elementData.get(key + "." + TYPE_HINT_ALIAS);
if (typeInfo != null && typeInfo.length > 0) {
elementData.put(TYPE_HINT_ALIAS, typeInfo);
}
Class<?> typeToUse = getTypeHint(key, elementData, valueType);
if (conversionService.canConvert(byte[].class, typeToUse)) {
target.add(fromBytes(elementData.get(key), typeToUse));
} else {
target.add(readInternal(key, valueType, new RedisData(elementData)));
}
}
return isArray ? toArray(target, collectionType, valueType) : (target.isEmpty() ? null : target);
}
/**
* @param keyspace
* @param path
* @param mapValueType
* @param source
* @param sink
*/
private void writeMap(String keyspace, String path, Class<?> mapValueType, Map<?, ?> source, RedisData sink) {
if (CollectionUtils.isEmpty(source)) {
return;
}
for (Map.Entry<?, ?> entry : source.entrySet()) {
if (entry.getValue() == null || entry.getKey() == null) {
continue;
}
String currentPath = path + ".[" + entry.getKey() + "]";
if (!ClassUtils.isAssignable(mapValueType, entry.getValue().getClass())) {
throw new MappingException(
String.format(INVALID_TYPE_ASSIGNMENT, entry.getValue().getClass(), currentPath, mapValueType));
}
if (customConversions.hasCustomWriteTarget(entry.getValue().getClass())) {
writeToBucket(currentPath, entry.getValue(), sink, mapValueType);
} else {
writeInternal(keyspace, currentPath, entry.getValue(), ClassTypeInformation.from(mapValueType), sink);
}
}
}
/**
* @param path
* @param mapType
* @param keyType
* @param valueType
* @param source
* @return
*/
private Map<?, ?> readMapOfSimpleTypes(String path, Class<?> mapType, Class<?> keyType, Class<?> valueType,
RedisData source) {
Bucket partial = source.getBucket().extract(path + ".[");
Map<Object, Object> target = CollectionFactory.createMap(mapType, partial.size());
for (Entry<String, byte[]> entry : partial.entrySet()) {
if (entry.getKey().endsWith(TYPE_HINT_ALIAS)) {
continue;
}
String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(entry.getKey());
if (!matcher.find()) {
throw new IllegalArgumentException(
String.format("Cannot extract map value for key '%s' in path '%s'.", entry.getKey(), path));
}
String key = matcher.group(2);
Class<?> typeToUse = getTypeHint(path + ".[" + key + "]", source.getBucket(), valueType);
target.put(key, fromBytes(entry.getValue(), typeToUse));
}
return target.isEmpty() ? null : target;
}
/**
* @param path
* @param mapType
* @param keyType
* @param valueType
* @param source
* @return
*/
private Map<?, ?> readMapOfComplexTypes(String path, Class<?> mapType, Class<?> keyType, Class<?> valueType,
RedisData source) {
Set<String> keys = source.getBucket().extractAllKeysFor(path);
Map<Object, Object> target = CollectionFactory.createMap(mapType, keys.size());
for (String key : keys) {
String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])";
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(key);
if (!matcher.find()) {
throw new IllegalArgumentException(
String.format("Cannot extract map value for key '%s' in path '%s'.", key, path));
}
String mapKey = matcher.group(2);
Bucket partial = source.getBucket().extract(key);
byte[] typeInfo = partial.get(key + "." + TYPE_HINT_ALIAS);
if (typeInfo != null && typeInfo.length > 0) {
partial.put(TYPE_HINT_ALIAS, typeInfo);
}
Object o = readInternal(key, valueType, new RedisData(partial));
target.put(mapKey, o);
}
return target.isEmpty() ? null : target;
}
private Class<?> getTypeHint(String path, Bucket bucket, Class<?> fallback) {
byte[] typeInfo = bucket.get(path + "." + TYPE_HINT_ALIAS);
if (typeInfo == null || typeInfo.length < 1) {
return fallback;
}
String typeName = fromBytes(typeInfo, String.class);
try {
return ClassUtils.forName(typeName, this.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
throw new MappingException(String.format("Cannot find class for type %s. ", typeName), e);
} catch (LinkageError e) {
throw new MappingException(String.format("Cannot find class for type %s. ", typeName), e);
}
}
/**
* Convert given source to binary representation using the underlying {@link ConversionService}.
*
* @param source
* @return
* @throws ConverterNotFoundException
*/
public byte[] toBytes(Object source) {
if (source instanceof byte[]) {
return (byte[]) source;
}
return conversionService.convert(source, byte[].class);
}
/**
* Convert given binary representation to desired target type using the underlying {@link ConversionService}.
*
* @param source
* @param type
* @return
* @throws ConverterNotFoundException
*/
public <T> T fromBytes(byte[] source, Class<T> type) {
return conversionService.convert(source, type);
}
/**
* Converts a given {@link Collection} into an array considering primitive types.
*
* @param source {@link Collection} of values to be added to the array.
* @param arrayType {@link Class} of array.
* @param valueType to be used for conversion before setting the actual value.
* @return
*/
private Object toArray(Collection<Object> source, Class<?> arrayType, Class<?> valueType) {
if (source.isEmpty()) {
return null;
}
if (!ClassUtils.isPrimitiveArray(arrayType)) {
return source.toArray((Object[]) Array.newInstance(valueType, source.size()));
}
Object targetArray = Array.newInstance(valueType, source.size());
Iterator<Object> iterator = source.iterator();
int i = 0;
while (iterator.hasNext()) {
Array.set(targetArray, i, conversionService.convert(iterator.next(), valueType));
i++;
}
return i > 0 ? targetArray : null;
}
/**
* Set {@link CustomConversions} to be applied.
*
* @param customConversions
*/
public void setCustomConversions(CustomConversions customConversions) {
this.customConversions = customConversions != null ? customConversions : new RedisCustomConversions();
}
public void setReferenceResolver(ReferenceResolver referenceResolver) {
this.referenceResolver = referenceResolver;
}
public void setIndexResolver(IndexResolver indexResolver) {
this.indexResolver = indexResolver;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityConverter#getMappingContext()
*/
@Override
public RedisMappingContext getMappingContext() {
return this.mappingContext;
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.EntityConverter#getConversionService()
*/
@Override
public ConversionService getConversionService() {
return this.conversionService;
}
@Override
public void afterPropertiesSet() {
this.initializeConverters();
}
private void initializeConverters() {
customConversions.registerConvertersIn(conversionService);
}
/**
* @author Christoph Strobl
*/
private static class ConverterAwareParameterValueProvider implements PropertyValueProvider<RedisPersistentProperty> {
private final String path;
private final RedisData source;
private final ConversionService conversionService;
public ConverterAwareParameterValueProvider(String path, RedisData source, ConversionService conversionService) {
this.path = path;
this.source = source;
this.conversionService = conversionService;
}
@Override
@SuppressWarnings("unchecked")
public <T> Optional<T> getPropertyValue(RedisPersistentProperty property) {
String name = StringUtils.hasText(path) ? path + "." + property.getName() : property.getName();
return Optional.ofNullable((T) conversionService.convert(source.getBucket().get(name), property.getActualType()));
}
}
/**
* @author Christoph Strobl
*/
private static class RedisTypeAliasAccessor implements TypeAliasAccessor<RedisData> {
private final String typeKey;
private final ConversionService conversionService;
RedisTypeAliasAccessor(ConversionService conversionService) {
this(conversionService, TYPE_HINT_ALIAS);
}
RedisTypeAliasAccessor(ConversionService conversionService, String typeKey) {
this.conversionService = conversionService;
this.typeKey = typeKey;
}
@Override
public Alias readAliasFrom(RedisData source) {
return Alias
.ofOptional(Optional.ofNullable(conversionService.convert(source.getBucket().get(typeKey), String.class)));
}
@Override
public void writeTypeTo(RedisData sink, Object alias) {
sink.getBucket().put(typeKey, conversionService.convert(alias, byte[].class));
}
}
enum ClassNameKeySpaceResolver implements KeySpaceResolver {
INSTANCE;
/*
* (non-Javadoc)
* @see org.springframework.data.keyvalue.core.KeySpaceResolver#resolveKeySpace(java.lang.Class)
*/
@Override
public String resolveKeySpace(Class<?> type) {
Assert.notNull(type, "Type must not be null!");
return ClassUtils.getUserClass(type).getName();
}
}
private enum NaturalOrderingKeyComparator implements Comparator<String> {
INSTANCE;
/*
* (non-Javadoc)
* @see java.util.Comparator#compare(java.lang.Object, java.lang.Object)
*/
public int compare(String s1, String s2) {
int s1offset = 0;
int s2offset = 0;
while (s1offset < s1.length() && s2offset < s2.length()) {
Part thisPart = extractPart(s1, s1offset);
Part thatPart = extractPart(s2, s2offset);
int result = thisPart.compareTo(thatPart);
if (result != 0) {
return result;
}
s1offset += thisPart.length();
s2offset += thatPart.length();
}
return 0;
}
private Part extractPart(String source, int offset) {
StringBuilder builder = new StringBuilder();
char c = source.charAt(offset);
builder.append(c);
boolean isDigit = Character.isDigit(c);
for (int i = offset + 1; i < source.length(); i++) {
c = source.charAt(i);
if ((isDigit && !Character.isDigit(c)) || (!isDigit && Character.isDigit(c))) {
break;
}
builder.append(c);
}
return new Part(builder.toString(), isDigit);
}
private static class Part implements Comparable<Part> {
private final String rawValue;
private final Long longValue;
Part(String value, boolean isDigit) {
this.rawValue = value;
this.longValue = isDigit ? Long.valueOf(value) : null;
}
boolean isNumeric() {
return longValue != null;
}
int length() {
return rawValue.length();
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(Part that) {
if (this.isNumeric() && that.isNumeric()) {
return this.longValue.compareTo(that.longValue);
}
return this.rawValue.compareTo(that.rawValue);
}
}
}
}