All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.data.redis.core.convert.MappingRedisConverter Maven / Gradle / Ivy

There is a newer version: 3.2.5
Show newest version
/*
 * Copyright 2015-2018 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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.RequiredArgsConstructor;

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.Map.Entry;
import java.util.Optional;
import java.util.Set;
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.EntityInstantiator;
import org.springframework.data.convert.EntityInstantiators;
import org.springframework.data.mapping.AssociationHandler;
import org.springframework.data.mapping.MappingException;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PreferredConstructor;
import org.springframework.data.mapping.PropertyHandler;
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.redis.util.ByteUtils;
import org.springframework.data.util.ClassTypeInformation;
import org.springframework.data.util.TypeInformation;
import org.springframework.lang.Nullable;
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. 
*
* NOTE {@link MappingRedisConverter} is an {@link InitializingBean} and requires * {@link MappingRedisConverter#afterPropertiesSet()} to be called. * *
 * 
 * @RedisHash("persons")
 * class Person {
 *
 *   @Id String id;
 *   String firstname;
 *
 *   List nicknames;
 *   List coworkers;
 *
 *   Address address;
 *   @Reference Country nationality;
 * }
 * 
 * 
* * The above is represented as: * *
 * 
 * _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
 * 
 * 
* * @author Christoph Strobl * @author Greg Turnquist * @author Mark Paluch * @since 1.7 */ public class MappingRedisConverter implements RedisConverter, InitializingBean { 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 RedisTypeMapper typeMapper; private final Comparator listKeyComparator = new NullSafeComparator<>(NaturalOrderingKeyComparator.INSTANCE, true); private IndexResolver indexResolver; private @Nullable ReferenceResolver referenceResolver; private CustomConversions customConversions; /** * Creates new {@link MappingRedisConverter}. * * @param context can be {@literal null}. */ MappingRedisConverter(RedisMappingContext context) { this(context, null, 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 can be not be {@literal null}. */ public MappingRedisConverter(@Nullable RedisMappingContext mappingContext, @Nullable IndexResolver indexResolver, @Nullable ReferenceResolver referenceResolver) { this(mappingContext, indexResolver, referenceResolver, 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 can be {@literal null}. * @param typeMapper can be {@literal null}. * @since 2.1 */ public MappingRedisConverter(@Nullable RedisMappingContext mappingContext, @Nullable IndexResolver indexResolver, @Nullable ReferenceResolver referenceResolver, @Nullable RedisTypeMapper typeMapper) { this.mappingContext = mappingContext != null ? mappingContext : new RedisMappingContext(); this.entityInstantiators = new EntityInstantiators(); this.conversionService = new DefaultConversionService(); this.customConversions = new RedisCustomConversions(); this.typeMapper = typeMapper != null ? typeMapper : new DefaultRedisTypeMapper(DefaultRedisTypeMapper.DEFAULT_TYPE_KEY, this.mappingContext); this.indexResolver = indexResolver != null ? indexResolver : new PathIndexResolver(this.mappingContext); this.referenceResolver = referenceResolver; } /* * (non-Javadoc) * @see org.springframework.data.convert.EntityReader#read(java.lang.Class, java.lang.Object) */ @Override public R read(Class type, RedisData source) { return readInternal("", type, source); } @SuppressWarnings("unchecked") @Nullable private R readInternal(String path, Class type, RedisData source) { if (source.getBucket() == null || source.getBucket().isEmpty()) { return null; } TypeInformation readType = typeMapper.readType(source.getBucket().getPath(), ClassTypeInformation.from(type)); RedisPersistentEntity entity = mappingContext.getPersistentEntity(readType); if (customConversions.hasCustomReadTarget(Map.class, readType.getType())) { Map partial = new HashMap<>(); if (!path.isEmpty()) { for (Entry 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 != null && entity.hasIdProperty()) { PersistentPropertyAccessor propertyAccessor = entity.getPropertyAccessor(instance); propertyAccessor.setProperty(entity.getRequiredIdProperty(), source.getId()); instance = propertyAccessor.getBean(); } 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); Object instance = instantiator.createInstance((RedisPersistentEntity) entity, new PersistentEntityParameterValueProvider<>(entity, new ConverterAwareParameterValueProvider(path, source, conversionService), this.conversionService)); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance); entity.doWithProperties((PropertyHandler) persistentProperty -> { PreferredConstructor constructor = entity.getPersistenceConstructor(); if (constructor.isConstructorParameter(persistentProperty)) { return; } Object targetValue = readProperty(path, source, persistentProperty); if (targetValue != null) { accessor.setProperty(persistentProperty, targetValue); } }); readAssociation(path, source, entity, accessor); return (R) accessor.getBean(); } @SuppressWarnings("unchecked") @Nullable protected Object readProperty(String path, RedisData source, RedisPersistentProperty persistentProperty) { String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName(); TypeInformation typeInformation = persistentProperty.getTypeInformation(); if (persistentProperty.isMap()) { Class mapValueType = persistentProperty.getMapValueType(); if (mapValueType == null) { throw new IllegalArgumentException("Unable to retrieve MapValueType!"); } if (conversionService.canConvert(byte[].class, mapValueType)) { return readMapOfSimpleTypes(currentPath, typeInformation.getType(), typeInformation.getRequiredComponentType().getType(), mapValueType, source); } return readMapOfComplexTypes(currentPath, typeInformation.getType(), typeInformation.getRequiredComponentType().getType(), mapValueType, source); } if (typeInformation.isCollectionLike()) { return readCollectionOrArray(currentPath, typeInformation.getType(), typeInformation.getRequiredComponentType().getActualType().getType(), source.getBucket()); } if (persistentProperty.isEntity() && !conversionService.canConvert(byte[].class, typeInformation.getActualType().getType())) { Bucket bucket = source.getBucket().extract(currentPath + "."); RedisData newBucket = new RedisData(bucket); TypeInformation typeToRead = typeMapper.readType(bucket.getPropertyPath(currentPath), typeInformation.getActualType()); return readInternal(currentPath, typeToRead.getType(), newBucket); } byte[] sourceBytes = source.getBucket().get(currentPath); if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) { return sourceBytes == null ? fromBytes(sourceBytes, typeInformation.getActualType().getType()) : source.getId(); } Class typeToUse = getTypeHint(currentPath, source.getBucket(), persistentProperty.getActualType()); return fromBytes(sourceBytes, typeToUse); } private void readAssociation(String path, RedisData source, RedisPersistentEntity entity, PersistentPropertyAccessor accessor) { entity.doWithAssociations((AssociationHandler) association -> { String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() : association.getInverse().getName(); if (association.getInverse().isCollectionLike()) { Bucket bucket = source.getBucket().extract(currentPath + ".["); Collection target = CollectionFactory.createCollection(association.getInverse().getType(), association.getInverse().getComponentType(), bucket.size()); for (Entry entry : bucket.entrySet()) { String referenceKey = fromBytes(entry.getValue(), String.class); if (!KeyspaceIdentifier.isValid(referenceKey)) { continue; } KeyspaceIdentifier identifier = KeyspaceIdentifier.of(referenceKey); Map rawHash = referenceResolver.resolveReference(identifier.getId(), identifier.getKeyspace()); if (!CollectionUtils.isEmpty(rawHash)) { target.add(read(association.getInverse().getActualType(), new RedisData(rawHash))); } } accessor.setProperty(association.getInverse(), target); } else { byte[] binKey = source.getBucket().get(currentPath); if (binKey == null || binKey.length == 0) { return; } String referenceKey = fromBytes(binKey, String.class); if (KeyspaceIdentifier.isValid(referenceKey)) { KeyspaceIdentifier identifier = KeyspaceIdentifier.of(referenceKey); Map rawHash = referenceResolver.resolveReference(identifier.getId(), identifier.getKeyspace()); if (!CollectionUtils.isEmpty(rawHash)) { accessor.setProperty(association.getInverse(), 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, RedisData sink) { if (source == null) { return; } if (source instanceof PartialUpdate) { writePartialUpdate((PartialUpdate) source, sink); return; } RedisPersistentEntity entity = mappingContext.getPersistentEntity(source.getClass()); if (!customConversions.hasCustomWriteTarget(source.getClass())) { typeMapper.writeType(ClassUtils.getUserClass(source), sink.getBucket().getPath()); } if (entity == null) { typeMapper.writeType(ClassUtils.getUserClass(source), sink.getBucket().getPath()); sink.getBucket().put("_raw", conversionService.convert(source, byte[].class)); return; } sink.setKeyspace(entity.getKeySpace()); writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink); Object identifier = entity.getIdentifierAccessor(source).getIdentifier(); if (identifier != null) { sink.setId(getConversionService().convert(identifier, String.class)); } Long ttl = entity.getTimeToLiveAccessor().getTimeToLive(source); if (ttl != null && ttl > 0) { sink.setTimeToLive(ttl); } for (IndexedData indexedData : indexResolver.resolveIndexesFor(entity.getTypeInformation(), source)) { sink.addIndexedData(indexedData); } } protected void writePartialUpdate(PartialUpdate update, RedisData sink) { RedisPersistentEntity entity = mappingContext.getRequiredPersistentEntity(update.getTarget()); write(update.getValue(), sink); for (String key : sink.getBucket().keySet()) { if (typeMapper.isTypeKey(key)) { sink.getBucket().remove(key); break; } } 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().getRequiredMapValueType() : 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.getRequiredAssociation() .getInverse().getTypeInformation().getRequiredComponentType().getActualType()); int i = 0; for (Object o : (Collection) pUpdate.getValue()) { Object refId = ref.getPropertyAccessor(o).getProperty(ref.getRequiredIdProperty()); if (refId != null) { sink.getBucket().put(pUpdate.getPropertyPath() + ".[" + i + "]", toBytes(ref.getKeySpace() + ":" + refId)); i++; } } } else { RedisPersistentEntity ref = mappingContext .getRequiredPersistentEntity(targetProperty.getRequiredAssociation().getInverse().getTypeInformation()); Object refId = ref.getPropertyAccessor(pUpdate.getValue()).getProperty(ref.getRequiredIdProperty()); if (refId != null) { sink.getBucket().put(pUpdate.getPropertyPath(), toBytes(ref.getKeySpace() + ":" + refId)); } } } else if (targetProperty.isCollectionLike()) { Collection collection = pUpdate.getValue() instanceof Collection ? (Collection) pUpdate.getValue() : Collections. singleton(pUpdate.getValue()); writeCollection(entity.getKeySpace(), pUpdate.getPropertyPath(), collection, targetProperty.getTypeInformation().getActualType(), sink); } else if (targetProperty.isMap()) { Map map = new HashMap<>(); 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(), map, sink); } else { writeInternal(entity.getKeySpace(), pUpdate.getPropertyPath(), pUpdate.getValue(), targetProperty.getTypeInformation(), sink); Set 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); } } @Nullable RedisPersistentProperty getTargetPropertyOrNullForPath(String path, Class type) { try { PersistentPropertyPath 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(String keyspace, String path, @Nullable Object value, TypeInformation typeHint, 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()) { typeMapper.writeType(value.getClass(), sink.getBucket().getPropertyPath(path)); } RedisPersistentEntity entity = mappingContext.getRequiredPersistentEntity(value.getClass()); PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); entity.doWithProperties((PropertyHandler) persistentProperty -> { String propertyStringPath = (!path.isEmpty() ? path + "." : "") + persistentProperty.getName(); Object propertyValue = accessor.getProperty(persistentProperty); if (persistentProperty.isIdProperty()) { if (propertyValue != null) { sink.getBucket().put(propertyStringPath, toBytes(propertyValue)); } return; } if (persistentProperty.isMap()) { if (propertyValue != null) { writeMap(keyspace, propertyStringPath, persistentProperty.getMapValueType(), (Map) propertyValue, sink); } } else if (persistentProperty.isCollectionLike()) { if (propertyValue == null) { writeCollection(keyspace, propertyStringPath, null, persistentProperty.getTypeInformation().getRequiredComponentType(), sink); } else { if (Iterable.class.isAssignableFrom(propertyValue.getClass())) { writeCollection(keyspace, propertyStringPath, (Iterable) propertyValue, persistentProperty.getTypeInformation().getRequiredComponentType(), sink); } else if (propertyValue.getClass().isArray()) { writeCollection(keyspace, propertyStringPath, CollectionUtils.arrayToList(propertyValue), persistentProperty.getTypeInformation().getRequiredComponentType(), sink); } else { throw new RuntimeException("Don't know how to handle " + propertyValue.getClass() + " type collection"); } } } else if (persistentProperty.isEntity()) { if (propertyValue != null) { writeInternal(keyspace, propertyStringPath, propertyValue, persistentProperty.getTypeInformation().getActualType(), sink); } } else { if (propertyValue != null) { writeToBucket(propertyStringPath, propertyValue, sink, persistentProperty.getType()); } } }); writeAssociation(path, entity, value, sink); } private void writeAssociation(String path, RedisPersistentEntity entity, Object value, RedisData sink) { if (value == null) { return; } PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value); entity.doWithAssociations((AssociationHandler) association -> { Object refObject = accessor.getProperty(association.getInverse()); if (refObject == null) { return; } if (association.getInverse().isCollectionLike()) { RedisPersistentEntity ref = mappingContext.getRequiredPersistentEntity( association.getInverse().getTypeInformation().getRequiredComponentType().getActualType()); String keyspace = ref.getKeySpace(); String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName(); int i = 0; for (Object o : (Collection) refObject) { Object refId = ref.getPropertyAccessor(o).getProperty(ref.getRequiredIdProperty()); if (refId != null) { sink.getBucket().put(propertyStringPath + ".[" + i + "]", toBytes(keyspace + ":" + refId)); i++; } } } else { RedisPersistentEntity ref = mappingContext .getRequiredPersistentEntity(association.getInverse().getTypeInformation()); String keyspace = ref.getKeySpace(); Object refId = ref.getPropertyAccessor(refObject).getProperty(ref.getIdProperty()); if (refId != null) { String propertyStringPath = (!path.isEmpty() ? path + "." : "") + association.getInverse().getName(); sink.getBucket().put(propertyStringPath, toBytes(keyspace + ":" + refId)); } } }); } /** * @param keyspace * @param path * @param values * @param typeHint * @param sink */ private void writeCollection(String keyspace, String path, @Nullable 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, @Nullable Object value, RedisData sink, Class propertyType) { if (value == null || (value instanceof Optional && !((Optional) value).isPresent())) { return; } if (customConversions.hasCustomWriteTarget(value.getClass())) { Optional> targetType = customConversions.getCustomWriteTarget(value.getClass()); if (!targetType.filter(it -> ClassUtils.isAssignable(Map.class, it)).isPresent() && customConversions.isSimpleType(value.getClass()) && value.getClass() != propertyType) { typeMapper.writeType(value.getClass(), sink.getBucket().getPropertyPath(path)); } 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 keys = new ArrayList<>(bucket.extractAllKeysFor(path)); keys.sort(listKeyComparator); boolean isArray = collectionType.isArray(); Class collectionTypeToUse = isArray ? ArrayList.class : collectionType; Collection target = CollectionFactory.createCollection(collectionTypeToUse, valueType, keys.size()); for (String key : keys) { if (typeMapper.isTypeKey(key)) { continue; } Bucket elementData = bucket.extract(key); TypeInformation typeInformation = typeMapper.readType(elementData.getPropertyPath(key), ClassTypeInformation.from(valueType)); Class typeToUse = typeInformation.getType(); if (conversionService.canConvert(byte[].class, typeToUse)) { target.add(fromBytes(elementData.get(key), typeToUse)); } else { target.add(readInternal(key, typeToUse, 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 + ".[" + mapMapKey(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); } } } private String mapMapKey(Object key) { if (conversionService.canConvert(key.getClass(), byte[].class)) { return new String(conversionService.convert(key, byte[].class)); } return conversionService.convert(key, String.class); } /** * @param path * @param mapType * @param keyType * @param valueType * @param source * @return */ @Nullable private Map readMapOfSimpleTypes(String path, Class mapType, Class keyType, Class valueType, RedisData source) { Bucket partial = source.getBucket().extract(path + ".["); Map target = CollectionFactory.createMap(mapType, partial.size()); for (Entry entry : partial.entrySet()) { if (typeMapper.isTypeKey(entry.getKey())) { continue; } Object key = extractMapKeyForPath(path, entry.getKey(), keyType); 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 */ @Nullable private Map readMapOfComplexTypes(String path, Class mapType, Class keyType, Class valueType, RedisData source) { Set keys = source.getBucket().extractAllKeysFor(path); Map target = CollectionFactory.createMap(mapType, keys.size()); for (String key : keys) { Bucket partial = source.getBucket().extract(key); Object mapKey = extractMapKeyForPath(path, key, keyType); TypeInformation typeInformation = typeMapper.readType(source.getBucket().getPropertyPath(key), ClassTypeInformation.from(valueType)); Object o = readInternal(key, typeInformation.getType(), new RedisData(partial)); target.put(mapKey, o); } return target.isEmpty() ? null : target; } private Object extractMapKeyForPath(String path, String key, Class targetType) { 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)); } Object mapKey = matcher.group(2); if (ClassUtils.isAssignable(targetType, mapKey.getClass())) { return mapKey; } return conversionService.convert(toBytes(mapKey), targetType); } private Class getTypeHint(String path, Bucket bucket, Class fallback) { TypeInformation typeInformation = typeMapper.readType(bucket.getPropertyPath(path), ClassTypeInformation.from(fallback)); return typeInformation.getType(); } /** * 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 fromBytes(byte[] source, Class 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 */ @Nullable private Object toArray(Collection 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 iterator = source.iterator(); int i = 0; while (iterator.hasNext()) { Array.set(targetArray, i, conversionService.convert(iterator.next(), valueType)); i++; } return i > 0 ? targetArray : null; } public void setIndexResolver(IndexResolver indexResolver) { this.indexResolver = indexResolver; } public void setReferenceResolver(ReferenceResolver referenceResolver) { this.referenceResolver = referenceResolver; } /** * Set {@link CustomConversions} to be applied. * * @param customConversions */ public void setCustomConversions(@Nullable CustomConversions customConversions) { this.customConversions = customConversions != null ? customConversions : new RedisCustomConversions(); } /* * (non-Javadoc) * @see org.springframework.data.convert.EntityConverter#getMappingContext() */ @Override public RedisMappingContext getMappingContext() { return this.mappingContext; } /* * (non-Javadoc) * @see org.springframework.data.redis.core.convert.RedisConverter#getIndexResolver() */ @Nullable @Override public IndexResolver getIndexResolver() { return this.indexResolver; } /* * (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 * @author Mark Paluch */ @RequiredArgsConstructor private class ConverterAwareParameterValueProvider implements PropertyValueProvider { private final String path; private final RedisData source; private final ConversionService conversionService; @Override @SuppressWarnings("unchecked") public T getPropertyValue(RedisPersistentProperty property) { Object value = readProperty(path, source, property); if (value != null && !property.getActualType().isInstance(value)) { return (T) conversionService.convert(value, property.getActualType()); } return (T) value; } } private enum NaturalOrderingKeyComparator implements Comparator { 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 { private final String rawValue; private final @Nullable 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); } } } /** * Value object representing a Redis Hash/Object identifier composed from keyspace and object id in the form of * {@literal keyspace:id}. * * @author Mark Paluch * @since 1.8.10 */ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter public static class KeyspaceIdentifier { public static final String PHANTOM = "phantom"; public static final String DELIMITTER = ":"; public static final String PHANTOM_SUFFIX = DELIMITTER + PHANTOM; private String keyspace; private String id; private boolean phantomKey; /** * Parse a {@code key} into {@link KeyspaceIdentifier}. * * @param key the key representation. * @return {@link BinaryKeyspaceIdentifier} for binary key. */ public static KeyspaceIdentifier of(String key) { Assert.isTrue(isValid(key), String.format("Invalid key %s", key)); boolean phantomKey = key.endsWith(PHANTOM_SUFFIX); int keyspaceEndIndex = key.indexOf(DELIMITTER); String keyspace = key.substring(0, keyspaceEndIndex); String id; if (phantomKey) { id = key.substring(keyspaceEndIndex + 1, key.length() - PHANTOM_SUFFIX.length()); } else { id = key.substring(keyspaceEndIndex + 1); } return new KeyspaceIdentifier(keyspace, id, phantomKey); } /** * Check whether the {@code key} is valid, in particular whether the key contains a keyspace and an id part in the * form of {@literal keyspace:id}. * * @param key the key. * @return {@literal true} if the key is valid. */ public static boolean isValid(String key) { if (key == null) { return false; } int keyspaceEndIndex = key.indexOf(DELIMITTER); return keyspaceEndIndex > 0 && key.length() > keyspaceEndIndex; } } /** * Value object representing a binary Redis Hash/Object identifier composed from keyspace and object id in the form of * {@literal keyspace:id}. * * @author Mark Paluch * @since 1.8.10 */ @AllArgsConstructor(access = AccessLevel.PRIVATE) @Getter public static class BinaryKeyspaceIdentifier { public static final byte[] PHANTOM = KeyspaceIdentifier.PHANTOM.getBytes(); public static final byte DELIMITTER = ':'; public static final byte[] PHANTOM_SUFFIX = ByteUtils.concat(new byte[] { DELIMITTER }, PHANTOM); private byte[] keyspace; private byte[] id; private boolean phantomKey; /** * Parse a binary {@code key} into {@link BinaryKeyspaceIdentifier}. * * @param key the binary key representation. * @return {@link BinaryKeyspaceIdentifier} for binary key. */ public static BinaryKeyspaceIdentifier of(byte[] key) { Assert.isTrue(isValid(key), String.format("Invalid key %s", new String(key))); boolean phantomKey = ByteUtils.startsWith(key, PHANTOM_SUFFIX, key.length - PHANTOM_SUFFIX.length); int keyspaceEndIndex = ByteUtils.indexOf(key, DELIMITTER); byte[] keyspace = extractKeyspace(key, keyspaceEndIndex); byte[] id = extractId(key, phantomKey, keyspaceEndIndex); return new BinaryKeyspaceIdentifier(keyspace, id, phantomKey); } /** * Check whether the {@code key} is valid, in particular whether the key contains a keyspace and an id part in the * form of {@literal keyspace:id}. * * @param key the key. * @return {@literal true} if the key is valid. */ public static boolean isValid(byte[] key) { if (key == null) { return false; } int keyspaceEndIndex = ByteUtils.indexOf(key, DELIMITTER); return keyspaceEndIndex > 0 && key.length > keyspaceEndIndex; } private static byte[] extractId(byte[] key, boolean phantomKey, int keyspaceEndIndex) { int idSize; if (phantomKey) { idSize = (key.length - PHANTOM_SUFFIX.length) - (keyspaceEndIndex + 1); } else { idSize = key.length - (keyspaceEndIndex + 1); } byte[] id = new byte[idSize]; System.arraycopy(key, keyspaceEndIndex + 1, id, 0, idSize); return id; } private static byte[] extractKeyspace(byte[] key, int keyspaceEndIndex) { byte[] keyspace = new byte[keyspaceEndIndex]; System.arraycopy(key, 0, keyspace, 0, keyspaceEndIndex); return keyspace; } } }