/*
* Copyright 2015-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.redis.core.convert;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import org.springframework.data.geo.Point;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PropertyHandler;
import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation;
import org.springframework.data.redis.core.index.ConfigurableIndexDefinitionProvider;
import org.springframework.data.redis.core.index.GeoIndexDefinition;
import org.springframework.data.redis.core.index.GeoIndexed;
import org.springframework.data.redis.core.index.IndexConfiguration;
import org.springframework.data.redis.core.index.IndexDefinition;
import org.springframework.data.redis.core.index.IndexDefinition.Condition;
import org.springframework.data.redis.core.index.IndexDefinition.IndexingContext;
import org.springframework.data.redis.core.index.Indexed;
import org.springframework.data.redis.core.index.SimpleIndexDefinition;
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;
/**
* {@link IndexResolver} implementation considering properties annotated with {@link Indexed} or paths set up in
* {@link IndexConfiguration}.
*
* @author Christoph Strobl
* @author Greg Turnquist
* @since 1.7
*/
public class PathIndexResolver implements IndexResolver {
private final Set<Class<?>> VALUE_TYPES = new HashSet<Class<?>>(
Arrays.<Class<?>> asList(Point.class, GeoLocation.class));
private ConfigurableIndexDefinitionProvider indexConfiguration;
private RedisMappingContext mappingContext;
private IndexedDataFactoryProvider indexedDataFactoryProvider;
/**
* Creates new {@link PathIndexResolver} with empty {@link IndexConfiguration}.
*/
public PathIndexResolver() {
this(new RedisMappingContext());
}
/**
* Creates new {@link PathIndexResolver} with given {@link IndexConfiguration}.
*
* @param mappingContext must not be {@literal null}.
*/
public PathIndexResolver(RedisMappingContext mappingContext) {
Assert.notNull(mappingContext, "MappingContext must not be null!");
this.mappingContext = mappingContext;
this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration();
this.indexedDataFactoryProvider = new IndexedDataFactoryProvider();
}
/*
* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(org.springframework.data.util.TypeInformation, java.lang.Object)
*/
public Set<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value) {
return doResolveIndexesFor(mappingContext.getPersistentEntity(typeInformation).get().getKeySpace(), "",
typeInformation, null, value);
}
/* (non-Javadoc)
* @see org.springframework.data.redis.core.convert.IndexResolver#resolveIndexesFor(java.lang.String, java.lang.String, org.springframework.data.util.TypeInformation, java.lang.Object)
*/
@Override
public Set<IndexedData> resolveIndexesFor(String keyspace, String path, TypeInformation<?> typeInformation,
Object value) {
return doResolveIndexesFor(keyspace, path, typeInformation, null, value);
}
private Set<IndexedData> doResolveIndexesFor(final String keyspace, final String path,
TypeInformation<?> typeInformation, PersistentProperty<?> fallback, Object value) {
Optional<RedisPersistentEntity<?>> entity = mappingContext.getPersistentEntity(typeInformation);
if (!entity.isPresent() || (value != null && VALUE_TYPES.contains(value.getClass()))) {
return resolveIndex(keyspace, path, fallback, value);
}
// this might happen on update where we address a property within an entity directly
if (!ClassUtils.isAssignable(entity.get().getType(), value.getClass())) {
String propertyName = path.lastIndexOf('.') > 0 ? path.substring(path.lastIndexOf('.') + 1, path.length()) : path;
return resolveIndex(keyspace, path, entity.get().getPersistentProperty(propertyName).get(), value);
}
final PersistentPropertyAccessor accessor = entity.get().getPropertyAccessor(value);
final Set<IndexedData> indexes = new LinkedHashSet<IndexedData>();
entity.get().doWithProperties(new PropertyHandler<RedisPersistentProperty>() {
@Override
public void doWithPersistentProperty(RedisPersistentProperty persistentProperty) {
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
Optional<Object> propertyValue = accessor.getProperty(persistentProperty);
if (propertyValue.isPresent()) {
TypeInformation<?> typeHint = persistentProperty.isMap()
? persistentProperty.getTypeInformation().getMapValueType().get()
: persistentProperty.getTypeInformation().getActualType();
if (persistentProperty.isMap()) {
for (Entry<?, ?> entry : ((Map<?, ?>) propertyValue.get()).entrySet()) {
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue());
indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(),
typeToUse.getActualType(), persistentProperty, entry.getValue()));
}
} else if (persistentProperty.isCollectionLike()) {
final Iterable<?> iterable;
if (Iterable.class.isAssignableFrom(propertyValue.get().getClass())) {
iterable = (Iterable<?>) propertyValue.get();
} else if (propertyValue.get().getClass().isArray()) {
iterable = CollectionUtils.arrayToList(propertyValue.get());
} else {
throw new RuntimeException(
"Don't know how to handle " + propertyValue.get().getClass() + " type of collection");
}
for (Object listValue : iterable) {
if (listValue != null) {
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, listValue);
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), persistentProperty,
listValue));
}
}
}
else if (persistentProperty.isEntity()
|| persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) {
typeHint = updateTypeHintForActualValue(typeHint, propertyValue.get());
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), persistentProperty,
propertyValue.get()));
} else {
indexes.addAll(resolveIndex(keyspace, currentPath, persistentProperty, propertyValue.get()));
}
}
}
private TypeInformation<?> updateTypeHintForActualValue(TypeInformation<?> typeHint, Object propertyValue) {
if (typeHint.equals(ClassTypeInformation.OBJECT) || typeHint.getClass().isInterface()) {
try {
typeHint = mappingContext.getPersistentEntity(propertyValue.getClass()).get().getTypeInformation();
} catch (Exception e) {
// ignore for cases where property value cannot be resolved as an entity, in that case the provided type
// hint has to be sufficient
}
}
return typeHint;
}
});
return indexes;
}
protected Set<IndexedData> resolveIndex(String keyspace, String propertyPath, PersistentProperty<?> property,
Object value) {
String path = normalizeIndexPath(propertyPath, property);
Set<IndexedData> data = new LinkedHashSet<IndexedData>();
if (indexConfiguration.hasIndexFor(keyspace, path)) {
IndexingContext context = new IndexingContext(keyspace, path,
property != null ? property.getTypeInformation() : ClassTypeInformation.OBJECT);
for (IndexDefinition indexDefinition : indexConfiguration.getIndexDefinitionsFor(keyspace, path)) {
if (!verifyConditions(indexDefinition.getConditions(), value, context)) {
continue;
}
Object transformedValue = indexDefinition.valueTransformer().convert(value);
IndexedData indexedData = null;
if (transformedValue == null) {
indexedData = new RemoveIndexedData(indexedData);
} else {
indexedData = indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value);
}
data.add(indexedData);
}
}
else if (property != null && property.isAnnotationPresent(Indexed.class)) {
SimpleIndexDefinition indexDefinition = new SimpleIndexDefinition(keyspace, path);
indexConfiguration.addIndexDefinition(indexDefinition);
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
} else if (property != null && property.isAnnotationPresent(GeoIndexed.class)) {
GeoIndexDefinition indexDefinition = new GeoIndexDefinition(keyspace, path);
indexConfiguration.addIndexDefinition(indexDefinition);
data.add(indexedDataFactoryProvider.getIndexedDataFactory(indexDefinition).createIndexedDataFor(value));
}
return data;
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private boolean verifyConditions(Iterable<Condition<?>> conditions, Object value, IndexingContext context) {
for (Condition condition : conditions) {
// TODO: generics lookup
if (!condition.matches(value, context)) {
return false;
}
}
return true;
}
private String normalizeIndexPath(String path, PersistentProperty<?> property) {
if (property == null) {
return path;
}
if (property.isMap()) {
return path.replaceAll("\\[", "").replaceAll("\\]", "");
}
if (property.isCollectionLike()) {
return path.replaceAll("\\[(\\p{Digit})*\\]", "").replaceAll("\\.\\.", ".");
}
return path;
}
}