spring-data-redis MappingRedisConverter 源码
spring-data-redis MappingRedisConverter 代码
文件路径:/src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java
/*
* Copyright 2015-2022 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
*
* https://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.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.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.EntityInstantiator;
import org.springframework.data.mapping.model.EntityInstantiators;
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.ProxyUtils;
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.ObjectUtils;
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
* @author Golam Mazid Sajib
* @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<String> 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}.
* @since 2.4
*/
public 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;
}
@Override
@SuppressWarnings("unchecked")
public <R> R read(Class<R> type, RedisData source) {
TypeInformation<?> readType = typeMapper.readType(source.getBucket().getPath(), ClassTypeInformation.from(type));
return readType.isCollectionLike()
? (R) readCollectionOrArray("", ArrayList.class, Object.class, source.getBucket())
: doReadInternal("", type, source);
}
@Nullable
private <R> R readInternal(String path, Class<R> type, RedisData source) {
return source.getBucket().isEmpty() ? null : doReadInternal(path, type, source);
}
@SuppressWarnings("unchecked")
private <R> R doReadInternal(String path, Class<R> type, RedisData source) {
TypeInformation<?> readType = typeMapper.readType(source.getBucket().getPath(), ClassTypeInformation.from(type));
if (customConversions.hasCustomReadTarget(Map.class, readType.getType())) {
Map<String, byte[]> partial = new HashMap<>();
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());
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(readType);
if (entity != null && entity.hasIdProperty()) {
PersistentPropertyAccessor<R> 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());
}
RedisPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(readType);
EntityInstantiator instantiator = entityInstantiators.getInstantiatorFor(entity);
Object instance = instantiator.createInstance((RedisPersistentEntity<RedisPersistentProperty>) entity,
new PersistentEntityParameterValueProvider<>(entity,
new ConverterAwareParameterValueProvider(path, source, conversionService), this.conversionService));
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(instance);
entity.doWithProperties((PropertyHandler<RedisPersistentProperty>) persistentProperty -> {
PreferredConstructor<?, RedisPersistentProperty> constructor = entity.getPersistenceConstructor();
if (constructor != null && 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();
}
@Nullable
protected Object readProperty(String path, RedisData source, RedisPersistentProperty persistentProperty) {
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
TypeInformation<?> typeInformation = typeMapper.readType(source.getBucket().getPropertyPath(currentPath),
persistentProperty.getTypeInformation());
if (typeInformation.isMap()) {
Class<?> mapValueType = null;
if (typeInformation.getMapValueType() != null) {
mapValueType = typeInformation.getMapValueType().getType();
}
if (mapValueType == null && persistentProperty.isMap()) {
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()) {
if (!isByteArray(typeInformation)) {
return readCollectionOrArray(currentPath, typeInformation.getType(),
typeInformation.getRequiredComponentType().getType(), source.getBucket());
}
if (!source.getBucket().hasValue(currentPath) && isByteArray(typeInformation)) {
return readCollectionOrArray(currentPath, typeInformation.getType(),
typeInformation.getRequiredComponentType().getType(), source.getBucket());
}
}
if (mappingContext.getPersistentEntity(typeInformation) != null
&& !conversionService.canConvert(byte[].class, typeInformation.getRequiredActualType().getType())) {
Bucket bucket = source.getBucket().extract(currentPath + ".");
RedisData newBucket = new RedisData(bucket);
return readInternal(currentPath, typeInformation.getType(), newBucket);
}
byte[] sourceBytes = source.getBucket().get(currentPath);
if (typeInformation.getType().isPrimitive() && sourceBytes == null) {
return null;
}
if (persistentProperty.isIdProperty() && ObjectUtils.isEmpty(path.isEmpty())) {
return sourceBytes != null ? fromBytes(sourceBytes, typeInformation.getType()) : source.getId();
}
if (sourceBytes == null) {
return null;
}
if (customConversions.hasCustomReadTarget(byte[].class, persistentProperty.getType())) {
return fromBytes(sourceBytes, persistentProperty.getType());
}
Class<?> typeToUse = getTypeHint(currentPath, source.getBucket(), persistentProperty.getType());
return fromBytes(sourceBytes, typeToUse);
}
private void readAssociation(String path, RedisData source, RedisPersistentEntity<?> entity,
PersistentPropertyAccessor<?> accessor) {
entity.doWithAssociations((AssociationHandler<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(), bucket.size());
for (Entry<String, byte[]> entry : bucket.entrySet()) {
String referenceKey = fromBytes(entry.getValue(), String.class);
if (!KeyspaceIdentifier.isValid(referenceKey)) {
continue;
}
KeyspaceIdentifier identifier = KeyspaceIdentifier.of(referenceKey);
Map<byte[], byte[]> 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<byte[], byte[]> rawHash = referenceResolver.resolveReference(identifier.getId(),
identifier.getKeyspace());
if (!CollectionUtils.isEmpty(rawHash)) {
accessor.setProperty(association.getInverse(),
read(association.getInverse().getActualType(), new RedisData(rawHash)));
}
}
}
});
}
@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());
if (entity.getTypeInformation().isCollectionLike()) {
writeCollection(entity.getKeySpace(), "", (List) source, entity.getTypeInformation().getRequiredComponentType(),
sink);
} else {
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().getRequiredActualType());
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() && !isByteArray(targetProperty)) {
Collection<?> collection = pUpdate.getValue() instanceof Collection ? (Collection<?>) pUpdate.getValue()
: Collections.singleton(pUpdate.getValue());
writeCollection(entity.getKeySpace(), pUpdate.getPropertyPath(), collection,
targetProperty.getTypeInformation().getRequiredActualType(), sink);
} else if (targetProperty.isMap()) {
Map<Object, Object> 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<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);
}
}
@Nullable
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(@Nullable String keyspace, String path, @Nullable Object value,
TypeInformation<?> typeHint, RedisData sink) {
if (value == null) {
return;
}
if (customConversions.hasCustomWriteTarget(value.getClass())) {
Optional<Class<?>> targetType = customConversions.getCustomWriteTarget(value.getClass());
if (!StringUtils.hasText(path) && targetType.isPresent()
&& ClassUtils.isAssignable(byte[].class, targetType.get())) {
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 instanceof byte[]) {
sink.getBucket().put(StringUtils.hasText(path) ? path : "_raw", (byte[]) value);
return;
}
if (value.getClass() != typeHint.getType()) {
typeMapper.writeType(value.getClass(), sink.getBucket().getPropertyPath(path));
}
RedisPersistentEntity<?> entity = mappingContext.getRequiredPersistentEntity(value.getClass());
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(value);
entity.doWithProperties((PropertyHandler<RedisPersistentProperty>) 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() && !isByteArray(persistentProperty)) {
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 (propertyValue != null) {
if (customConversions.isSimpleType(ProxyUtils.getUserClass(propertyValue.getClass()))) {
writeToBucket(propertyStringPath, propertyValue, sink, persistentProperty.getType());
} else {
writeInternal(keyspace, propertyStringPath, propertyValue,
persistentProperty.getTypeInformation().getRequiredActualType(), sink);
}
}
});
writeAssociation(path, entity, value, sink);
}
private void writeAssociation(String path, RedisPersistentEntity<?> entity, @Nullable Object value, RedisData sink) {
if (value == null) {
return;
}
PersistentPropertyAccessor<Object> accessor = entity.getPropertyAccessor(value);
entity.doWithAssociations((AssociationHandler<RedisPersistentProperty>) association -> {
Object refObject = accessor.getProperty(association.getInverse());
if (refObject == null) {
return;
}
if (association.getInverse().isCollectionLike()) {
RedisPersistentEntity<?> ref = mappingContext.getRequiredPersistentEntity(
association.getInverse().getTypeInformation().getRequiredComponentType().getRequiredActualType());
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();
if (keyspace != null) {
Object refId = ref.getPropertyAccessor(refObject).getProperty(ref.getRequiredIdProperty());
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(@Nullable 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 + (path.equals("") ? "" : ".") + "[" + 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 (value instanceof byte[]) {
sink.getBucket().put(path, toBytes(value));
return;
}
if (customConversions.hasCustomWriteTarget(value.getClass())) {
Optional<Class<?>> targetType = customConversions.getCustomWriteTarget(value.getClass());
if (!propertyType.isPrimitive() && !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()));
}
}
}
@Nullable
private Object readCollectionOrArray(String path, Class<?> collectionType, Class<?> valueType, Bucket bucket) {
List<String> keys = new ArrayList<>(bucket.extractAllKeysFor(path));
keys.sort(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 (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(@Nullable 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<Object, Object> target = CollectionFactory.createMap(mapType, partial.size());
for (Entry<String, byte[]> 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<String> keys = source.getBucket().extractAllKeysFor(path);
Map<Object, Object> 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;
}
@Nullable
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> T fromBytes(byte[] source, Class<T> type) {
if (type.isInstance(source)) {
return type.cast(source);
}
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<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;
}
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();
}
@Override
public RedisMappingContext getMappingContext() {
return this.mappingContext;
}
@Nullable
@Override
public IndexResolver getIndexResolver() {
return this.indexResolver;
}
@Override
public ConversionService getConversionService() {
return this.conversionService;
}
@Override
public void afterPropertiesSet() {
this.initializeConverters();
}
private void initializeConverters() {
customConversions.registerConvertersIn(conversionService);
}
private static boolean isByteArray(RedisPersistentProperty property) {
return property.getType().equals(byte[].class);
}
private static boolean isByteArray(TypeInformation<?> type) {
return type.getType().equals(byte[].class);
}
/**
* @author Christoph Strobl
* @author Mark Paluch
*/
private class ConverterAwareParameterValueProvider implements PropertyValueProvider<RedisPersistentProperty> {
private final String path;
private final RedisData source;
private final ConversionService conversionService;
ConverterAwareParameterValueProvider(String path, RedisData source, ConversionService conversionService) {
this.path = path;
this.source = source;
this.conversionService = conversionService;
}
@Override
@SuppressWarnings("unchecked")
public <T> T getPropertyValue(RedisPersistentProperty property) {
Object value = readProperty(path, source, property);
if (value == null || ClassUtils.isAssignableValue(property.getType(), value)) {
return (T) value;
}
return (T) conversionService.convert(value, property.getType());
}
}
private enum NaturalOrderingKeyComparator implements Comparator<String> {
INSTANCE;
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 @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();
}
@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
* @author Stefan Berger
* @since 1.8.10
*/
public static class KeyspaceIdentifier {
public static final String PHANTOM = "phantom";
public static final String DELIMITER = ":";
public static final String PHANTOM_SUFFIX = DELIMITER + PHANTOM;
private final String keyspace;
private final String id;
private final boolean phantomKey;
private KeyspaceIdentifier(String keyspace, String id, boolean phantomKey) {
this.keyspace = keyspace;
this.id = id;
this.phantomKey = 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(DELIMITER);
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(@Nullable String key) {
if (key == null) {
return false;
}
int keyspaceEndIndex = key.indexOf(DELIMITER);
return keyspaceEndIndex > 0 && key.length() > keyspaceEndIndex;
}
public String getKeyspace() {
return this.keyspace;
}
public String getId() {
return this.id;
}
public boolean isPhantomKey() {
return this.phantomKey;
}
}
/**
* 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
* @author Stefan Berger
* @since 1.8.10
*/
public static class BinaryKeyspaceIdentifier {
public static final byte[] PHANTOM = KeyspaceIdentifier.PHANTOM.getBytes();
public static final byte DELIMITER = ':';
public static final byte[] PHANTOM_SUFFIX = ByteUtils.concat(new byte[] { DELIMITER }, PHANTOM);
private final byte[] keyspace;
private final byte[] id;
private final boolean phantomKey;
private BinaryKeyspaceIdentifier(byte[] keyspace, byte[] id, boolean phantomKey) {
this.keyspace = keyspace;
this.id = id;
this.phantomKey = 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, DELIMITER);
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.length == 0) {
return false;
}
int keyspaceEndIndex = ByteUtils.indexOf(key, DELIMITER);
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;
}
public byte[] getKeyspace() {
return this.keyspace;
}
public byte[] getId() {
return this.id;
}
public boolean isPhantomKey() {
return this.phantomKey;
}
}
}
相关信息
相关文章
spring-data-redis BinaryConverters 源码
spring-data-redis CompositeIndexResolver 源码
spring-data-redis DefaultRedisTypeMapper 源码
spring-data-redis GeoIndexedPropertyValue 源码
spring-data-redis IndexResolver 源码
spring-data-redis IndexedData 源码
spring-data-redis IndexedDataFactoryProvider 源码
0
赞
热门推荐
-
2、 - 优质文章
-
3、 gate.io
-
8、 golang
-
9、 openharmony
-
10、 Vue中input框自动聚焦