DATAREDIS-425 - Add Support for basic CRUD and finder Operations backed by Hashes and Sets.
We now enable storing domain object as a flat Redis 'HASH' and maintain additional 'SET' structures to enable finder operations on simple properties.
@RedisHash("persons");
class Person {
@id String id;
@Indexed String firstname;
String lastname;
Map<String, String> attributes;
City city;
@Reference Person mother;
}
The above is stored in the HASH with key 'persons:1' as
_class = org.example.Person
id = 1
firstname = rand
lastname = al’thor
attributes.[eye-color] = grey
attributes.[hair-color] = red
city.name = emond's field
city.region = two rivers
mother = persons:2
Complex types are flattened out to their full property path for each of the values provided. If the properties actual value type does not match the declared one the '_class' type hint is added to the entry.
city._class = CityInAndor.class
city.name = emond's field
city.region = two rivers
city.country = andor
Map and Collection like structures are stored with their key/index values as part of the property path. If the map/collection value type does not match the actutal objects one the '_class' type hint is added to the entry.
list.[0]._class = DomainType.class
list.[0].property1 = ...
map.[key-1]._class = DomainType.class
map.[key-1].property1 = ...
Properties marked with '@Reference' are stored as semantic references by just storing the key to the referenced object 'HASH' instead of embedding its values.
mother = persons:2
Please note that referenced objects are not transitively updated/saved and that lazy loading of references will be part of future development.
A 'save' operation therefore executes the following:
# flatten domain type and add as hash
HMSET persons:1 id 1 firstname rand …
# add the newly inserted entry to the list of all entries of that type
SADD persons 1
# index the firstname for finder lookup
SADD persons.firstname:rand 1
Simple finder operation like 'findByFirstname' use 'SINTER' to find matching
SINTER persons.firstname:rand
HGETALL persons:1
Besides resolving an index via the '@Index' annotation we also allow to add custom configuration via the 'indexConfiguration' attribute of '@EnableRedisRepositories'.
@Configuration
@EnableRedisRepositories(indexConfiguration = CustomIndexConfiguration.class)
class Config { }
static class CustomIndexConfiguration extends IndexConfiguration {
@Override
protected Iterable<RedisIndexDefinition> initialConfiguration() {
return Arrays.asList(
new SimpleIndexDefinition("persons", "lastname"),
);
}
}
The '@TimeToLive' annotation allows to define a property or method providing an expiration time when storing the key in redis.
@RedisHash
class Person {
@Id String id;
@TimeToLive Long ttl;
}
Original Pull Request: #156
This commit is contained in:
@@ -150,7 +150,7 @@ abstract public class LettuceConverters extends Converters {
|
||||
public Map<byte[], byte[]> convert(final List<byte[]> source) {
|
||||
|
||||
if (CollectionUtils.isEmpty(source)) {
|
||||
Collections.emptyMap();
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
Map<byte[], byte[]> target = new LinkedHashMap<byte[], byte[]>();
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.convert.IndexedData;
|
||||
import org.springframework.data.redis.core.convert.RedisConverter;
|
||||
import org.springframework.data.redis.core.convert.SimpleIndexedPropertyValue;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
/**
|
||||
* {@link IndexWriter} takes care of writing <a href="http://redis.io/topics/indexes">secondary index</a> structures to
|
||||
* Redis. Depending on the type of {@link IndexedData} it uses eg. Sets with specific names to add actually referenced
|
||||
* keys to. While doing so {@link IndexWriter} also keeps track of all indexes associated with the root types key, which
|
||||
* allows to remove the root key from all indexes in case of deletion.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
class IndexWriter {
|
||||
|
||||
private final RedisConnection connection;
|
||||
private final RedisConverter converter;
|
||||
|
||||
/**
|
||||
* Creates new {@link IndexWriter}.
|
||||
*
|
||||
* @param keyspace The key space to write index values to. Must not be {@literal null}.
|
||||
* @param connection must not be {@literal null}.
|
||||
* @param converter must not be {@literal null}.
|
||||
*/
|
||||
public IndexWriter(RedisConnection connection, RedisConverter converter) {
|
||||
|
||||
Assert.notNull(connection, "RedisConnection cannot be null!");
|
||||
Assert.notNull(converter, "RedisConverter cannot be null!");
|
||||
|
||||
this.connection = connection;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates indexes by first removing key from existing one and then persisting new index data.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @param indexValues can be {@literal null}.
|
||||
*/
|
||||
public void updateIndexes(Object key, Iterable<IndexedData> indexValues) {
|
||||
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
if (indexValues == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] binKey = toBytes(key);
|
||||
|
||||
removeKeyFromExistingIndexes(binKey, indexValues);
|
||||
addKeyToIndexes(binKey, indexValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a key from all available indexes.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
*/
|
||||
public void removeKeyFromIndexes(String keyspace, Object key) {
|
||||
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
|
||||
byte[] binKey = toBytes(key);
|
||||
byte[] indexHelperKey = ByteUtils.concatAll(toBytes(keyspace + ":"), binKey, toBytes(":idx"));
|
||||
|
||||
for (byte[] indexKey : connection.sMembers(indexHelperKey)) {
|
||||
connection.sRem(indexKey, binKey);
|
||||
}
|
||||
|
||||
connection.del(indexHelperKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all indexes.
|
||||
*/
|
||||
public void removeAllIndexes(String keyspace) {
|
||||
|
||||
Set<byte[]> potentialIndex = connection.keys(toBytes(keyspace + ":*"));
|
||||
|
||||
if (!potentialIndex.isEmpty()) {
|
||||
connection.del(potentialIndex.toArray(new byte[potentialIndex.size()][]));
|
||||
}
|
||||
}
|
||||
|
||||
private void removeKeyFromExistingIndexes(byte[] key, Iterable<IndexedData> indexValues) {
|
||||
|
||||
for (IndexedData indexData : indexValues) {
|
||||
removeKeyFromExistingIndexes(key, indexData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove given key from all indexes matching {@link IndexedData#getPath()}:
|
||||
*
|
||||
* @param key
|
||||
* @param indexedData
|
||||
*/
|
||||
protected void removeKeyFromExistingIndexes(byte[] key, IndexedData indexedData) {
|
||||
|
||||
Assert.notNull(indexedData, "IndexedData must not be null!");
|
||||
Set<byte[]> existingKeys = connection.keys(toBytes(indexedData.getKeySpace() + ":" + indexedData.getPath() + ":*"));
|
||||
|
||||
if (!CollectionUtils.isEmpty(existingKeys)) {
|
||||
for (byte[] existingKey : existingKeys) {
|
||||
connection.sRem(existingKey, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addKeyToIndexes(byte[] key, Iterable<IndexedData> indexValues) {
|
||||
|
||||
for (IndexedData indexData : indexValues) {
|
||||
addKeyToIndex(key, indexData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a given key to the index for {@link IndexedData#getPath()}.
|
||||
*
|
||||
* @param key must not be {@literal null}.
|
||||
* @param indexedData must not be {@literal null}.
|
||||
*/
|
||||
protected void addKeyToIndex(byte[] key, IndexedData indexedData) {
|
||||
|
||||
Assert.notNull(key, "Key must not be null!");
|
||||
Assert.notNull(indexedData, "IndexedData must not be null!");
|
||||
|
||||
if (indexedData instanceof SimpleIndexedPropertyValue) {
|
||||
|
||||
Object value = ((SimpleIndexedPropertyValue) indexedData).getValue();
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] indexKey = toBytes(indexedData.getKeySpace() + ":" + indexedData.getPath() + ":");
|
||||
indexKey = ByteUtils.concat(indexKey, toBytes(value));
|
||||
connection.sAdd(indexKey, key);
|
||||
|
||||
// keep track of indexes used for the object
|
||||
connection.sAdd(ByteUtils.concatAll(toBytes(indexedData.getKeySpace() + ":"), key, toBytes(":idx")), indexKey);
|
||||
} else {
|
||||
throw new IllegalArgumentException(String.format("Cannot write index data for unknown index type %s",
|
||||
indexedData.getClass()));
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] toBytes(Object source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
if (source instanceof byte[]) {
|
||||
return (byte[]) source;
|
||||
}
|
||||
|
||||
return converter.getConversionService().convert(source, byte[].class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.data.annotation.Persistent;
|
||||
import org.springframework.data.keyvalue.annotation.KeySpace;
|
||||
|
||||
/**
|
||||
* {@link RedisHash} marks Objects as aggregate roots to be stored in a Redis hash.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@Persistent
|
||||
@Documented
|
||||
@Inherited
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(value = { ElementType.TYPE })
|
||||
@KeySpace
|
||||
public @interface RedisHash {
|
||||
|
||||
/**
|
||||
* The prefix to distinguish between domain types.
|
||||
*
|
||||
* @return
|
||||
* @see KeySpace
|
||||
*/
|
||||
@AliasFor(annotation = KeySpace.class, attribute = "value")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* Time before expire in seconds. Superseded by {@link TimeToLive}.
|
||||
*
|
||||
* @return positive number when expiration should be applied.
|
||||
*/
|
||||
long timeToLive() default -1L;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
|
||||
/**
|
||||
* {@link RedisKeyExpiredEvent} is Redis specific {@link ApplicationEvent} published when a specific key in Redis
|
||||
* expires. It might but must not hold the expired value itself next to the key.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisKeyExpiredEvent<T> extends RedisKeyspaceEvent {
|
||||
|
||||
private final byte[][] args;
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyExpiredEvent}.
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
public RedisKeyExpiredEvent(byte[] key) {
|
||||
this(key, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyExpiredEvent}
|
||||
*
|
||||
* @param key
|
||||
* @param value
|
||||
*/
|
||||
public RedisKeyExpiredEvent(byte[] key, Object value) {
|
||||
super(key);
|
||||
|
||||
args = ByteUtils.split(key, ':');
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the keyspace in which the expiration occured.
|
||||
*
|
||||
* @return {@literal null} if it could not be determined.
|
||||
*/
|
||||
public String getKeyspace() {
|
||||
|
||||
if (args.length >= 2) {
|
||||
return new String(args[0], Charset.forName("UTF-8"));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expired objects id;
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public byte[] getId() {
|
||||
return args.length == 2 ? args[1] : args[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the expired Object
|
||||
*
|
||||
* @return {@literal null} if not present.
|
||||
*/
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.EventObject#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RedisKeyExpiredEvent [keyspace=" + getKeyspace() + ", id=" + getId() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,608 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.convert.ConverterNotFoundException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.keyvalue.core.AbstractKeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.KeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.convert.CustomConversions;
|
||||
import org.springframework.data.redis.core.convert.IndexResolverImpl;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.convert.MappingRedisConverter;
|
||||
import org.springframework.data.redis.core.convert.RedisConverter;
|
||||
import org.springframework.data.redis.core.convert.RedisData;
|
||||
import org.springframework.data.redis.core.convert.ReferenceResolver;
|
||||
import org.springframework.data.redis.core.mapping.RedisMappingContext;
|
||||
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
import org.springframework.data.util.CloseableIterator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Redis specific {@link KeyValueAdapter} implementation. Uses binary codec to read/write data from/to Redis. Objects
|
||||
* are stored in a Redis Hash using the value of {@link RedisHash}, the {@link KeyspaceConfiguration} or just
|
||||
* {@link Class#getName()} as a prefix. <br />
|
||||
* <strong>Example</strong>
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* @RedisHash("persons")
|
||||
* class Person {
|
||||
* @Id String id;
|
||||
* String name;
|
||||
* }
|
||||
*
|
||||
*
|
||||
* prefix ID
|
||||
* | |
|
||||
* V V
|
||||
* hgetall persons:5d67b7e1-8640-4475-beeb-c666fab4c0e5
|
||||
* 1) id
|
||||
* 2) 5d67b7e1-8640-4475-beeb-c666fab4c0e5
|
||||
* 3) name
|
||||
* 4) Rand al'Thor
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* <br />
|
||||
* The {@link KeyValueAdapter} is <strong>not</strong> intended to store simple types such as {@link String} values.
|
||||
* Please use {@link RedisTemplate} for this purpose.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisKeyValueAdapter extends AbstractKeyValueAdapter implements ApplicationContextAware,
|
||||
ApplicationListener<RedisKeyspaceEvent> {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(RedisKeyValueAdapter.class);
|
||||
|
||||
private RedisOperations<?, ?> redisOps;
|
||||
private RedisConverter converter;
|
||||
private RedisMessageListenerContainer messageListenerContainer;
|
||||
private KeyExpirationEventMessageListener expirationListener;
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyValueAdapter} with default {@link RedisMappingContext} and default
|
||||
* {@link CustomConversions}.
|
||||
*
|
||||
* @param redisOps must not be {@literal null}.
|
||||
*/
|
||||
public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps) {
|
||||
this(redisOps, new RedisMappingContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyValueAdapter} with default {@link CustomConversions}.
|
||||
*
|
||||
* @param redisOps must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext mappingContext) {
|
||||
this(redisOps, mappingContext, new CustomConversions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyValueAdapter}.
|
||||
*
|
||||
* @param redisOps must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
* @param customConversions can be {@literal null}.
|
||||
*/
|
||||
public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisMappingContext mappingContext,
|
||||
CustomConversions customConversions) {
|
||||
|
||||
super(new RedisQueryEngine());
|
||||
|
||||
Assert.notNull(redisOps, "RedisOperations must not be null!");
|
||||
Assert.notNull(mappingContext, "RedisMappingContext must not be null!");
|
||||
|
||||
MappingRedisConverter mappingConverter = new MappingRedisConverter(mappingContext, new IndexResolverImpl(
|
||||
mappingContext), new ReferenceResolverImpl(this));
|
||||
mappingConverter.setCustomConversions(customConversions == null ? new CustomConversions() : customConversions);
|
||||
mappingConverter.afterPropertiesSet();
|
||||
|
||||
converter = mappingConverter;
|
||||
this.redisOps = redisOps;
|
||||
|
||||
initKeyExpirationListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyValueAdapter} with specific {@link RedisConverter}.
|
||||
*
|
||||
* @param redisOps must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public RedisKeyValueAdapter(RedisOperations<?, ?> redisOps, RedisConverter redisConverter) {
|
||||
|
||||
super(new RedisQueryEngine());
|
||||
|
||||
Assert.notNull(redisOps, "RedisOperations must not be null!");
|
||||
|
||||
converter = redisConverter;
|
||||
this.redisOps = redisOps;
|
||||
|
||||
initKeyExpirationListener();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#put(java.io.Serializable, java.lang.Object, java.io.Serializable)
|
||||
*/
|
||||
public Object put(final Serializable id, final Object item, final Serializable keyspace) {
|
||||
|
||||
final RedisData rdo = item instanceof RedisData ? (RedisData) item : new RedisData();
|
||||
if (!(item instanceof RedisData)) {
|
||||
converter.write(item, rdo);
|
||||
}
|
||||
|
||||
if (rdo.getId() == null) {
|
||||
|
||||
rdo.setId(id);
|
||||
|
||||
if (!(item instanceof RedisData)) {
|
||||
KeyValuePersistentProperty idProperty = converter.getMappingContext().getPersistentEntity(item.getClass())
|
||||
.getIdProperty();
|
||||
converter.getMappingContext().getPersistentEntity(item.getClass()).getPropertyAccessor(item)
|
||||
.setProperty(idProperty, id);
|
||||
}
|
||||
}
|
||||
|
||||
redisOps.execute(new RedisCallback<Object>() {
|
||||
|
||||
@Override
|
||||
public Object doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
byte[] key = toBytes(rdo.getId());
|
||||
byte[] objectKey = createKey(rdo.getKeyspace(), rdo.getId());
|
||||
|
||||
connection.del(objectKey);
|
||||
|
||||
connection.hMSet(objectKey, rdo.getBucket().rawMap());
|
||||
|
||||
if (rdo.getTimeToLive() != null && rdo.getTimeToLive().longValue() > 0) {
|
||||
|
||||
connection.expire(objectKey, rdo.getTimeToLive().longValue());
|
||||
|
||||
// add phantom key so values can be restored
|
||||
byte[] phantomKey = ByteUtils.concat(objectKey, toBytes(":phantom"));
|
||||
connection.del(phantomKey);
|
||||
connection.hMSet(phantomKey, rdo.getBucket().rawMap());
|
||||
connection.expire(phantomKey, rdo.getTimeToLive().longValue() + 300);
|
||||
}
|
||||
|
||||
connection.sAdd(toBytes(rdo.getKeyspace()), key);
|
||||
|
||||
new IndexWriter(connection, converter).updateIndexes(key, rdo.getIndexedData());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#contains(java.io.Serializable, java.io.Serializable)
|
||||
*/
|
||||
public boolean contains(final Serializable id, final Serializable keyspace) {
|
||||
|
||||
Boolean exists = redisOps.execute(new RedisCallback<Boolean>() {
|
||||
|
||||
@Override
|
||||
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
return connection.sIsMember(toBytes(keyspace), toBytes(id));
|
||||
}
|
||||
});
|
||||
|
||||
return exists != null ? exists.booleanValue() : false;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#get(java.io.Serializable, java.io.Serializable)
|
||||
*/
|
||||
public Object get(Serializable id, Serializable keyspace) {
|
||||
return get(id, keyspace, Object.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @param keyspace
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
public <T> T get(Serializable id, Serializable keyspace, Class<T> type) {
|
||||
|
||||
final byte[] binId = createKey(keyspace, id);
|
||||
|
||||
Map<byte[], byte[]> raw = redisOps.execute(new RedisCallback<Map<byte[], byte[]>>() {
|
||||
|
||||
@Override
|
||||
public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
return connection.hGetAll(binId);
|
||||
}
|
||||
});
|
||||
|
||||
RedisData data = new RedisData(raw);
|
||||
data.setId(id);
|
||||
data.setKeyspace(keyspace.toString());
|
||||
|
||||
return converter.read(type, data);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#delete(java.io.Serializable, java.io.Serializable)
|
||||
*/
|
||||
public Object delete(final Serializable id, final Serializable keyspace) {
|
||||
return delete(id, keyspace, Object.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.AbstractKeyValueAdapter#delete(java.io.Serializable, java.io.Serializable, java.lang.Class)
|
||||
*/
|
||||
public <T> T delete(final Serializable id, final Serializable keyspace, final Class<T> type) {
|
||||
|
||||
final byte[] binId = toBytes(id);
|
||||
final byte[] binKeyspace = toBytes(keyspace);
|
||||
|
||||
T o = get(id, keyspace, type);
|
||||
|
||||
if (o != null) {
|
||||
|
||||
redisOps.execute(new RedisCallback<Void>() {
|
||||
|
||||
@Override
|
||||
public Void doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
connection.del(createKey(keyspace, id));
|
||||
connection.sRem(binKeyspace, binId);
|
||||
|
||||
new IndexWriter(connection, converter).removeKeyFromIndexes(keyspace.toString(), binId);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#getAllOf(java.io.Serializable)
|
||||
*/
|
||||
public List<?> getAllOf(final Serializable keyspace) {
|
||||
|
||||
final byte[] binKeyspace = toBytes(keyspace);
|
||||
|
||||
List<Map<byte[], byte[]>> raw = redisOps.execute(new RedisCallback<List<Map<byte[], byte[]>>>() {
|
||||
|
||||
@Override
|
||||
public List<Map<byte[], byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
final List<Map<byte[], byte[]>> rawData = new ArrayList<Map<byte[], byte[]>>();
|
||||
|
||||
Set<byte[]> members = connection.sMembers(binKeyspace);
|
||||
|
||||
for (byte[] id : members) {
|
||||
rawData.add(connection.hGetAll(createKey(binKeyspace, id)));
|
||||
}
|
||||
|
||||
return rawData;
|
||||
}
|
||||
});
|
||||
|
||||
List<Object> result = new ArrayList<Object>(raw.size());
|
||||
for (Map<byte[], byte[]> rawData : raw) {
|
||||
result.add(converter.read(Object.class, new RedisData(rawData)));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#deleteAllOf(java.io.Serializable)
|
||||
*/
|
||||
public void deleteAllOf(final Serializable keyspace) {
|
||||
|
||||
redisOps.execute(new RedisCallback<Void>() {
|
||||
|
||||
@Override
|
||||
public Void doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
connection.del(toBytes(keyspace));
|
||||
new IndexWriter(connection, converter).removeAllIndexes(keyspace.toString());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#entries(java.io.Serializable)
|
||||
*/
|
||||
public CloseableIterator<Entry<Serializable, Object>> entries(Serializable keyspace) {
|
||||
throw new UnsupportedOperationException("Not yet implemented");
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueAdapter#count(java.io.Serializable)
|
||||
*/
|
||||
public long count(final Serializable keyspace) {
|
||||
|
||||
Long count = redisOps.execute(new RedisCallback<Long>() {
|
||||
|
||||
@Override
|
||||
public Long doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
return connection.sCard(toBytes(keyspace));
|
||||
}
|
||||
});
|
||||
|
||||
return count != null ? count.longValue() : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute {@link RedisCallback} via underlying {@link RedisOperations}.
|
||||
*
|
||||
* @param callback must not be {@literal null}.
|
||||
* @see RedisOperations#execute(RedisCallback)
|
||||
* @return
|
||||
*/
|
||||
public <T> T execute(RedisCallback<T> callback) {
|
||||
return redisOps.execute(callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link RedisConverter} in use.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public RedisConverter getConverter() {
|
||||
return this.converter;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
public byte[] createKey(Serializable keyspace, Serializable id) {
|
||||
return toBytes(keyspace + ":" + id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 converter.getConversionService().convert(source, byte[].class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.DisposableBean#destroy()
|
||||
*/
|
||||
public void destroy() throws Exception {
|
||||
|
||||
if (redisOps instanceof RedisTemplate) {
|
||||
RedisConnectionFactory connectionFactory = ((RedisTemplate<?, ?>) redisOps).getConnectionFactory();
|
||||
if (connectionFactory instanceof DisposableBean) {
|
||||
((DisposableBean) connectionFactory).destroy();
|
||||
}
|
||||
}
|
||||
|
||||
this.expirationListener.destroy();
|
||||
this.messageListenerContainer.destroy();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
|
||||
*/
|
||||
@Override
|
||||
public void onApplicationEvent(RedisKeyspaceEvent event) {
|
||||
|
||||
LOGGER.debug("Received %s .", event);
|
||||
|
||||
if (event instanceof RedisKeyExpiredEvent) {
|
||||
|
||||
final RedisKeyExpiredEvent expiredEvent = (RedisKeyExpiredEvent) event;
|
||||
|
||||
redisOps.execute(new RedisCallback<Void>() {
|
||||
|
||||
@Override
|
||||
public Void doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
LOGGER.debug("Cleaning up expired key '%s' data structures in keyspace '%s'.", expiredEvent.getSource(),
|
||||
expiredEvent.getKeyspace());
|
||||
|
||||
connection.sRem(toBytes(expiredEvent.getKeyspace()), expiredEvent.getId());
|
||||
new IndexWriter(connection, converter).removeKeyFromIndexes(expiredEvent.getKeyspace(), expiredEvent.getId());
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
this.expirationListener.setApplicationEventPublisher(applicationContext);
|
||||
}
|
||||
|
||||
private void initKeyExpirationListener() {
|
||||
|
||||
this.messageListenerContainer = new RedisMessageListenerContainer();
|
||||
messageListenerContainer.setConnectionFactory(((RedisTemplate<?, ?>) redisOps).getConnectionFactory());
|
||||
messageListenerContainer.afterPropertiesSet();
|
||||
messageListenerContainer.start();
|
||||
|
||||
this.expirationListener = new MappingExpirationListener(this.messageListenerContainer, this.redisOps,
|
||||
this.converter);
|
||||
this.expirationListener.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link MessageListener} implementation used to capture Redis keypspace notifications. Tries to read a previously
|
||||
* created phantom key {@code keyspace:id:phantom} to provide the expired object as part of the published
|
||||
* {@link RedisKeyExpiredEvent}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
static class MappingExpirationListener extends KeyExpirationEventMessageListener {
|
||||
|
||||
private final RedisOperations<?, ?> ops;
|
||||
private final RedisConverter converter;
|
||||
|
||||
/**
|
||||
* Creates new {@link MappingExpirationListener}.
|
||||
*
|
||||
* @param listenerContainer
|
||||
* @param ops
|
||||
* @param converter
|
||||
*/
|
||||
public MappingExpirationListener(RedisMessageListenerContainer listenerContainer, RedisOperations<?, ?> ops,
|
||||
RedisConverter converter) {
|
||||
|
||||
super(listenerContainer);
|
||||
this.ops = ops;
|
||||
this.converter = converter;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[])
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
|
||||
if (!isKeyExpirationMessage(message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
byte[] key = message.getBody();
|
||||
|
||||
final byte[] phantomKey = ByteUtils.concat(key, converter.getConversionService()
|
||||
.convert(":phantom", byte[].class));
|
||||
|
||||
Map<byte[], byte[]> hash = ops.execute(new RedisCallback<Map<byte[], byte[]>>() {
|
||||
|
||||
@Override
|
||||
public Map<byte[], byte[]> doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
Map<byte[], byte[]> hash = connection.hGetAll(phantomKey);
|
||||
|
||||
if (!org.springframework.util.CollectionUtils.isEmpty(hash)) {
|
||||
connection.del(phantomKey);
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
});
|
||||
|
||||
Object value = converter.read(Object.class, new RedisData(hash));
|
||||
publishEvent(new RedisKeyExpiredEvent(key, value));
|
||||
}
|
||||
|
||||
private boolean isKeyExpirationMessage(Message message) {
|
||||
|
||||
if (message == null || message.getChannel() == null || message.getBody() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
byte[][] args = ByteUtils.split(message.getBody(), ':');
|
||||
if (args.length != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link ReferenceResolver} using {@link RedisKeyValueAdapter} to read and convert referenced entities.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
static class ReferenceResolverImpl implements ReferenceResolver {
|
||||
|
||||
private RedisKeyValueAdapter adapter;
|
||||
|
||||
ReferenceResolverImpl() {}
|
||||
|
||||
/**
|
||||
* @param adapter must not be {@literal null}.
|
||||
*/
|
||||
public ReferenceResolverImpl(RedisKeyValueAdapter adapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param adapter
|
||||
*/
|
||||
public void setAdapter(RedisKeyValueAdapter adapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.convert.ReferenceResolver#resolveReference(java.io.Serializable, java.io.Serializable, java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public <T> T resolveReference(Serializable id, Serializable keyspace, Class<T> type) {
|
||||
return (T) adapter.get(id, keyspace, type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.keyvalue.core.KeyValueAdapter;
|
||||
import org.springframework.data.keyvalue.core.KeyValueCallback;
|
||||
import org.springframework.data.keyvalue.core.KeyValueTemplate;
|
||||
import org.springframework.data.redis.core.mapping.RedisMappingContext;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Redis specific implementation of {@link KeyValueTemplate}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisKeyValueTemplate extends KeyValueTemplate {
|
||||
|
||||
/**
|
||||
* Create new {@link RedisKeyValueTemplate}.
|
||||
*
|
||||
* @param adapter must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
public RedisKeyValueTemplate(RedisKeyValueAdapter adapter, RedisMappingContext mappingContext) {
|
||||
super(adapter, mappingContext);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueTemplate#getMappingContext()
|
||||
*/
|
||||
@Override
|
||||
public RedisMappingContext getMappingContext() {
|
||||
return (RedisMappingContext) super.getMappingContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve entities by resolving their {@literal id}s and converting them into required type. <br />
|
||||
* The callback provides either a single {@literal id} or an {@link Iterable} of {@literal id}s, used for retrieving
|
||||
* the actual domain types and shortcuts manual retrieval and conversion of {@literal id}s via {@link RedisTemplate}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* List<RedisSession> sessions = template.find(new RedisCallback<Set<byte[]>>() {
|
||||
* public Set<byte[]< doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
* return connection
|
||||
* .sMembers("spring:session:sessions:securityContext.authentication.principal.username:user"
|
||||
* .getBytes());
|
||||
* }
|
||||
* }, RedisSession.class);
|
||||
* </code>
|
||||
*
|
||||
* <pre>
|
||||
* @param callback provides the to retrieve entity ids. Must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return empty list if not elements found.
|
||||
*/
|
||||
public <T> List<T> find(final RedisCallback<?> callback, final Class<T> type) {
|
||||
|
||||
Assert.notNull(callback, "Callback must not be null.");
|
||||
|
||||
return execute(new RedisKeyValueCallback<List<T>>() {
|
||||
|
||||
@Override
|
||||
public List<T> doInRedis(RedisKeyValueAdapter adapter) {
|
||||
|
||||
Object callbackResult = adapter.execute(callback);
|
||||
|
||||
if (callbackResult == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Iterable<?> ids = ClassUtils.isAssignable(Iterable.class, callbackResult.getClass()) ? (Iterable<?>) callbackResult
|
||||
: Collections.singleton(callbackResult);
|
||||
|
||||
List<T> result = new ArrayList<T>();
|
||||
for (Object id : ids) {
|
||||
|
||||
String idToUse = adapter.getConverter().getConversionService().canConvert(id.getClass(), String.class) ? adapter
|
||||
.getConverter().getConversionService().convert(id, String.class)
|
||||
: id.toString();
|
||||
|
||||
T candidate = findById(idToUse, type);
|
||||
if (candidate != null) {
|
||||
result.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Redis specific {@link KeyValueCallback}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @param <T>
|
||||
* @since 1.7
|
||||
*/
|
||||
public static abstract class RedisKeyValueCallback<T> implements KeyValueCallback<T> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.KeyValueCallback#doInKeyValue(org.springframework.data.keyvalue.core.KeyValueAdapter)
|
||||
*/
|
||||
@Override
|
||||
public T doInKeyValue(KeyValueAdapter adapter) {
|
||||
return doInRedis((RedisKeyValueAdapter) adapter);
|
||||
}
|
||||
|
||||
public abstract T doInRedis(RedisKeyValueAdapter adapter);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
|
||||
/**
|
||||
* Redis specific {@link ApplicationEvent} published when a key expires in Redis.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisKeyspaceEvent extends ApplicationEvent {
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisKeyspaceEvent}.
|
||||
*
|
||||
* @param key The key that expired. Must not be {@literal null}.
|
||||
*/
|
||||
public RedisKeyspaceEvent(byte[] key) {
|
||||
super(key);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.util.EventObject#getSource()
|
||||
*/
|
||||
public byte[] getSource() {
|
||||
return (byte[]) super.getSource();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.data.keyvalue.core.CriteriaAccessor;
|
||||
import org.springframework.data.keyvalue.core.QueryEngine;
|
||||
import org.springframework.data.keyvalue.core.SortAccessor;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.core.convert.RedisData;
|
||||
import org.springframework.data.redis.repository.query.RedisOperationChain;
|
||||
import org.springframework.data.redis.util.ByteUtils;
|
||||
|
||||
/**
|
||||
* Redis specific {@link QueryEngine} implementation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
class RedisQueryEngine extends QueryEngine<RedisKeyValueAdapter, RedisOperationChain, Comparator<?>> {
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisQueryEngine} with defaults.
|
||||
*/
|
||||
public RedisQueryEngine() {
|
||||
this(new RedisCriteriaAccessor(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisQueryEngine}.
|
||||
*
|
||||
* @param criteriaAccessor
|
||||
* @param sortAccessor
|
||||
* @see QueryEngine#QueryEngine(CriteriaAccessor, SortAccessor)
|
||||
*/
|
||||
public RedisQueryEngine(CriteriaAccessor<RedisOperationChain> criteriaAccessor,
|
||||
SortAccessor<Comparator<?>> sortAccessor) {
|
||||
super(criteriaAccessor, sortAccessor);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable, java.lang.Class)
|
||||
*/
|
||||
public <T> Collection<T> execute(final RedisOperationChain criteria, final Comparator<?> sort, int offset, int rows,
|
||||
final Serializable keyspace, Class<T> type) {
|
||||
|
||||
RedisCallback<Map<byte[], Map<byte[], byte[]>>> callback = new RedisCallback<Map<byte[], Map<byte[], byte[]>>>() {
|
||||
|
||||
@Override
|
||||
public Map<byte[], Map<byte[], byte[]>> doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
String key = keyspace + ":";
|
||||
byte[][] keys = new byte[criteria.getSismember().size()][];
|
||||
int i = 0;
|
||||
for (Object o : criteria.getSismember()) {
|
||||
keys[i] = getAdapter().getConverter().getConversionService().convert(key + o, byte[].class);
|
||||
i++;
|
||||
}
|
||||
|
||||
Set<byte[]> allKeys = connection.sInter(keys);
|
||||
|
||||
byte[] keyspaceBin = getAdapter().getConverter().getConversionService().convert(keyspace + ":", byte[].class);
|
||||
|
||||
final Map<byte[], Map<byte[], byte[]>> rawData = new LinkedHashMap<byte[], Map<byte[], byte[]>>();
|
||||
|
||||
for (byte[] id : allKeys) {
|
||||
|
||||
byte[] singleKey = ByteUtils.concat(keyspaceBin, id);
|
||||
rawData.put(id, connection.hGetAll(singleKey));
|
||||
}
|
||||
|
||||
return rawData;
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
Map<byte[], Map<byte[], byte[]>> raw = this.getAdapter().execute(callback);
|
||||
|
||||
List<T> result = new ArrayList<T>(raw.size());
|
||||
for (Map.Entry<byte[], Map<byte[], byte[]>> entry : raw.entrySet()) {
|
||||
|
||||
RedisData data = new RedisData(entry.getValue());
|
||||
data.setId(getAdapter().getConverter().getConversionService().convert(entry.getKey(), String.class));
|
||||
data.setKeyspace(keyspace.toString());
|
||||
|
||||
T converted = this.getAdapter().getConverter().read(type, data);
|
||||
|
||||
if (converted != null) {
|
||||
result.add(converted);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.QueryEngine#execute(java.lang.Object, java.lang.Object, int, int, java.io.Serializable)
|
||||
*/
|
||||
@Override
|
||||
public Collection<?> execute(final RedisOperationChain criteria, Comparator<?> sort, int offset, int rows,
|
||||
final Serializable keyspace) {
|
||||
return execute(criteria, sort, offset, rows, keyspace, Object.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.QueryEngine#count(java.lang.Object, java.io.Serializable)
|
||||
*/
|
||||
@Override
|
||||
public long count(final RedisOperationChain criteria, final Serializable keyspace) {
|
||||
|
||||
return this.getAdapter().execute(new RedisCallback<Long>() {
|
||||
|
||||
@Override
|
||||
public Long doInRedis(RedisConnection connection) throws DataAccessException {
|
||||
|
||||
String key = keyspace + ":";
|
||||
byte[][] keys = new byte[criteria.getSismember().size()][];
|
||||
int i = 0;
|
||||
for (Object o : criteria.getSismember()) {
|
||||
keys[i] = getAdapter().getConverter().getConversionService().convert(key + o, byte[].class);
|
||||
}
|
||||
|
||||
return (long) connection.sInter(keys).size();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
static class RedisCriteriaAccessor implements CriteriaAccessor<RedisOperationChain> {
|
||||
|
||||
@Override
|
||||
public RedisOperationChain resolve(KeyValueQuery<?> query) {
|
||||
return (RedisOperationChain) query.getCritieria();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.data.annotation.ReadOnlyProperty;
|
||||
|
||||
/**
|
||||
* {@link TimeToLive} marks a single numeric property on aggregate root to be used for setting expirations in Redis. The
|
||||
* annotated property supersedes any other timeout configuration.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
* @RedisHash
|
||||
* class Person {
|
||||
* @Id String id;
|
||||
* String name;
|
||||
* @TimeToLive Long timeout;
|
||||
* }
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Target(value = { ElementType.FIELD, ElementType.METHOD })
|
||||
@ReadOnlyProperty
|
||||
public @interface TimeToLive {
|
||||
|
||||
/**
|
||||
* {@link TimeUnit} unit to use.
|
||||
*
|
||||
* @return {@link TimeUnit#SECONDS} by default.
|
||||
*/
|
||||
TimeUnit unit() default TimeUnit.SECONDS;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
/**
|
||||
* {@link TimeToLiveAccessor} extracts the objects time to live used for {@code EXPIRE}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface TimeToLiveAccessor {
|
||||
|
||||
/**
|
||||
* @param source must not be {@literal null}.
|
||||
* @return {@literal null} if not configured.
|
||||
*/
|
||||
Long getTimeToLive(Object source);
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* Copyright 2015 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.nio.charset.Charset;
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.util.NumberUtils;
|
||||
|
||||
/**
|
||||
* Set of {@link ReadingConverter} and {@link WritingConverter} used to convert Objects into binary format.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
final class BinaryConverters {
|
||||
|
||||
/**
|
||||
* Use {@literal UTF-8} as default charset.
|
||||
*/
|
||||
public static final Charset CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
private BinaryConverters() {}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
private static class StringBasedConverter {
|
||||
|
||||
byte[] fromString(String source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
return source.getBytes(CHARSET);
|
||||
}
|
||||
|
||||
String toString(byte[] source) {
|
||||
return new String(source, CHARSET);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@WritingConverter
|
||||
static class StringToBytesConverter extends StringBasedConverter implements Converter<String, byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] convert(String source) {
|
||||
return fromString(source);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@ReadingConverter
|
||||
static class BytesToStringConverter extends StringBasedConverter implements Converter<byte[], String> {
|
||||
|
||||
@Override
|
||||
public String convert(byte[] source) {
|
||||
return toString(source);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@WritingConverter
|
||||
static class NumberToBytesConverter extends StringBasedConverter implements Converter<Number, byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] convert(Number source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
return fromString(source.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@WritingConverter
|
||||
static class EnumToBytesConverter extends StringBasedConverter implements Converter<Enum<?>, byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] convert(Enum<?> source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
return fromString(source.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@ReadingConverter
|
||||
static final class BytesToEnumConverterFactory implements ConverterFactory<byte[], Enum<?>> {
|
||||
|
||||
@Override
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public <T extends Enum<?>> Converter<byte[], T> getConverter(Class<T> targetType) {
|
||||
|
||||
Class<?> enumType = targetType;
|
||||
while (enumType != null && !enumType.isEnum()) {
|
||||
enumType = enumType.getSuperclass();
|
||||
}
|
||||
if (enumType == null) {
|
||||
throw new IllegalArgumentException("The target type " + targetType.getName() + " does not refer to an enum");
|
||||
}
|
||||
return new BytesToEnum(enumType);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
private class BytesToEnum<T extends Enum<T>> extends StringBasedConverter implements Converter<byte[], T> {
|
||||
|
||||
private final Class<T> enumType;
|
||||
|
||||
public BytesToEnum(Class<T> enumType) {
|
||||
this.enumType = enumType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T convert(byte[] source) {
|
||||
|
||||
String value = toString(source);
|
||||
|
||||
if (value == null || value.length() == 0) {
|
||||
return null;
|
||||
}
|
||||
return (T) Enum.valueOf(this.enumType, value.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@ReadingConverter
|
||||
static class BytesToNumberConverterFactory implements ConverterFactory<byte[], Number> {
|
||||
|
||||
@Override
|
||||
public <T extends Number> Converter<byte[], T> getConverter(Class<T> targetType) {
|
||||
return new BytesToNumberConverter<T>(targetType);
|
||||
}
|
||||
|
||||
private static final class BytesToNumberConverter<T extends Number> extends StringBasedConverter implements
|
||||
Converter<byte[], T> {
|
||||
|
||||
private final Class<T> targetType;
|
||||
|
||||
public BytesToNumberConverter(Class<T> targetType) {
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public T convert(byte[] source) {
|
||||
|
||||
if (source == null || source.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return NumberUtils.parseNumber(toString(source), targetType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@WritingConverter
|
||||
static class BooleanToBytesConverter extends StringBasedConverter implements Converter<Boolean, byte[]> {
|
||||
|
||||
final byte[] _true = fromString("1");
|
||||
final byte[] _false = fromString("0");
|
||||
|
||||
@Override
|
||||
public byte[] convert(Boolean source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
return source.booleanValue() ? _true : _false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@ReadingConverter
|
||||
static class BytesToBooleanConverter extends StringBasedConverter implements Converter<byte[], Boolean> {
|
||||
|
||||
@Override
|
||||
public Boolean convert(byte[] source) {
|
||||
|
||||
if (source == null || source.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String value = toString(source);
|
||||
return ("1".equals(value) || "true".equalsIgnoreCase(value)) ? Boolean.TRUE : Boolean.FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@WritingConverter
|
||||
static class DateToBytesConverter extends StringBasedConverter implements Converter<Date, byte[]> {
|
||||
|
||||
@Override
|
||||
public byte[] convert(Date source) {
|
||||
|
||||
if (source == null) {
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
return fromString(Long.toString(source.getTime()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@ReadingConverter
|
||||
static class BytesToDateConverter extends StringBasedConverter implements Converter<byte[], Date> {
|
||||
|
||||
@Override
|
||||
public Date convert(byte[] source) {
|
||||
|
||||
if (source == null || source.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String value = toString(source);
|
||||
try {
|
||||
return new Date(NumberUtils.parseNumber(value, Long.class));
|
||||
} catch (NumberFormatException nfe) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
try {
|
||||
return DateFormat.getInstance().parse(value);
|
||||
} catch (ParseException e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException("Cannot parse date out of " + source);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2015 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.nio.charset.Charset;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Bucket is the data bag for Redis hash structures to be used with {@link RedisData}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class Bucket {
|
||||
|
||||
/**
|
||||
* Encoding used for converting {@link Byte} to and from {@link String}.
|
||||
*/
|
||||
public static final Charset CHARSET = Charset.forName("UTF-8");
|
||||
|
||||
private final Map<String, byte[]> data;
|
||||
|
||||
/**
|
||||
* Creates new empty bucket
|
||||
*/
|
||||
public Bucket() {
|
||||
data = new LinkedHashMap<String, byte[]>();
|
||||
}
|
||||
|
||||
Bucket(Map<String, byte[]> data) {
|
||||
|
||||
Assert.notNull(data, "Inital data must not be null!");
|
||||
this.data = new LinkedHashMap<String, byte[]>(data.size());
|
||||
this.data.putAll(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link String} representation of property dot path with given value.
|
||||
*
|
||||
* @param path must not be {@literal null} or {@link String#isEmpty()}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
public void put(String path, byte[] value) {
|
||||
|
||||
Assert.hasText(path, "Path to property must not be null or empty.");
|
||||
data.put(path, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get value assigned with path.
|
||||
*
|
||||
* @param path path must not be {@literal null} or {@link String#isEmpty()}.
|
||||
* @return {@literal null} if not set.
|
||||
*/
|
||||
public byte[] get(String path) {
|
||||
|
||||
Assert.hasText(path, "Path to property must not be null or empty.");
|
||||
return data.get(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* A set view of the mappings contained in this bucket.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Set<Entry<String, byte[]>> entrySet() {
|
||||
return data.entrySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} when no data present in {@link Bucket}.
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return data.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Collection<byte[]> values() {
|
||||
return data.values();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Set<String> keySet() {
|
||||
return data.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Key/value pairs contained in the {@link Bucket}.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Map<String, byte[]> asMap() {
|
||||
return Collections.unmodifiableMap(this.data);
|
||||
}
|
||||
|
||||
public Bucket extract(String prefix) {
|
||||
|
||||
Bucket partial = new Bucket();
|
||||
for (Map.Entry<String, byte[]> entry : data.entrySet()) {
|
||||
if (entry.getKey().startsWith(prefix)) {
|
||||
partial.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return partial;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the keys matching a given path.
|
||||
*
|
||||
* @param path the path to look for. Can be {@literal null}.
|
||||
* @return all keys if path is {@null} or empty.
|
||||
*/
|
||||
public Set<String> extractAllKeysFor(String path) {
|
||||
|
||||
if (!StringUtils.hasText(path)) {
|
||||
return keySet();
|
||||
}
|
||||
|
||||
Pattern pattern = Pattern.compile("(" + Pattern.quote(path) + ")\\.\\[.*?\\]");
|
||||
|
||||
Set<String> keys = new LinkedHashSet<String>();
|
||||
for (Map.Entry<String, byte[]> entry : data.entrySet()) {
|
||||
|
||||
Matcher matcher = pattern.matcher(entry.getKey());
|
||||
if (matcher.find()) {
|
||||
keys.add(matcher.group());
|
||||
}
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get keys and values in binary format.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Map<byte[], byte[]> rawMap() {
|
||||
|
||||
Map<byte[], byte[]> raw = new LinkedHashMap<byte[], byte[]>(data.size());
|
||||
for (Map.Entry<String, byte[]> entry : data.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
raw.put(entry.getKey().getBytes(CHARSET), entry.getValue());
|
||||
}
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Bucket from a given raw map.
|
||||
*
|
||||
* @param source can be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public static Bucket newBucketFromRawMap(Map<byte[], byte[]> source) {
|
||||
|
||||
Bucket bucket = new Bucket();
|
||||
if (source == null) {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
for (Map.Entry<byte[], byte[]> entry : source.entrySet()) {
|
||||
bucket.put(new String(entry.getKey(), CHARSET), entry.getValue());
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Bucket from a given {@link String} map.
|
||||
*
|
||||
* @param source can be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public static Bucket newBucketFromStringMap(Map<String, String> source) {
|
||||
|
||||
Bucket bucket = new Bucket();
|
||||
if (source == null) {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
for (Map.Entry<String, String> entry : source.entrySet()) {
|
||||
bucket.put(entry.getKey(), StringUtils.hasText(entry.getValue()) ? entry.getValue().getBytes(CHARSET)
|
||||
: new byte[] {});
|
||||
}
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Bucket [data=" + safeToString() + "]";
|
||||
}
|
||||
|
||||
private String safeToString() {
|
||||
|
||||
Map<String, String> serialized = new LinkedHashMap<String, String>();
|
||||
for (Map.Entry<String, byte[]> entry : data.entrySet()) {
|
||||
if (entry.getValue() != null) {
|
||||
serialized.put(entry.getKey(), toUtf8String(entry.getValue()));
|
||||
} else {
|
||||
serialized.put(entry.getKey(), null);
|
||||
}
|
||||
}
|
||||
return serialized.toString();
|
||||
|
||||
}
|
||||
|
||||
private String toUtf8String(byte[] raw) {
|
||||
|
||||
try {
|
||||
return new String(raw, CHARSET);
|
||||
} catch (Exception e) {
|
||||
// Ignore this one
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
/*
|
||||
* Copyright 2011-2015 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.data.redis.core.convert;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.core.GenericTypeResolver;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConverterFactory;
|
||||
import org.springframework.core.convert.converter.GenericConverter;
|
||||
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
|
||||
import org.springframework.core.convert.support.GenericConversionService;
|
||||
import org.springframework.data.convert.ReadingConverter;
|
||||
import org.springframework.data.convert.WritingConverter;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.util.CacheValue;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic
|
||||
* around them.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
* @author Thomas Darimont
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class CustomConversions {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(CustomConversions.class);
|
||||
private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a Redis supported type! You might wanna check you annotation setup at the converter implementation.";
|
||||
private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a Redis supported type! You might wanna check you annotation setup at the converter implementation.";
|
||||
|
||||
private final Set<ConvertiblePair> readingPairs;
|
||||
private final Set<ConvertiblePair> writingPairs;
|
||||
private final Set<Class<?>> customSimpleTypes;
|
||||
private final SimpleTypeHolder simpleTypeHolder;
|
||||
|
||||
private final List<Object> converters;
|
||||
|
||||
private final Map<ConvertiblePair, CacheValue<Class<?>>> customReadTargetTypes;
|
||||
private final Map<ConvertiblePair, CacheValue<Class<?>>> customWriteTargetTypes;
|
||||
private final Map<Class<?>, CacheValue<Class<?>>> rawWriteTargetTypes;
|
||||
|
||||
/**
|
||||
* Creates an empty {@link CustomConversions} object.
|
||||
*/
|
||||
public CustomConversions() {
|
||||
this(new ArrayList<Object>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link CustomConversions} instance registering the given converters.
|
||||
*
|
||||
* @param converters
|
||||
*/
|
||||
public CustomConversions(List<?> converters) {
|
||||
|
||||
Assert.notNull(converters);
|
||||
|
||||
this.readingPairs = new LinkedHashSet<ConvertiblePair>();
|
||||
this.writingPairs = new LinkedHashSet<ConvertiblePair>();
|
||||
this.customSimpleTypes = new HashSet<Class<?>>();
|
||||
this.customReadTargetTypes = new ConcurrentHashMap<ConvertiblePair, CacheValue<Class<?>>>();
|
||||
this.customWriteTargetTypes = new ConcurrentHashMap<ConvertiblePair, CacheValue<Class<?>>>();
|
||||
this.rawWriteTargetTypes = new ConcurrentHashMap<Class<?>, CacheValue<Class<?>>>();
|
||||
|
||||
List<Object> toRegister = new ArrayList<Object>();
|
||||
|
||||
// Add user provided converters to make sure they can override the defaults
|
||||
toRegister.addAll(converters);
|
||||
toRegister.add(new BinaryConverters.StringToBytesConverter());
|
||||
toRegister.add(new BinaryConverters.BytesToStringConverter());
|
||||
toRegister.add(new BinaryConverters.NumberToBytesConverter());
|
||||
toRegister.add(new BinaryConverters.BytesToNumberConverterFactory());
|
||||
toRegister.add(new BinaryConverters.EnumToBytesConverter());
|
||||
toRegister.add(new BinaryConverters.BytesToEnumConverterFactory());
|
||||
toRegister.add(new BinaryConverters.BooleanToBytesConverter());
|
||||
toRegister.add(new BinaryConverters.BytesToBooleanConverter());
|
||||
toRegister.add(new BinaryConverters.DateToBytesConverter());
|
||||
toRegister.add(new BinaryConverters.BytesToDateConverter());
|
||||
|
||||
for (Object c : toRegister) {
|
||||
registerConversion(c);
|
||||
}
|
||||
|
||||
Collections.reverse(toRegister);
|
||||
|
||||
this.converters = Collections.unmodifiableList(toRegister);
|
||||
this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the underlying {@link SimpleTypeHolder}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public SimpleTypeHolder getSimpleTypeHolder() {
|
||||
return simpleTypeHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given type is considered to be simple. That means it's either a general simple type or we have
|
||||
* a writing {@link Converter} registered for a particular type.
|
||||
*
|
||||
* @see SimpleTypeHolder#isSimpleType(Class)
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
public boolean isSimpleType(Class<?> type) {
|
||||
return simpleTypeHolder.isSimpleType(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the given {@link GenericConversionService} with the convertes registered.
|
||||
*
|
||||
* @param conversionService
|
||||
*/
|
||||
public void registerConvertersIn(GenericConversionService conversionService) {
|
||||
|
||||
for (Object converter : converters) {
|
||||
|
||||
boolean added = false;
|
||||
|
||||
if (converter instanceof Converter) {
|
||||
conversionService.addConverter((Converter<?, ?>) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (converter instanceof ConverterFactory) {
|
||||
conversionService.addConverterFactory((ConverterFactory<?, ?>) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (converter instanceof GenericConverter) {
|
||||
conversionService.addConverter((GenericConverter) converter);
|
||||
added = true;
|
||||
}
|
||||
|
||||
if (!added) {
|
||||
throw new IllegalArgumentException("Given set contains element that is neither Converter nor ConverterFactory!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a conversion for the given converter. Inspects either generics or the {@link ConvertiblePair}s returned
|
||||
* by a {@link GenericConverter}.
|
||||
*
|
||||
* @param converter
|
||||
*/
|
||||
private void registerConversion(Object converter) {
|
||||
|
||||
Class<?> type = converter.getClass();
|
||||
boolean isWriting = type.isAnnotationPresent(WritingConverter.class);
|
||||
boolean isReading = type.isAnnotationPresent(ReadingConverter.class);
|
||||
|
||||
if (converter instanceof GenericConverter) {
|
||||
GenericConverter genericConverter = (GenericConverter) converter;
|
||||
for (ConvertiblePair pair : genericConverter.getConvertibleTypes()) {
|
||||
register(new ConverterRegistration(pair, isReading, isWriting));
|
||||
}
|
||||
} else if (converter instanceof Converter) {
|
||||
Class<?>[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), Converter.class);
|
||||
register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting));
|
||||
} else if (converter instanceof ConverterFactory) {
|
||||
|
||||
Class<?>[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), ConverterFactory.class);
|
||||
register(new ConverterRegistration(new ConvertiblePair(arguments[0], arguments[1]), isReading, isWriting));
|
||||
}
|
||||
|
||||
else {
|
||||
throw new IllegalArgumentException("Unsupported Converter type!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic
|
||||
* Redis types.
|
||||
*
|
||||
* @param pair
|
||||
*/
|
||||
private void register(ConverterRegistration converterRegistration) {
|
||||
|
||||
ConvertiblePair pair = converterRegistration.getConvertiblePair();
|
||||
|
||||
if (converterRegistration.isReading()) {
|
||||
|
||||
readingPairs.add(pair);
|
||||
|
||||
if (LOG.isWarnEnabled() && !converterRegistration.isSimpleSourceType()) {
|
||||
LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
|
||||
}
|
||||
}
|
||||
|
||||
if (converterRegistration.isWriting()) {
|
||||
|
||||
writingPairs.add(pair);
|
||||
customSimpleTypes.add(pair.getSourceType());
|
||||
|
||||
if (LOG.isWarnEnabled() && !converterRegistration.isSimpleTargetType()) {
|
||||
LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target type to convert to in case we have a custom conversion registered to convert the given source
|
||||
* type into a Redis native one.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public Class<?> getCustomWriteTarget(final Class<?> sourceType) {
|
||||
|
||||
return getOrCreateAndCache(sourceType, rawWriteTargetTypes, new Producer() {
|
||||
|
||||
@Override
|
||||
public Class<?> get() {
|
||||
return getCustomTarget(sourceType, null, writingPairs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the target type we can readTargetWriteLocl an inject of the given source type to. The returned type might
|
||||
* be a subclass of the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply
|
||||
* return the first target type matching or {@literal null} if no conversion can be found.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @param requestedTargetType
|
||||
* @return
|
||||
*/
|
||||
public Class<?> getCustomWriteTarget(final Class<?> sourceType, final Class<?> requestedTargetType) {
|
||||
|
||||
if (requestedTargetType == null) {
|
||||
return getCustomWriteTarget(sourceType);
|
||||
}
|
||||
|
||||
return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customWriteTargetTypes,
|
||||
new Producer() {
|
||||
|
||||
@Override
|
||||
public Class<?> get() {
|
||||
return getCustomTarget(sourceType, requestedTargetType, writingPairs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to readTargetWriteLocl into a Redis native type. The
|
||||
* returned type might be a subclass of the given expected type though.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomWriteTarget(Class<?> sourceType) {
|
||||
return hasCustomWriteTarget(sourceType, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to readTargetWriteLocl an object of the given source type
|
||||
* into an object of the given Redis native target type.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomWriteTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
return getCustomWriteTarget(sourceType, requestedTargetType) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we have a custom conversion registered to readTargetReadLock the given source into the given target
|
||||
* type.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}
|
||||
* @param requestedTargetType must not be {@literal null}
|
||||
* @return
|
||||
*/
|
||||
public boolean hasCustomReadTarget(Class<?> sourceType, Class<?> requestedTargetType) {
|
||||
return getCustomReadTarget(sourceType, requestedTargetType) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the
|
||||
* returned {@link Class} could be an assignable type to the given {@code requestedTargetType}.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType can be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private Class<?> getCustomReadTarget(final Class<?> sourceType, final Class<?> requestedTargetType) {
|
||||
|
||||
if (requestedTargetType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getOrCreateAndCache(new ConvertiblePair(sourceType, requestedTargetType), customReadTargetTypes,
|
||||
new Producer() {
|
||||
|
||||
@Override
|
||||
public Class<?> get() {
|
||||
return getCustomTarget(sourceType, requestedTargetType, readingPairs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspects the given {@link ConvertiblePair}s for ones that have a source compatible type as source. Additionally
|
||||
* checks assignability of the target type if one is given.
|
||||
*
|
||||
* @param sourceType must not be {@literal null}.
|
||||
* @param requestedTargetType can be {@literal null}.
|
||||
* @param pairs must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static Class<?> getCustomTarget(Class<?> sourceType, Class<?> requestedTargetType,
|
||||
Collection<ConvertiblePair> pairs) {
|
||||
|
||||
Assert.notNull(sourceType);
|
||||
Assert.notNull(pairs);
|
||||
|
||||
if (requestedTargetType != null && pairs.contains(new ConvertiblePair(sourceType, requestedTargetType))) {
|
||||
return requestedTargetType;
|
||||
}
|
||||
|
||||
for (ConvertiblePair typePair : pairs) {
|
||||
if (typePair.getSourceType().isAssignableFrom(sourceType)) {
|
||||
Class<?> targetType = typePair.getTargetType();
|
||||
if (requestedTargetType == null || targetType.isAssignableFrom(requestedTargetType)) {
|
||||
return targetType;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to find a value for the given key in the given cache or produce one using the given {@link Producer} and
|
||||
* store it in the cache.
|
||||
*
|
||||
* @param key the key to lookup a potentially existing value, must not be {@literal null}.
|
||||
* @param cache the cache to find the value in, must not be {@literal null}.
|
||||
* @param producer the {@link Producer} to create values to cache, must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
private static <T> Class<?> getOrCreateAndCache(T key, Map<T, CacheValue<Class<?>>> cache, Producer producer) {
|
||||
|
||||
CacheValue<Class<?>> cacheValue = cache.get(key);
|
||||
|
||||
if (cacheValue != null) {
|
||||
return cacheValue.getValue();
|
||||
}
|
||||
|
||||
Class<?> type = producer.get();
|
||||
cache.put(key, CacheValue.<Class<?>> ofNullable(type));
|
||||
|
||||
return type;
|
||||
}
|
||||
|
||||
private interface Producer {
|
||||
|
||||
Class<?> get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Conversion registration information.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
static class ConverterRegistration {
|
||||
|
||||
private final ConvertiblePair convertiblePair;
|
||||
private final boolean reading;
|
||||
private final boolean writing;
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConverterRegistration}.
|
||||
*
|
||||
* @param convertiblePair must not be {@literal null}.
|
||||
* @param isReading whether to force to consider the converter for reading.
|
||||
* @param isWritingwhether to force to consider the converter for reading.
|
||||
*/
|
||||
public ConverterRegistration(ConvertiblePair convertiblePair, boolean isReading, boolean isWriting) {
|
||||
|
||||
Assert.notNull(convertiblePair);
|
||||
|
||||
this.convertiblePair = convertiblePair;
|
||||
this.reading = isReading;
|
||||
this.writing = isWriting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link ConverterRegistration} from the given source and target type and read/write flags.
|
||||
*
|
||||
* @param source the source type to be converted from, must not be {@literal null}.
|
||||
* @param target the target type to be converted to, must not be {@literal null}.
|
||||
* @param isReading whether to force to consider the converter for reading.
|
||||
* @param isWriting whether to force to consider the converter for writing.
|
||||
*/
|
||||
public ConverterRegistration(Class<?> source, Class<?> target, boolean isReading, boolean isWriting) {
|
||||
this(new ConvertiblePair(source, target), isReading, isWriting);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the converter shall be used for writing.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isWriting() {
|
||||
return writing == true || (!reading && isSimpleTargetType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the converter shall be used for reading.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isReading() {
|
||||
return reading == true || (!writing && isSimpleSourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual conversion pair.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public ConvertiblePair getConvertiblePair() {
|
||||
return convertiblePair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the source type is a Redis simple one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isSimpleSourceType() {
|
||||
return isRedisBasicType(convertiblePair.getSourceType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the target type is a Redis simple one.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean isSimpleTargetType() {
|
||||
return isRedisBasicType(convertiblePair.getTargetType());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the given type is a type that Redis can handle basically.
|
||||
*
|
||||
* @param type
|
||||
* @return
|
||||
*/
|
||||
private static boolean isRedisBasicType(Class<?> type) {
|
||||
return (byte[].class.equals(type) || Map.class.equals(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2015 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.Set;
|
||||
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
|
||||
/**
|
||||
* {@link IndexResolver} extracts secondary index structures to be applied on a given path, {@link PersistentProperty}
|
||||
* and value.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface IndexResolver {
|
||||
|
||||
/**
|
||||
* Resolves all indexes for given type information / value combination.
|
||||
*
|
||||
* @param typeInformation must not be {@literal null}.
|
||||
* @param value the actual value. Can be {@literal null}.
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
Set<IndexedData> resolveIndexesFor(TypeInformation<?> typeInformation, Object value);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
* Copyright 2015 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.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentPropertyAccessor;
|
||||
import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration.RedisIndexSetting;
|
||||
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.util.ClassTypeInformation;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link IndexResolver} implementation considering properties annotated with {@link Indexed} or paths set up in
|
||||
* {@link IndexConfiguration}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class IndexResolverImpl implements IndexResolver {
|
||||
|
||||
private IndexConfiguration indexConfiguration;
|
||||
private RedisMappingContext mappingContext;
|
||||
|
||||
/**
|
||||
* Creates new {@link IndexResolverImpl} with empty {@link IndexConfiguration}.
|
||||
*/
|
||||
public IndexResolverImpl() {
|
||||
this(new RedisMappingContext());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link IndexResolverImpl} with given {@link IndexConfiguration}.
|
||||
*
|
||||
* @param mapppingContext must not be {@literal null}.
|
||||
*/
|
||||
public IndexResolverImpl(RedisMappingContext mappingContext) {
|
||||
|
||||
Assert.notNull(mappingContext, "MappingContext must not be null!");
|
||||
this.mappingContext = mappingContext;
|
||||
this.indexConfiguration = mappingContext.getMappingConfiguration().getIndexConfiguration();
|
||||
}
|
||||
|
||||
/*
|
||||
* (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).getKeySpace(), "", typeInformation,
|
||||
value);
|
||||
}
|
||||
|
||||
private Set<IndexedData> doResolveIndexesFor(final String keyspace, final String path,
|
||||
TypeInformation<?> typeInformation, Object value) {
|
||||
|
||||
RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeInformation);
|
||||
|
||||
if (entity == null) {
|
||||
|
||||
IndexedData index = resolveIndex(keyspace, path, null, value);
|
||||
if (index != null) {
|
||||
return Collections.singleton(index);
|
||||
}
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
|
||||
final Set<IndexedData> indexes = new LinkedHashSet<IndexedData>();
|
||||
|
||||
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
|
||||
|
||||
@Override
|
||||
public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) {
|
||||
|
||||
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
|
||||
|
||||
Object propertyValue = accessor.getProperty(persistentProperty);
|
||||
|
||||
if (propertyValue != null) {
|
||||
|
||||
TypeInformation<?> typeHint = persistentProperty.isMap() ? persistentProperty.getTypeInformation()
|
||||
.getMapValueType() : persistentProperty.getTypeInformation().getActualType();
|
||||
|
||||
IndexedData index = resolveIndex(keyspace, currentPath, persistentProperty, propertyValue);
|
||||
|
||||
if (index != null) {
|
||||
indexes.add(index);
|
||||
}
|
||||
|
||||
if (persistentProperty.isMap()) {
|
||||
|
||||
for (Entry<?, ?> entry : ((Map<?, ?>) propertyValue).entrySet()) {
|
||||
|
||||
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, entry.getValue());
|
||||
indexes.addAll(doResolveIndexesFor(keyspace, currentPath + "." + entry.getKey(),
|
||||
typeToUse.getActualType(), entry.getValue()));
|
||||
}
|
||||
|
||||
} else if (persistentProperty.isCollectionLike()) {
|
||||
|
||||
for (Object listValue : (Iterable<?>) propertyValue) {
|
||||
|
||||
TypeInformation<?> typeToUse = updateTypeHintForActualValue(typeHint, listValue);
|
||||
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeToUse.getActualType(), listValue));
|
||||
}
|
||||
}
|
||||
|
||||
else if (persistentProperty.isEntity()
|
||||
|| persistentProperty.getTypeInformation().getActualType().equals(ClassTypeInformation.OBJECT)) {
|
||||
|
||||
typeHint = updateTypeHintForActualValue(typeHint, propertyValue);
|
||||
indexes.addAll(doResolveIndexesFor(keyspace, currentPath, typeHint.getActualType(), propertyValue));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private TypeInformation<?> updateTypeHintForActualValue(TypeInformation<?> typeHint, Object propertyValue) {
|
||||
|
||||
if (typeHint.equals(ClassTypeInformation.OBJECT) || typeHint.getClass().isInterface()) {
|
||||
try {
|
||||
typeHint = mappingContext.getPersistentEntity(propertyValue.getClass()).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 IndexedData resolveIndex(String keyspace, String propertyPath, PersistentProperty<?> property, Object value) {
|
||||
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = normalizeIndexPath(propertyPath, property);
|
||||
|
||||
if (indexConfiguration.hasIndexFor(keyspace, path)) {
|
||||
return new SimpleIndexedPropertyValue(keyspace, path, value);
|
||||
}
|
||||
|
||||
else if (property != null && property.isAnnotationPresent(Indexed.class)) {
|
||||
|
||||
Indexed indexed = property.findAnnotation(Indexed.class);
|
||||
|
||||
indexConfiguration.addIndexDefinition(new RedisIndexSetting(keyspace, path, indexed.type()));
|
||||
|
||||
switch (indexed.type()) {
|
||||
case SIMPLE:
|
||||
return new SimpleIndexedPropertyValue(keyspace, path, value);
|
||||
default:
|
||||
throw new IllegalArgumentException(String.format("Unsupported index type '%s' for path '%s'.",
|
||||
indexed.type(), path));
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2015 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;
|
||||
|
||||
/**
|
||||
* {@link IndexedData} represents a secondary index for a property path in a given keyspace.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface IndexedData {
|
||||
|
||||
/**
|
||||
* Get the {@link String} representation of the dot path to the referenced property.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
String getPath();
|
||||
|
||||
/**
|
||||
* Get the associated keyspace the index resides in.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String getKeySpace();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
/*
|
||||
* Copyright 2015 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.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.TimeToLive;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link KeyspaceConfiguration} allows programmatic setup of keyspaces and time to live options for certain types. This
|
||||
* is suitable for cases where there is no option to use the equivalent {@link RedisHash} or {@link TimeToLive}
|
||||
* annotations.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class KeyspaceConfiguration {
|
||||
|
||||
private Map<Class<?>, KeyspaceSettings> settingsMap;
|
||||
|
||||
public KeyspaceConfiguration() {
|
||||
|
||||
this.settingsMap = new ConcurrentHashMap<Class<?>, KeyspaceSettings>();
|
||||
for (KeyspaceSettings initial : initialConfiguration()) {
|
||||
settingsMap.put(initial.type, initial);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if specific {@link KeyspaceSettings} are available for given type.
|
||||
*
|
||||
* @param type must not be {@literal null}.
|
||||
* @return true if settings exist.
|
||||
*/
|
||||
public boolean hasSettingsFor(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type to lookup must not be null!");
|
||||
|
||||
if (settingsMap.containsKey(type)) {
|
||||
|
||||
if (settingsMap.get(type) instanceof DefaultKeyspaceSetting) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
for (KeyspaceSettings assignment : settingsMap.values()) {
|
||||
if (assignment.inherit) {
|
||||
if (ClassUtils.isAssignable(assignment.type, type)) {
|
||||
settingsMap.put(type, assignment.cloneFor(type));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
settingsMap.put(type, new DefaultKeyspaceSetting(type));
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link KeyspaceSettings} for given type.
|
||||
*
|
||||
* @param type must not be {@literal null}
|
||||
* @return {@literal null} if no settings configured.
|
||||
*/
|
||||
public KeyspaceSettings getKeyspaceSettings(Class<?> type) {
|
||||
|
||||
if (!hasSettingsFor(type)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
KeyspaceSettings settings = settingsMap.get(type);
|
||||
if (settings == null || settings instanceof DefaultKeyspaceSetting) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook.
|
||||
*
|
||||
* @return must not return {@literal null}.
|
||||
*/
|
||||
protected Iterable<KeyspaceSettings> initialConfiguration() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add {@link KeyspaceSettings} for type.
|
||||
*
|
||||
* @param keyspaceSettings must not be {@literal null}.
|
||||
*/
|
||||
public void addKeyspaceSettings(KeyspaceSettings keyspaceSettings) {
|
||||
|
||||
Assert.notNull(keyspaceSettings);
|
||||
this.settingsMap.put(keyspaceSettings.getType(), keyspaceSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public static class KeyspaceSettings {
|
||||
|
||||
private final String keyspace;
|
||||
private final Class<?> type;
|
||||
private final boolean inherit;
|
||||
private Long timeToLive;
|
||||
private String timeToLivePropertyName;
|
||||
|
||||
public KeyspaceSettings(Class<?> type, String keyspace) {
|
||||
this(type, keyspace, true);
|
||||
}
|
||||
|
||||
public KeyspaceSettings(Class<?> type, String keyspace, boolean inherit) {
|
||||
|
||||
this.type = type;
|
||||
this.keyspace = keyspace;
|
||||
this.inherit = inherit;
|
||||
}
|
||||
|
||||
KeyspaceSettings cloneFor(Class<?> type) {
|
||||
return new KeyspaceSettings(type, this.keyspace, false);
|
||||
}
|
||||
|
||||
public String getKeyspace() {
|
||||
return keyspace;
|
||||
}
|
||||
|
||||
public Class<?> getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setTimeToLive(Long timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
public Long getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
|
||||
public void setTimeToLivePropertyName(String propertyName) {
|
||||
timeToLivePropertyName = propertyName;
|
||||
}
|
||||
|
||||
public String getTimeToLivePropertyName() {
|
||||
return timeToLivePropertyName;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Marker class indicating no settings defined.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
private static class DefaultKeyspaceSetting extends KeyspaceSettings {
|
||||
|
||||
public DefaultKeyspaceSetting(Class<?> type) {
|
||||
super(type, "#default#", false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2015 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 org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
|
||||
/**
|
||||
* {@link MappingConfiguration} is used for programmatic configuration of secondary indexes, key prefixes, expirations
|
||||
* and the such.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class MappingConfiguration {
|
||||
|
||||
private final IndexConfiguration indexConfiguration;
|
||||
private final KeyspaceConfiguration keyspaceConfiguration;
|
||||
|
||||
/**
|
||||
* Creates new {@link MappingConfiguration}.
|
||||
*
|
||||
* @param indexConfiguration must not be {@literal null}.
|
||||
* @param keyspaceConfiguration must not be {@literal null}.
|
||||
*/
|
||||
public MappingConfiguration(IndexConfiguration indexConfiguration, KeyspaceConfiguration keyspaceConfiguration) {
|
||||
|
||||
this.indexConfiguration = indexConfiguration;
|
||||
this.keyspaceConfiguration = keyspaceConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public IndexConfiguration getIndexConfiguration() {
|
||||
return indexConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public KeyspaceConfiguration getKeyspaceConfiguration() {
|
||||
return keyspaceConfiguration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,815 @@
|
||||
/*
|
||||
* Copyright 2015 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.io.Serializable;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
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.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.keyvalue.core.mapping.KeyValuePersistentEntity;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
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.model.PersistentEntityParameterValueProvider;
|
||||
import org.springframework.data.mapping.model.PropertyValueProvider;
|
||||
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.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;
|
||||
|
||||
/**
|
||||
* {@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
|
||||
* @since 1.7
|
||||
*/
|
||||
public class MappingRedisConverter implements RedisConverter, InitializingBean {
|
||||
|
||||
private final RedisMappingContext mappingContext;
|
||||
private final GenericConversionService conversionService;
|
||||
private final EntityInstantiators entityInstantiators;
|
||||
private final TypeMapper<RedisData> typeMapper;
|
||||
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 CustomConversions();
|
||||
|
||||
typeMapper = new DefaultTypeMapper<RedisData>(new RedisTypeAliasAccessor(this.conversionService));
|
||||
|
||||
this.referenceResolver = referenceResolver;
|
||||
|
||||
this.indexResolver = indexResolver != null ? indexResolver : new IndexResolverImpl(this.mappingContext);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.EntityReader#read(java.lang.Class, java.lang.Object)
|
||||
*/
|
||||
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);
|
||||
TypeInformation<?> typeToUse = readType != null ? readType : ClassTypeInformation.from(type);
|
||||
final RedisPersistentEntity<?> entity = mappingContext.getPersistentEntity(typeToUse);
|
||||
|
||||
if (customConversions.hasCustomReadTarget(Map.class, typeToUse.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, typeToUse.getType());
|
||||
|
||||
if (entity.hasIdProperty()) {
|
||||
entity.getPropertyAccessor(instance).setProperty(entity.getIdProperty(), source.getId());
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (conversionService.canConvert(byte[].class, typeToUse.getType())) {
|
||||
return (R) conversionService.convert(source.getBucket().get(StringUtils.hasText(path) ? path : "_raw"),
|
||||
typeToUse.getType());
|
||||
}
|
||||
|
||||
EntityInstantiator instantiator = entityInstantiators.getInstantiatorFor(entity);
|
||||
|
||||
Object instance = instantiator.createInstance((RedisPersistentEntity<?>) entity,
|
||||
new PersistentEntityParameterValueProvider<KeyValuePersistentProperty>(entity,
|
||||
new ConverterAwareParameterValueProvider(source, conversionService), null));
|
||||
|
||||
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(instance);
|
||||
|
||||
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
|
||||
|
||||
@Override
|
||||
public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) {
|
||||
|
||||
String currentPath = !path.isEmpty() ? path + "." + persistentProperty.getName() : persistentProperty.getName();
|
||||
|
||||
PreferredConstructor<?, KeyValuePersistentProperty> constructor = entity.getPersistenceConstructor();
|
||||
|
||||
if (constructor.isConstructorParameter(persistentProperty)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (persistentProperty.isMap()) {
|
||||
|
||||
if (conversionService.canConvert(byte[].class, persistentProperty.getMapValueType())) {
|
||||
accessor.setProperty(
|
||||
persistentProperty,
|
||||
readMapOfSimpleTypes(currentPath, persistentProperty.getType(), persistentProperty.getComponentType(),
|
||||
persistentProperty.getMapValueType(), source));
|
||||
} else {
|
||||
accessor.setProperty(
|
||||
persistentProperty,
|
||||
readMapOfComplexTypes(currentPath, persistentProperty.getType(), persistentProperty.getComponentType(),
|
||||
persistentProperty.getMapValueType(), source));
|
||||
}
|
||||
}
|
||||
|
||||
else if (persistentProperty.isCollectionLike()) {
|
||||
|
||||
if (conversionService.canConvert(byte[].class, persistentProperty.getComponentType())) {
|
||||
accessor.setProperty(
|
||||
persistentProperty,
|
||||
readCollectionOfSimpleTypes(currentPath, persistentProperty.getType(), persistentProperty
|
||||
.getTypeInformation().getComponentType().getActualType().getType(), source));
|
||||
} else {
|
||||
accessor.setProperty(
|
||||
persistentProperty,
|
||||
readCollectionOfComplexTypes(currentPath, persistentProperty.getType(), persistentProperty
|
||||
.getTypeInformation().getComponentType().getActualType().getType(), source.getBucket()));
|
||||
}
|
||||
|
||||
} else if (persistentProperty.isEntity()
|
||||
&& !conversionService.canConvert(byte[].class, persistentProperty.getTypeInformation().getActualType()
|
||||
.getType())) {
|
||||
|
||||
Class<?> targetType = persistentProperty.getTypeInformation().getActualType().getType();
|
||||
|
||||
Bucket bucket = source.getBucket().extract(currentPath + ".");
|
||||
|
||||
RedisData source = new RedisData(bucket);
|
||||
|
||||
byte[] type = bucket.get(currentPath + "._class");
|
||||
if (type != null && type.length > 0) {
|
||||
source.getBucket().put("_class", type);
|
||||
}
|
||||
|
||||
accessor.setProperty(persistentProperty, readInternal(currentPath, targetType, source));
|
||||
} else {
|
||||
|
||||
if (persistentProperty.isIdProperty() && StringUtils.isEmpty(path.isEmpty())) {
|
||||
|
||||
if (source.getBucket().get(currentPath) == null) {
|
||||
accessor.setProperty(persistentProperty,
|
||||
fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType()));
|
||||
} else {
|
||||
accessor.setProperty(persistentProperty, source.getId());
|
||||
}
|
||||
}
|
||||
|
||||
accessor.setProperty(persistentProperty,
|
||||
fromBytes(source.getBucket().get(currentPath), persistentProperty.getActualType()));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
readAssociation(path, source, entity, accessor);
|
||||
|
||||
return (R) instance;
|
||||
}
|
||||
|
||||
private void readAssociation(final String path, final RedisData source, final KeyValuePersistentEntity<?> entity,
|
||||
final PersistentPropertyAccessor accessor) {
|
||||
|
||||
entity.doWithAssociations(new AssociationHandler<KeyValuePersistentProperty>() {
|
||||
|
||||
@Override
|
||||
public void doWithAssociation(Association<KeyValuePersistentProperty> association) {
|
||||
|
||||
String currentPath = !path.isEmpty() ? path + "." + association.getInverse().getName() : association
|
||||
.getInverse().getName();
|
||||
|
||||
if (association.getInverse().isCollectionLike()) {
|
||||
|
||||
Collection<Object> target = CollectionFactory.createCollection(association.getInverse().getType(),
|
||||
association.getInverse().getComponentType(), 10);
|
||||
|
||||
Bucket bucket = source.getBucket().extract(currentPath + ".[");
|
||||
|
||||
for (Entry<String, byte[]> entry : bucket.entrySet()) {
|
||||
|
||||
String referenceKey = fromBytes(entry.getValue(), String.class);
|
||||
String[] args = referenceKey.split(":");
|
||||
|
||||
Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse()
|
||||
.getActualType());
|
||||
|
||||
if (loadedObject != null) {
|
||||
target.add(loadedObject);
|
||||
}
|
||||
}
|
||||
|
||||
accessor.setProperty(association.getInverse(), 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(":");
|
||||
Object loadedObject = referenceResolver.resolveReference(args[1], args[0], association.getInverse()
|
||||
.getActualType());
|
||||
|
||||
if (loadedObject != null) {
|
||||
accessor.setProperty(association.getInverse(), loadedObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.EntityWriter#write(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
public void write(Object source, final RedisData sink) {
|
||||
|
||||
final RedisPersistentEntity entity = mappingContext.getPersistentEntity(source.getClass());
|
||||
|
||||
if (!customConversions.hasCustomWriteTarget(source.getClass())) {
|
||||
typeMapper.writeType(ClassUtils.getUserClass(source), sink);
|
||||
}
|
||||
sink.setKeyspace(entity.getKeySpace());
|
||||
|
||||
writeInternal(entity.getKeySpace(), "", source, entity.getTypeInformation(), sink);
|
||||
sink.setId((Serializable) entity.getIdentifierAccessor(source).getIdentifier());
|
||||
|
||||
Long ttl = entity.getTimeToLiveAccessor().getTimeToLive(source);
|
||||
if (ttl != null && ttl > 0) {
|
||||
sink.setTimeToLive(ttl);
|
||||
}
|
||||
|
||||
for (IndexedData indexeData : indexResolver.resolveIndexesFor(entity.getTypeInformation(), source)) {
|
||||
sink.addIndexedData(indexeData);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 (customConversions.getCustomWriteTarget(value.getClass()).equals(byte[].class)) {
|
||||
sink.getBucket().put(StringUtils.hasText(path) ? path : "_raw", conversionService.convert(value, byte[].class));
|
||||
} else {
|
||||
writeToBucket(path, value, sink);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value.getClass() != typeHint.getType()) {
|
||||
sink.getBucket().put((!path.isEmpty() ? path + "._class" : "_class"), toBytes(value.getClass().getName()));
|
||||
}
|
||||
|
||||
final KeyValuePersistentEntity<?> entity = mappingContext.getPersistentEntity(value.getClass());
|
||||
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
|
||||
|
||||
entity.doWithProperties(new PropertyHandler<KeyValuePersistentProperty>() {
|
||||
|
||||
@Override
|
||||
public void doWithPersistentProperty(KeyValuePersistentProperty persistentProperty) {
|
||||
|
||||
String propertyStringPath = (!path.isEmpty() ? path + "." : "") + persistentProperty.getName();
|
||||
|
||||
if (persistentProperty.isIdProperty()) {
|
||||
|
||||
sink.getBucket().put(propertyStringPath, toBytes(accessor.getProperty(persistentProperty)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (persistentProperty.isMap()) {
|
||||
writeMap(keyspace, propertyStringPath, persistentProperty.getMapValueType(),
|
||||
(Map<?, ?>) accessor.getProperty(persistentProperty), sink);
|
||||
} else if (persistentProperty.isCollectionLike()) {
|
||||
writeCollection(keyspace, propertyStringPath, (Collection<?>) accessor.getProperty(persistentProperty),
|
||||
persistentProperty.getTypeInformation().getComponentType(), sink);
|
||||
} else if (persistentProperty.isEntity()) {
|
||||
writeInternal(keyspace, propertyStringPath, accessor.getProperty(persistentProperty), persistentProperty
|
||||
.getTypeInformation().getActualType(), sink);
|
||||
} else {
|
||||
|
||||
Object propertyValue = accessor.getProperty(persistentProperty);
|
||||
sink.getBucket().put(propertyStringPath, toBytes(propertyValue));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
writeAssiciation(keyspace, path, entity, value, sink);
|
||||
}
|
||||
|
||||
private void writeAssiciation(final String keyspace, final String path, final KeyValuePersistentEntity<?> entity,
|
||||
final Object value, final RedisData sink) {
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PersistentPropertyAccessor accessor = entity.getPropertyAccessor(value);
|
||||
|
||||
entity.doWithAssociations(new AssociationHandler<KeyValuePersistentProperty>() {
|
||||
|
||||
@Override
|
||||
public void doWithAssociation(Association<KeyValuePersistentProperty> association) {
|
||||
|
||||
Object refObject = accessor.getProperty(association.getInverse());
|
||||
if (refObject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (association.getInverse().isCollectionLike()) {
|
||||
|
||||
KeyValuePersistentEntity<?> ref = mappingContext.getPersistentEntity(association.getInverse()
|
||||
.getTypeInformation().getComponentType().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.getIdProperty());
|
||||
sink.getBucket().put(propertyStringPath + ".[" + i + "]", toBytes(keyspace + ":" + refId));
|
||||
i++;
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
KeyValuePersistentEntity<?> ref = mappingContext.getPersistentEntity(association.getInverse()
|
||||
.getTypeInformation());
|
||||
String keyspace = ref.getKeySpace();
|
||||
|
||||
Object refId = ref.getPropertyAccessor(refObject).getProperty(ref.getIdProperty());
|
||||
|
||||
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, Collection<?> values, TypeInformation<?> typeHint,
|
||||
RedisData sink) {
|
||||
|
||||
if (values == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int i = 0;
|
||||
for (Object value : values) {
|
||||
|
||||
String currentPath = path + ".[" + i + "]";
|
||||
|
||||
if (customConversions.hasCustomWriteTarget(value.getClass())) {
|
||||
writeToBucket(currentPath, value, sink);
|
||||
} else {
|
||||
writeInternal(keyspace, currentPath, value, typeHint, sink);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void writeToBucket(String path, Object value, RedisData sink) {
|
||||
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (customConversions.hasCustomWriteTarget(value.getClass())) {
|
||||
Class<?> targetType = customConversions.getCustomWriteTarget(value.getClass());
|
||||
|
||||
if (ClassUtils.isAssignable(Map.class, targetType)) {
|
||||
|
||||
Map<?, ?> map = (Map<?, ?>) conversionService.convert(value, targetType);
|
||||
for (Map.Entry<?, ?> entry : map.entrySet()) {
|
||||
sink.getBucket().put(path + (StringUtils.hasText(path) ? "." : "") + entry.getKey(),
|
||||
toBytes(entry.getValue()));
|
||||
}
|
||||
} else if (ClassUtils.isAssignable(byte[].class, targetType)) {
|
||||
sink.getBucket().put(path, toBytes(value));
|
||||
} else {
|
||||
throw new IllegalArgumentException("converter must not fool me!!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path
|
||||
* @param collectionType
|
||||
* @param valueType
|
||||
* @param source
|
||||
* @return
|
||||
*/
|
||||
private Collection<?> readCollectionOfSimpleTypes(String path, Class<?> collectionType, Class<?> valueType,
|
||||
RedisData source) {
|
||||
|
||||
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, 10);
|
||||
|
||||
Bucket partial = source.getBucket().extract(path + ".[");
|
||||
|
||||
for (byte[] value : partial.values()) {
|
||||
target.add(fromBytes(value, valueType));
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path
|
||||
* @param collectionType
|
||||
* @param valueType
|
||||
* @param source
|
||||
* @return
|
||||
*/
|
||||
private Collection<?> readCollectionOfComplexTypes(String path, Class<?> collectionType, Class<?> valueType,
|
||||
Bucket source) {
|
||||
|
||||
Collection<Object> target = CollectionFactory.createCollection(collectionType, valueType, 10);
|
||||
|
||||
Set<String> keys = source.extractAllKeysFor(path);
|
||||
|
||||
for (String key : keys) {
|
||||
|
||||
Bucket elementData = source.extract(key);
|
||||
|
||||
byte[] typeInfo = elementData.get(key + "._class");
|
||||
if (typeInfo != null && typeInfo.length > 0) {
|
||||
elementData.put("_class", typeInfo);
|
||||
}
|
||||
|
||||
Object o = readInternal(key, valueType, new RedisData(elementData));
|
||||
target.add(o);
|
||||
}
|
||||
|
||||
return 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 (customConversions.hasCustomWriteTarget(entry.getValue().getClass())) {
|
||||
writeToBucket(currentPath, entry.getValue(), sink);
|
||||
} 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) {
|
||||
|
||||
Map<Object, Object> target = CollectionFactory.createMap(mapType, 10);
|
||||
|
||||
Bucket partial = source.getBucket().extract(path + ".[");
|
||||
|
||||
for (Entry<String, byte[]> entry : partial.entrySet()) {
|
||||
|
||||
String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
|
||||
Matcher matcher = pattern.matcher(entry.getKey());
|
||||
if (!matcher.find()) {
|
||||
throw new RuntimeException("baähhh");
|
||||
}
|
||||
String key = matcher.group(2);
|
||||
target.put(key, fromBytes(entry.getValue(), valueType));
|
||||
}
|
||||
|
||||
return 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) {
|
||||
|
||||
Map<Object, Object> target = CollectionFactory.createMap(mapType, 10);
|
||||
|
||||
Set<String> keys = source.getBucket().extractAllKeysFor(path);
|
||||
|
||||
for (String key : keys) {
|
||||
|
||||
String regex = "^(" + Pattern.quote(path) + "\\.\\[)(.*?)(\\])";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
|
||||
Matcher matcher = pattern.matcher(key);
|
||||
if (!matcher.find()) {
|
||||
throw new RuntimeException("baähhh");
|
||||
}
|
||||
String mapKey = matcher.group(2);
|
||||
|
||||
Bucket partial = source.getBucket().extract(key);
|
||||
|
||||
byte[] typeInfo = partial.get(key + "._class");
|
||||
if (typeInfo != null && typeInfo.length > 0) {
|
||||
partial.put("_class", typeInfo);
|
||||
}
|
||||
|
||||
Object o = readInternal(key, valueType, new RedisData(partial));
|
||||
target.put(mapKey, o);
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set {@link CustomConversions} to be applied.
|
||||
*
|
||||
* @param customConversions
|
||||
*/
|
||||
public void setCustomConversions(CustomConversions customConversions) {
|
||||
this.customConversions = customConversions != null ? customConversions : new CustomConversions();
|
||||
}
|
||||
|
||||
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()
|
||||
*/
|
||||
public RedisMappingContext getMappingContext() {
|
||||
return this.mappingContext;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.convert.EntityConverter#getConversionService()
|
||||
*/
|
||||
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<KeyValuePersistentProperty> {
|
||||
|
||||
private final RedisData source;
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public ConverterAwareParameterValueProvider(RedisData source, ConversionService conversionService) {
|
||||
this.source = source;
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getPropertyValue(KeyValuePersistentProperty property) {
|
||||
return (T) conversionService.convert(source.getBucket().get(property.getName()), 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, "_class");
|
||||
}
|
||||
|
||||
RedisTypeAliasAccessor(ConversionService conversionService, String typeKey) {
|
||||
|
||||
this.conversionService = conversionService;
|
||||
this.typeKey = typeKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object readAliasFrom(RedisData source) {
|
||||
return 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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2015 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 org.springframework.data.convert.EntityConverter;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
|
||||
/**
|
||||
* Redis sepcific {@link EntityConverter}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface RedisConverter extends
|
||||
EntityConverter<KeyValuePersistentEntity<?>, KeyValuePersistentProperty, Object, RedisData> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2015 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.io.Serializable;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Data object holding {@link Bucket} representing the domain object to be stored in a Redis hash. Index information
|
||||
* points to additional structures holding the objects is for searching.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisData {
|
||||
|
||||
private String keyspace;
|
||||
private Serializable id;
|
||||
|
||||
private Bucket bucket;
|
||||
private Set<IndexedData> indexedData;
|
||||
|
||||
private Long timeToLive;
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisData} with empty {@link Bucket}.
|
||||
*/
|
||||
public RedisData() {
|
||||
this(Collections.<byte[], byte[]> emptyMap());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisData} with {@link Bucket} holding provided values.
|
||||
*
|
||||
* @param raw should not be {@literal null}.
|
||||
*/
|
||||
public RedisData(Map<byte[], byte[]> raw) {
|
||||
this(Bucket.newBucketFromRawMap(raw));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisData} with {@link Bucket}
|
||||
*
|
||||
* @param bucket must not be {@literal null}.
|
||||
*/
|
||||
public RedisData(Bucket bucket) {
|
||||
|
||||
Assert.notNull(bucket, "Bucket must not be null!");
|
||||
this.bucket = bucket;
|
||||
this.indexedData = new HashSet<IndexedData>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the id to be used as part of the key.
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
public void setId(Serializable id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
public Serializable getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time before expiration in seconds.
|
||||
*
|
||||
* @return {@literal null} if not set.
|
||||
*/
|
||||
public Long getTimeToLive() {
|
||||
return timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param index
|
||||
*/
|
||||
public void addIndexedData(IndexedData index) {
|
||||
|
||||
Assert.notNull(index, "IndexedData to add must not be null!");
|
||||
this.indexedData.add(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public Set<IndexedData> getIndexedData() {
|
||||
return Collections.unmodifiableSet(this.indexedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
public String getKeyspace() {
|
||||
return keyspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyspace
|
||||
*/
|
||||
public void setKeyspace(String keyspace) {
|
||||
this.keyspace = keyspace;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return
|
||||
*/
|
||||
public Bucket getBucket() {
|
||||
return bucket;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time before expiration in {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @param timeToLive can be {@literal null}.
|
||||
*/
|
||||
public void setTimeToLive(Long timeToLive) {
|
||||
this.timeToLive = timeToLive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the time before expiration converting the given arguments to {@link TimeUnit#SECONDS}.
|
||||
*
|
||||
* @param timeToLive must not be {@literal null}
|
||||
* @param timeUnit must not be {@literal null}
|
||||
*/
|
||||
public void setTimeToLive(Long timeToLive, TimeUnit timeUnit) {
|
||||
|
||||
Assert.notNull(timeToLive, "TimeToLive must not be null when used with TimeUnit!");
|
||||
Assert.notNull(timeToLive, "TimeUnit must not be null!");
|
||||
|
||||
setTimeToLive(TimeUnit.SECONDS.convert(timeToLive, timeUnit));
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "RedisDataObject [key=" + keyspace + ":" + id + ", hash=" + bucket + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2015 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.io.Serializable;
|
||||
|
||||
import org.springframework.data.annotation.Reference;
|
||||
|
||||
/**
|
||||
* {@link ReferenceResolver} retrieves Objects marked with {@link Reference} from Redis.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface ReferenceResolver {
|
||||
|
||||
/**
|
||||
* @param id must not be {@literal null}.
|
||||
* @param keyspace must not be {@literal null}.
|
||||
* @param type must not be {@literal null}.
|
||||
* @return {@literal null} if referenced object does not exist.
|
||||
*/
|
||||
<T> T resolveReference(Serializable id, Serializable keyspace, Class<T> type);
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2015 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 org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link IndexedData} implementation indicating storage of data within a Redis Set.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class SimpleIndexedPropertyValue implements IndexedData {
|
||||
|
||||
private final String keyspace;
|
||||
private final String path;
|
||||
private final Object value;
|
||||
|
||||
/**
|
||||
* Creates new {@link SimpleIndexedPropertyValue}.
|
||||
*
|
||||
* @param keyspace must not be {@literal null}.
|
||||
* @param path must not be {@literal null}.
|
||||
* @param value can be {@literal null}.
|
||||
*/
|
||||
public SimpleIndexedPropertyValue(String keyspace, String path, Object value) {
|
||||
|
||||
this.keyspace = keyspace;
|
||||
this.path = path;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value to index.
|
||||
*
|
||||
* @return can be {@literal null}.
|
||||
*/
|
||||
public Object getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.convert.IndexedData#getPath()
|
||||
*/
|
||||
@Override
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.convert.IndexedData#getKeySpace()
|
||||
*/
|
||||
@Override
|
||||
public String getKeySpace() {
|
||||
return this.keyspace;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#hashCode()
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = 1;
|
||||
result += ObjectUtils.nullSafeHashCode(keyspace);
|
||||
result += ObjectUtils.nullSafeHashCode(path);
|
||||
result += ObjectUtils.nullSafeHashCode(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#equals(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof SimpleIndexedPropertyValue)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SimpleIndexedPropertyValue that = (SimpleIndexedPropertyValue) obj;
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) {
|
||||
return false;
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(this.path, that.path)) {
|
||||
return false;
|
||||
}
|
||||
return ObjectUtils.nullSafeEquals(this.value, that.value);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see java.lang.Object#toString()
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SimpleIndexedPropertyValue [keyspace=" + keyspace + ", path=" + path + ", value=" + value + "]";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* Copyright 2015 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.index;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CopyOnWriteArraySet;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* {@link IndexConfiguration} allows programmatic setup of indexes. This is suitable for cases where there is no option
|
||||
* to use the equivalent {@link Indexed} annotation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class IndexConfiguration {
|
||||
|
||||
private final Set<RedisIndexSetting> definitions;
|
||||
|
||||
/**
|
||||
* Creates new empty {@link IndexConfiguration}.
|
||||
*/
|
||||
public IndexConfiguration() {
|
||||
this.definitions = new CopyOnWriteArraySet<RedisIndexSetting>();
|
||||
for (RedisIndexSetting initial : initialConfiguration()) {
|
||||
addIndexDefinition(initial);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an index is defined for a given keyspace and property path.
|
||||
*
|
||||
* @param keyspace
|
||||
* @param path
|
||||
* @return true if index is defined.
|
||||
*/
|
||||
public boolean hasIndexFor(Serializable keyspace, String path) {
|
||||
|
||||
for (IndexType type : IndexType.values()) {
|
||||
RedisIndexSetting def = getIndexDefinition(keyspace, path, type);
|
||||
if (def != null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of {@link RedisIndexSetting} for a given keyspace and property path.
|
||||
*
|
||||
* @param keyspace
|
||||
* @param path
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public List<RedisIndexSetting> getIndexDefinitionsFor(Serializable keyspace, String path) {
|
||||
|
||||
List<RedisIndexSetting> indexDefinitions = new ArrayList<RedisIndexSetting>();
|
||||
for (IndexType type : IndexType.values()) {
|
||||
RedisIndexSetting def = getIndexDefinition(keyspace, path, type);
|
||||
if (def != null) {
|
||||
indexDefinitions.add(def);
|
||||
}
|
||||
}
|
||||
|
||||
return indexDefinitions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add given {@link RedisIndexSetting}.
|
||||
*
|
||||
* @param indexDefinition must not be {@literal null}.
|
||||
*/
|
||||
public void addIndexDefinition(RedisIndexSetting indexDefinition) {
|
||||
|
||||
Assert.notNull(indexDefinition, "RedisIndexDefinition must not be null in order to be added.");
|
||||
this.definitions.add(indexDefinition);
|
||||
}
|
||||
|
||||
private RedisIndexSetting getIndexDefinition(Serializable keyspace, String path, IndexType type) {
|
||||
|
||||
for (RedisIndexSetting indexDef : definitions) {
|
||||
if (indexDef.getKeyspace().equals(keyspace) && indexDef.getPath().equals(path) && indexDef.getType().equals(type)) {
|
||||
return indexDef;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Customization hook.
|
||||
*
|
||||
* @return must not return {@literal null}.
|
||||
*/
|
||||
protected Iterable<RedisIndexSetting> initialConfiguration() {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public static class RedisIndexSetting {
|
||||
|
||||
private final Serializable keyspace;
|
||||
private final String path;
|
||||
private final IndexType type;
|
||||
|
||||
public RedisIndexSetting(Serializable keyspace, String path) {
|
||||
this(keyspace, path, null);
|
||||
}
|
||||
|
||||
public RedisIndexSetting(Serializable keyspace, String path, IndexType type) {
|
||||
|
||||
this.keyspace = keyspace;
|
||||
this.path = path;
|
||||
this.type = type == null ? IndexType.SIMPLE : type;
|
||||
}
|
||||
|
||||
public Serializable getKeyspace() {
|
||||
return keyspace;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public IndexType getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
|
||||
int result = ObjectUtils.nullSafeHashCode(keyspace);
|
||||
result += ObjectUtils.nullSafeHashCode(path);
|
||||
result += ObjectUtils.nullSafeHashCode(type);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
if (!(obj instanceof RedisIndexSetting)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
RedisIndexSetting that = (RedisIndexSetting) obj;
|
||||
|
||||
if (!ObjectUtils.nullSafeEquals(this.keyspace, that.keyspace)) {
|
||||
return false;
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(this.path, that.path)) {
|
||||
return false;
|
||||
}
|
||||
if (!ObjectUtils.nullSafeEquals(this.type, that.type)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2015 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.index;
|
||||
|
||||
/**
|
||||
* {@link IndexType} indicates structure of index.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public enum IndexType {
|
||||
|
||||
/**
|
||||
* Simple indicates usage of Redis Set for persisting data.
|
||||
*/
|
||||
SIMPLE
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2015 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.index;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* Mark property to be included in a secondary index.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@Documented
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE })
|
||||
public @interface Indexed {
|
||||
|
||||
/**
|
||||
* Type of index to use.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
IndexType type() default IndexType.SIMPLE;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2015 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.mapping;
|
||||
|
||||
import org.springframework.data.keyvalue.core.mapping.BasicKeyValuePersistentEntity;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
|
||||
import org.springframework.data.redis.core.TimeToLiveAccessor;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RedisPersistentEntity} implementation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @param <T>
|
||||
*/
|
||||
public class BasicRedisPersistentEntity<T> extends BasicKeyValuePersistentEntity<T> implements RedisPersistentEntity<T> {
|
||||
|
||||
private TimeToLiveAccessor timeToLiveAccessor;
|
||||
|
||||
/**
|
||||
* Creates new {@link BasicRedisPersistentEntity}.
|
||||
*
|
||||
* @param information must not be {@literal null}.
|
||||
* @param fallbackKeySpaceResolver can be {@literal null}.
|
||||
* @param timeToLiveResolver can be {@literal null}.
|
||||
*/
|
||||
public BasicRedisPersistentEntity(TypeInformation<T> information, KeySpaceResolver fallbackKeySpaceResolver,
|
||||
TimeToLiveAccessor timeToLiveAccessor) {
|
||||
super(information, fallbackKeySpaceResolver);
|
||||
|
||||
Assert.notNull(timeToLiveAccessor, "TimeToLiveAccessor must not be null");
|
||||
this.timeToLiveAccessor = timeToLiveAccessor;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.mapping.RedisPersistentEntity#getTimeToLiveAccessor()
|
||||
*/
|
||||
@Override
|
||||
public TimeToLiveAccessor getTimeToLiveAccessor() {
|
||||
return this.timeToLiveAccessor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
/*
|
||||
* Copyright 2015 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.mapping;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.data.keyvalue.annotation.KeySpace;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeySpaceResolver;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
import org.springframework.data.redis.core.RedisHash;
|
||||
import org.springframework.data.redis.core.TimeToLive;
|
||||
import org.springframework.data.redis.core.TimeToLiveAccessor;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings;
|
||||
import org.springframework.data.redis.core.convert.MappingConfiguration;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.ReflectionUtils.MethodCallback;
|
||||
import org.springframework.util.ReflectionUtils.MethodFilter;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Redis specific {@link MappingContext}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisMappingContext extends KeyValueMappingContext {
|
||||
|
||||
private MappingConfiguration mappingConfiguration;
|
||||
private KeySpaceResolver fallbackKeySpaceResolver;
|
||||
private TimeToLiveAccessor timeToLiveAccessor;
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisMappingContext} with empty {@link MappingConfiguration}.
|
||||
*/
|
||||
public RedisMappingContext() {
|
||||
this(new MappingConfiguration(new IndexConfiguration(), new KeyspaceConfiguration()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisMappingContext}.
|
||||
*
|
||||
* @param mappingConfiguration can be {@literal null}.
|
||||
*/
|
||||
public RedisMappingContext(MappingConfiguration mappingConfiguration) {
|
||||
|
||||
this.mappingConfiguration = mappingConfiguration != null ? mappingConfiguration : new MappingConfiguration(
|
||||
new IndexConfiguration(), new KeyspaceConfiguration());
|
||||
|
||||
setFallbackKeySpaceResolver(new ConfigAwareKeySpaceResolver(this.mappingConfiguration.getKeyspaceConfiguration()));
|
||||
this.timeToLiveAccessor = new ConfigAwareTimeToLiveAccessor(this.mappingConfiguration.getKeyspaceConfiguration(),
|
||||
this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures the {@link KeySpaceResolver} to be used if not explicit key space is annotated to the domain type.
|
||||
*
|
||||
* @param fallbackKeySpaceResolver can be {@literal null}.
|
||||
*/
|
||||
public void setFallbackKeySpaceResolver(KeySpaceResolver fallbackKeySpaceResolver) {
|
||||
this.fallbackKeySpaceResolver = fallbackKeySpaceResolver;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#createPersistentEntity(org.springframework.data.util.TypeInformation)
|
||||
*/
|
||||
@Override
|
||||
protected <T> RedisPersistentEntity<T> createPersistentEntity(TypeInformation<T> typeInformation) {
|
||||
return new BasicRedisPersistentEntity<T>(typeInformation, fallbackKeySpaceResolver, timeToLiveAccessor);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public RedisPersistentEntity<?> getPersistentEntity(Class<?> type) {
|
||||
return (RedisPersistentEntity<?>) super.getPersistentEntity(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.mapping.PersistentProperty)
|
||||
*/
|
||||
@Override
|
||||
public RedisPersistentEntity<?> getPersistentEntity(KeyValuePersistentProperty persistentProperty) {
|
||||
return (RedisPersistentEntity<?>) super.getPersistentEntity(persistentProperty);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.context.AbstractMappingContext#getPersistentEntity(org.springframework.data.util.TypeInformation)
|
||||
*/
|
||||
@Override
|
||||
public RedisPersistentEntity<?> getPersistentEntity(TypeInformation<?> type) {
|
||||
return (RedisPersistentEntity<?>) super.getPersistentEntity(type);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext#createPersistentProperty(java.lang.reflect.Field, java.beans.PropertyDescriptor, org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity, org.springframework.data.mapping.model.SimpleTypeHolder)
|
||||
*/
|
||||
@Override
|
||||
protected KeyValuePersistentProperty createPersistentProperty(Field field, PropertyDescriptor descriptor,
|
||||
KeyValuePersistentEntity<?> owner, SimpleTypeHolder simpleTypeHolder) {
|
||||
return new RedisPersistentProperty(field, descriptor, owner, simpleTypeHolder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link MappingConfiguration} used.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
public MappingConfiguration getMappingConfiguration() {
|
||||
return mappingConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link KeySpaceResolver} implementation considering {@link KeySpace} and {@link KeyspaceConfiguration}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
static class ConfigAwareKeySpaceResolver implements KeySpaceResolver {
|
||||
|
||||
private final KeyspaceConfiguration keyspaceConfig;
|
||||
|
||||
public ConfigAwareKeySpaceResolver(KeyspaceConfiguration keyspaceConfig) {
|
||||
|
||||
this.keyspaceConfig = keyspaceConfig;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.core.mapping.KeySpaceResolver#resolveKeySpace(java.lang.Class)
|
||||
*/
|
||||
@Override
|
||||
public String resolveKeySpace(Class<?> type) {
|
||||
|
||||
Assert.notNull(type, "Type must not be null!");
|
||||
if (keyspaceConfig.hasSettingsFor(type)) {
|
||||
|
||||
String value = keyspaceConfig.getKeyspaceSettings(type).getKeyspace();
|
||||
if (StringUtils.hasText(value)) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
return ClassNameKeySpaceResolver.INSTANCE.resolveKeySpace(type);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link KeySpaceResolver} implementation considering {@link KeySpace}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link TimeToLiveAccessor} implementation considering {@link KeyspaceConfiguration}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
static class ConfigAwareTimeToLiveAccessor implements TimeToLiveAccessor {
|
||||
|
||||
private final Map<Class<?>, Long> defaultTimeouts;
|
||||
private final Map<Class<?>, PersistentProperty<?>> timeoutProperties;
|
||||
private final Map<Class<?>, Method> timeoutMethods;
|
||||
private final KeyspaceConfiguration keyspaceConfig;
|
||||
private final RedisMappingContext mappingContext;
|
||||
|
||||
/**
|
||||
* Creates new {@link ConfigAwareTimeToLiveAccessor}
|
||||
*
|
||||
* @param keyspaceConfig must not be {@literal null}.
|
||||
* @param mappingContext must not be {@literal null}.
|
||||
*/
|
||||
ConfigAwareTimeToLiveAccessor(KeyspaceConfiguration keyspaceConfig, RedisMappingContext mappingContext) {
|
||||
|
||||
Assert.notNull(keyspaceConfig, "KeyspaceConfiguration must not be null!");
|
||||
Assert.notNull(mappingContext, "MappingContext must not be null!");
|
||||
|
||||
this.defaultTimeouts = new HashMap<Class<?>, Long>();
|
||||
this.timeoutProperties = new HashMap<Class<?>, PersistentProperty<?>>();
|
||||
this.timeoutMethods = new HashMap<Class<?>, Method>();
|
||||
this.keyspaceConfig = keyspaceConfig;
|
||||
this.mappingContext = mappingContext;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.core.TimeToLiveResolver#resolveTimeToLive(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings({ "rawtypes" })
|
||||
public Long getTimeToLive(final Object source) {
|
||||
|
||||
Assert.notNull(source, "Source must not be null!");
|
||||
Class<?> type = source instanceof Class<?> ? (Class<?>) source : source.getClass();
|
||||
|
||||
Long defaultTimeout = resolveDefaultTimeOut(type);
|
||||
TimeUnit unit = TimeUnit.SECONDS;
|
||||
|
||||
PersistentProperty<?> ttlProperty = resolveTtlProperty(type);
|
||||
|
||||
if (ttlProperty != null) {
|
||||
|
||||
if (ttlProperty.findAnnotation(TimeToLive.class) != null) {
|
||||
unit = ttlProperty.findAnnotation(TimeToLive.class).unit();
|
||||
}
|
||||
|
||||
RedisPersistentEntity entity = mappingContext.getPersistentEntity(type);
|
||||
Long timeout = (Long) entity.getPropertyAccessor(source).getProperty(ttlProperty);
|
||||
if (timeout != null) {
|
||||
return TimeUnit.SECONDS.convert(timeout, unit);
|
||||
}
|
||||
} else {
|
||||
|
||||
Method timeoutMethod = resolveTimeMethod(type);
|
||||
if (timeoutMethod != null) {
|
||||
|
||||
TimeToLive ttl = AnnotationUtils.findAnnotation(timeoutMethod, TimeToLive.class);
|
||||
try {
|
||||
Number timeout = (Number) timeoutMethod.invoke(source);
|
||||
if (timeout != null) {
|
||||
return TimeUnit.SECONDS.convert(timeout.longValue(), ttl.unit());
|
||||
}
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new IllegalStateException("Not allowed to access method '" + timeoutMethod.getName() + "': "
|
||||
+ e.getMessage(), e);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new IllegalStateException("Cannot invoke method '" + timeoutMethod.getName()
|
||||
+ " without arguments': " + e.getMessage(), e);
|
||||
} catch (InvocationTargetException e) {
|
||||
throw new IllegalStateException(
|
||||
"Cannot access method '" + timeoutMethod.getName() + "': " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultTimeout;
|
||||
}
|
||||
|
||||
private Long resolveDefaultTimeOut(Class<?> type) {
|
||||
|
||||
if (this.defaultTimeouts.containsKey(type)) {
|
||||
return defaultTimeouts.get(type);
|
||||
}
|
||||
|
||||
Long defaultTimeout = null;
|
||||
|
||||
if (keyspaceConfig.hasSettingsFor(type)) {
|
||||
defaultTimeout = keyspaceConfig.getKeyspaceSettings(type).getTimeToLive();
|
||||
}
|
||||
|
||||
RedisHash hash = mappingContext.getPersistentEntity(type).findAnnotation(RedisHash.class);
|
||||
if (hash != null && hash.timeToLive() > 0) {
|
||||
defaultTimeout = hash.timeToLive();
|
||||
}
|
||||
|
||||
defaultTimeouts.put(type, defaultTimeout);
|
||||
return defaultTimeout;
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
private PersistentProperty<?> resolveTtlProperty(Class<?> type) {
|
||||
|
||||
if (timeoutProperties.containsKey(type)) {
|
||||
return timeoutProperties.get(type);
|
||||
}
|
||||
|
||||
RedisPersistentEntity entity = mappingContext.getPersistentEntity(type);
|
||||
PersistentProperty<?> ttlProperty = entity.getPersistentProperty(TimeToLive.class);
|
||||
|
||||
if (ttlProperty != null) {
|
||||
|
||||
timeoutProperties.put(type, ttlProperty);
|
||||
return ttlProperty;
|
||||
}
|
||||
|
||||
if (keyspaceConfig.hasSettingsFor(type)) {
|
||||
|
||||
KeyspaceSettings settings = keyspaceConfig.getKeyspaceSettings(type);
|
||||
if (StringUtils.hasText(settings.getTimeToLivePropertyName())) {
|
||||
|
||||
ttlProperty = entity.getPersistentProperty(settings.getTimeToLivePropertyName());
|
||||
timeoutProperties.put(type, ttlProperty);
|
||||
return ttlProperty;
|
||||
}
|
||||
}
|
||||
|
||||
timeoutProperties.put(type, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
private Method resolveTimeMethod(final Class<?> type) {
|
||||
|
||||
if (timeoutMethods.containsKey(type)) {
|
||||
return timeoutMethods.get(type);
|
||||
}
|
||||
|
||||
timeoutMethods.put(type, null);
|
||||
ReflectionUtils.doWithMethods(type, new MethodCallback() {
|
||||
|
||||
@Override
|
||||
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
|
||||
timeoutMethods.put(type, method);
|
||||
}
|
||||
}, new MethodFilter() {
|
||||
|
||||
@Override
|
||||
public boolean matches(Method method) {
|
||||
return ClassUtils.isAssignable(Number.class, method.getReturnType())
|
||||
&& AnnotationUtils.findAnnotation(method, TimeToLive.class) != null;
|
||||
}
|
||||
});
|
||||
|
||||
return timeoutMethods.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2015 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.mapping;
|
||||
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.redis.core.TimeToLiveAccessor;
|
||||
|
||||
/**
|
||||
* Redis specific {@link PersistentEntity}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @param <T>
|
||||
* @since 1.7
|
||||
*/
|
||||
public interface RedisPersistentEntity<T> extends KeyValuePersistentEntity<T> {
|
||||
|
||||
/**
|
||||
* Get the {@link TimeToLiveAccessor} associated with the entity.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
*/
|
||||
TimeToLiveAccessor getTimeToLiveAccessor();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright 2015 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.mapping;
|
||||
|
||||
import java.beans.PropertyDescriptor;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import org.springframework.data.keyvalue.core.mapping.KeyValuePersistentProperty;
|
||||
import org.springframework.data.mapping.PersistentEntity;
|
||||
import org.springframework.data.mapping.PersistentProperty;
|
||||
import org.springframework.data.mapping.model.SimpleTypeHolder;
|
||||
|
||||
/**
|
||||
* Redis specific {@link PersistentProperty} implementation.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisPersistentProperty extends KeyValuePersistentProperty {
|
||||
|
||||
/**
|
||||
* Creates new {@link RedisPersistentProperty}.
|
||||
*
|
||||
* @param field
|
||||
* @param propertyDescriptor
|
||||
* @param owner
|
||||
* @param simpleTypeHolder
|
||||
*/
|
||||
public RedisPersistentProperty(Field field, PropertyDescriptor propertyDescriptor,
|
||||
PersistentEntity<?, KeyValuePersistentProperty> owner, SimpleTypeHolder simpleTypeHolder) {
|
||||
super(field, propertyDescriptor, owner, simpleTypeHolder);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2015 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.listener;
|
||||
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.core.RedisKeyExpiredEvent;
|
||||
|
||||
/**
|
||||
* {@link MessageListener} publishing {@link RedisKeyExpiredEvent}s via {@link ApplicationEventPublisher} by listening
|
||||
* to Redis keyspace notifications for key expirations.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class KeyExpirationEventMessageListener extends KeyspaceEventMessageListener implements
|
||||
ApplicationEventPublisherAware {
|
||||
|
||||
private ApplicationEventPublisher publisher;
|
||||
private static final Topic KEYEVENT_EXPIRED_TOPIC = new PatternTopic("__keyevent@*__:expired");
|
||||
|
||||
/**
|
||||
* Creates new {@link MessageListener} for {@code __keyevent@*__:expired} messages.
|
||||
*
|
||||
* @param listenerContainer must not be {@literal null}.
|
||||
*/
|
||||
public KeyExpirationEventMessageListener(RedisMessageListenerContainer listenerContainer) {
|
||||
super(listenerContainer);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doRegister(org.springframework.data.redis.listener.RedisMessageListenerContainer)
|
||||
*/
|
||||
@Override
|
||||
protected void doRegister(RedisMessageListenerContainer listenerContainer) {
|
||||
listenerContainer.addMessageListener(this, KEYEVENT_EXPIRED_TOPIC);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.listener.KeyspaceEventMessageListener#doHandleMessage(org.springframework.data.redis.connection.Message)
|
||||
*/
|
||||
@Override
|
||||
protected void doHandleMessage(Message message) {
|
||||
publishEvent(new RedisKeyExpiredEvent(message.getBody()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish the event in case an {@link ApplicationEventPublisher} is set.
|
||||
*
|
||||
* @param event can be {@literal null}.
|
||||
*/
|
||||
protected void publishEvent(RedisKeyExpiredEvent event) {
|
||||
|
||||
if (publisher != null && event != null) {
|
||||
this.publisher.publishEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.context.ApplicationEventPublisherAware#setApplicationEventPublisher(org.springframework.context.ApplicationEventPublisher)
|
||||
*/
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.publisher = applicationEventPublisher;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2015 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.listener;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.data.redis.connection.Message;
|
||||
import org.springframework.data.redis.connection.MessageListener;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base {@link MessageListener} implementation for listening to Redis keyspace notifications.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public abstract class KeyspaceEventMessageListener implements MessageListener, InitializingBean, DisposableBean {
|
||||
|
||||
private final RedisMessageListenerContainer listenerContainer;
|
||||
private static final Topic TOPIC_ALL_KEYEVENTS = new PatternTopic("__keyevent@*");
|
||||
|
||||
/**
|
||||
* Creates new {@link KeyspaceEventMessageListener}.
|
||||
*
|
||||
* @param listenerContainer must not be {@literal null}.
|
||||
*/
|
||||
public KeyspaceEventMessageListener(RedisMessageListenerContainer listenerContainer) {
|
||||
|
||||
Assert.notNull(listenerContainer, "RedisMessageListenerContainer to run in must not be null!");
|
||||
this.listenerContainer = listenerContainer;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.redis.connection.MessageListener#onMessage(org.springframework.data.redis.connection.Message, byte[])
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
|
||||
if (message == null || message.getChannel() == null || message.getBody() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
doHandleMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the actual message
|
||||
*
|
||||
* @param message never {@literal null}.
|
||||
*/
|
||||
protected abstract void doHandleMessage(Message message);
|
||||
|
||||
/**
|
||||
* Initialize the message listener by writing requried redis config for {@literal notify-keyspace-events} and
|
||||
* registering the listener within the container.
|
||||
*/
|
||||
public void init() {
|
||||
|
||||
RedisConnection connection = listenerContainer.getConnectionFactory().getConnection();
|
||||
List<String> config = connection.getConfig("notify-keyspace-events");
|
||||
|
||||
if (config.size() == 2) {
|
||||
|
||||
if (!StringUtils.hasText(config.get(1))) {
|
||||
|
||||
// TODO more fine grained reaction on event configuration
|
||||
connection.setConfig("notify-keyspace-events", "KEA");
|
||||
}
|
||||
}
|
||||
connection.close();
|
||||
|
||||
doRegister(listenerContainer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register instance within the container.
|
||||
*
|
||||
* @param container never {@literal null}.
|
||||
*/
|
||||
protected void doRegister(RedisMessageListenerContainer container) {
|
||||
listenerContainer.addMessageListener(this, TOPIC_ALL_KEYEVENTS);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.DisposableBean#destroy()
|
||||
*/
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
listenerContainer.removeMessageListener(this);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
|
||||
*/
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
init();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.configuration;
|
||||
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.context.annotation.ComponentScan.Filter;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.keyvalue.core.KeyValueOperations;
|
||||
import org.springframework.data.keyvalue.repository.config.QueryCreatorType;
|
||||
import org.springframework.data.redis.core.RedisOperations;
|
||||
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
|
||||
import org.springframework.data.redis.core.index.IndexConfiguration;
|
||||
import org.springframework.data.redis.repository.query.RedisQueryCreator;
|
||||
import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
|
||||
/**
|
||||
* Annotation to activate Redis repositories. If no base package is configured through either {@link #value()},
|
||||
* {@link #basePackages()} or {@link #basePackageClasses()} it will trigger scanning of the package of annotated class.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@Import(RedisRepositoriesRegistrar.class)
|
||||
@QueryCreatorType(RedisQueryCreator.class)
|
||||
public @interface EnableRedisRepositories {
|
||||
|
||||
/**
|
||||
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation declarations e.g.:
|
||||
* {@code @EnableJpaRepositories("org.my.pkg")} instead of {@code @EnableJpaRepositories(basePackages="org.my.pkg")}.
|
||||
*/
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* Base packages to scan for annotated components. {@link #value()} is an alias for (and mutually exclusive with) this
|
||||
* attribute. Use {@link #basePackageClasses()} for a type-safe alternative to String-based package names.
|
||||
*/
|
||||
String[] basePackages() default {};
|
||||
|
||||
/**
|
||||
* Type-safe alternative to {@link #basePackages()} for specifying the packages to scan for annotated components. The
|
||||
* package of each class specified will be scanned. Consider creating a special no-op marker class or interface in
|
||||
* each package that serves no purpose other than being referenced by this attribute.
|
||||
*/
|
||||
Class<?>[] basePackageClasses() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are not eligible for component scanning.
|
||||
*/
|
||||
Filter[] excludeFilters() default {};
|
||||
|
||||
/**
|
||||
* Specifies which types are eligible for component scanning. Further narrows the set of candidate components from
|
||||
* everything in {@link #basePackages()} to everything in the base packages that matches the given filter or filters.
|
||||
*/
|
||||
Filter[] includeFilters() default {};
|
||||
|
||||
/**
|
||||
* Returns the postfix to be used when looking up custom repository implementations. Defaults to {@literal Impl}. So
|
||||
* for a repository named {@code PersonRepository} the corresponding implementation class will be looked up scanning
|
||||
* for {@code PersonRepositoryImpl}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String repositoryImplementationPostfix() default "Impl";
|
||||
|
||||
/**
|
||||
* Configures the location of where to find the Spring Data named queries properties file.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String namedQueriesLocation() default "";
|
||||
|
||||
/**
|
||||
* Returns the key of the {@link QueryLookupStrategy} to be used for lookup queries for query methods. Defaults to
|
||||
* {@link Key#CREATE_IF_NOT_FOUND}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Key queryLookupStrategy() default Key.CREATE_IF_NOT_FOUND;
|
||||
|
||||
/**
|
||||
* Returns the {@link FactoryBean} class to be used for each repository instance. Defaults to
|
||||
* {@link RedisRepositoryFactoryBean}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<?> repositoryFactoryBeanClass() default RedisRepositoryFactoryBean.class;
|
||||
|
||||
/**
|
||||
* Configure the repository base class to be used to create repository proxies for this particular configuration.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<?> repositoryBaseClass() default DefaultRepositoryBaseClass.class;
|
||||
|
||||
/**
|
||||
* Configures the name of the {@link KeyValueOperations} bean to be used with the repositories detected.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String keyValueTemplateRef() default "redisKeyValueTemplate";
|
||||
|
||||
/**
|
||||
* Configures whether nested repository-interfaces (e.g. defined as inner classes) should be discovered by the
|
||||
* repositories infrastructure.
|
||||
*/
|
||||
boolean considerNestedRepositories() default false;
|
||||
|
||||
/**
|
||||
* Configures the bean name of the {@link RedisOperations} to be used. Defaulted to {@literal redisTemplate}.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
String redisTemplateRef() default "redisTemplate";
|
||||
|
||||
/**
|
||||
* Set up index patterns using simple configuration class.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<? extends IndexConfiguration> indexConfiguration() default IndexConfiguration.class;
|
||||
|
||||
/**
|
||||
* Set up keyspaces for specific types.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
Class<? extends KeyspaceConfiguration> keyspaceConfiguration() default KeyspaceConfiguration.class;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.configuration;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
|
||||
/**
|
||||
* Redis specific {@link ImportBeanDefinitionRegistrar}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisRepositoriesRegistrar extends RepositoryBeanDefinitionRegistrarSupport {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getAnnotation()
|
||||
*/
|
||||
@Override
|
||||
protected Class<? extends Annotation> getAnnotation() {
|
||||
return EnableRedisRepositories.class;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.config.RepositoryBeanDefinitionRegistrarSupport#getExtension()
|
||||
*/
|
||||
@Override
|
||||
protected RepositoryConfigurationExtension getExtension() {
|
||||
return new RedisRepositoryConfigurationExtension();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.configuration;
|
||||
|
||||
import org.springframework.beans.DirectFieldAccessor;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.ConstructorArgumentValues;
|
||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension;
|
||||
import org.springframework.data.redis.core.RedisKeyValueAdapter;
|
||||
import org.springframework.data.redis.core.RedisKeyValueTemplate;
|
||||
import org.springframework.data.redis.core.convert.CustomConversions;
|
||||
import org.springframework.data.redis.core.convert.MappingConfiguration;
|
||||
import org.springframework.data.redis.core.convert.MappingRedisConverter;
|
||||
import org.springframework.data.redis.core.mapping.RedisMappingContext;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationExtension;
|
||||
import org.springframework.data.repository.config.RepositoryConfigurationSource;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link RepositoryConfigurationExtension} for Redis.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisRepositoryConfigurationExtension extends KeyValueRepositoryConfigurationExtension {
|
||||
|
||||
private static final String REDIS_CONVERTER_BEAN_NAME = "redisConverter";
|
||||
private static final String REDIS_REFERENCE_RESOLVER_BEAN_NAME = "redisReferenceResolver";
|
||||
private static final String REDIS_ADAPTER_BEAN_NAME = "redisKeyValueAdapter";
|
||||
private static final String REDIS_CUSTOM_CONVERSIONS_BEAN_NAME = "redisCustomConversions";
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getModuleName()
|
||||
*/
|
||||
@Override
|
||||
public String getModuleName() {
|
||||
return "Redis";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getModulePrefix()
|
||||
*/
|
||||
@Override
|
||||
protected String getModulePrefix() {
|
||||
return "redis";
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getDefaultKeyValueTemplateRef()
|
||||
*/
|
||||
@Override
|
||||
protected String getDefaultKeyValueTemplateRef() {
|
||||
return "redisKeyValueTemplate";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeansForRoot(BeanDefinitionRegistry registry, RepositoryConfigurationSource configurationSource) {
|
||||
|
||||
RootBeanDefinition mappingContextDefinition = createRedisMappingContext(configurationSource);
|
||||
mappingContextDefinition.setSource(configurationSource.getSource());
|
||||
|
||||
registerIfNotAlreadyRegistered(mappingContextDefinition, registry, MAPPING_CONTEXT_BEAN_NAME, configurationSource);
|
||||
|
||||
// register coustom conversions
|
||||
RootBeanDefinition customConversions = new RootBeanDefinition(CustomConversions.class);
|
||||
registerIfNotAlreadyRegistered(customConversions, registry, REDIS_CUSTOM_CONVERSIONS_BEAN_NAME, configurationSource);
|
||||
|
||||
// Register referenceResolver
|
||||
RootBeanDefinition redisReferenceResolver = createRedisReferenceResolverDefinition();
|
||||
redisReferenceResolver.setSource(configurationSource.getSource());
|
||||
registerIfNotAlreadyRegistered(redisReferenceResolver, registry, REDIS_REFERENCE_RESOLVER_BEAN_NAME,
|
||||
configurationSource);
|
||||
|
||||
// Register converter
|
||||
RootBeanDefinition redisConverterDefinition = createRedisConverterDefinition();
|
||||
redisConverterDefinition.setSource(configurationSource.getSource());
|
||||
|
||||
registerIfNotAlreadyRegistered(redisConverterDefinition, registry, REDIS_CONVERTER_BEAN_NAME, configurationSource);
|
||||
|
||||
// register Adapter
|
||||
RootBeanDefinition redisKeyValueAdapterDefinition = new RootBeanDefinition(RedisKeyValueAdapter.class);
|
||||
|
||||
ConstructorArgumentValues constructorArgumentValuesForRedisKeyValueAdapter = new ConstructorArgumentValues();
|
||||
|
||||
String redisTemplateRef = configurationSource.getAttribute("redisTemplateRef");
|
||||
if (StringUtils.hasText(redisTemplateRef)) {
|
||||
|
||||
constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(0, new RuntimeBeanReference(
|
||||
redisTemplateRef));
|
||||
}
|
||||
|
||||
constructorArgumentValuesForRedisKeyValueAdapter.addIndexedArgumentValue(1, new RuntimeBeanReference(
|
||||
REDIS_CONVERTER_BEAN_NAME));
|
||||
|
||||
redisKeyValueAdapterDefinition.setConstructorArgumentValues(constructorArgumentValuesForRedisKeyValueAdapter);
|
||||
registerIfNotAlreadyRegistered(redisKeyValueAdapterDefinition, registry, REDIS_ADAPTER_BEAN_NAME,
|
||||
configurationSource);
|
||||
|
||||
super.registerBeansForRoot(registry, configurationSource);
|
||||
}
|
||||
|
||||
private RootBeanDefinition createRedisReferenceResolverDefinition() {
|
||||
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition();
|
||||
beanDef.setBeanClassName("org.springframework.data.redis.core.RedisKeyValueAdapter.ReferenceResolverImpl");
|
||||
|
||||
MutablePropertyValues props = new MutablePropertyValues();
|
||||
props.add("adapter", new RuntimeBeanReference(REDIS_ADAPTER_BEAN_NAME));
|
||||
beanDef.setPropertyValues(props);
|
||||
|
||||
return beanDef;
|
||||
}
|
||||
|
||||
private RootBeanDefinition createRedisMappingContext(RepositoryConfigurationSource configurationSource) {
|
||||
|
||||
ConstructorArgumentValues mappingContextArgs = new ConstructorArgumentValues();
|
||||
mappingContextArgs.addIndexedArgumentValue(0, createMappingConfigBeanDef(configurationSource));
|
||||
|
||||
RootBeanDefinition mappingContextBeanDef = new RootBeanDefinition(RedisMappingContext.class);
|
||||
mappingContextBeanDef.setConstructorArgumentValues(mappingContextArgs);
|
||||
|
||||
return mappingContextBeanDef;
|
||||
}
|
||||
|
||||
private BeanDefinition createMappingConfigBeanDef(RepositoryConfigurationSource configurationSource) {
|
||||
|
||||
DirectFieldAccessor dfa = new DirectFieldAccessor(configurationSource);
|
||||
AnnotationAttributes aa = (AnnotationAttributes) dfa.getPropertyValue("attributes");
|
||||
|
||||
GenericBeanDefinition indexConfiguration = new GenericBeanDefinition();
|
||||
indexConfiguration.setBeanClass(aa.getClass("indexConfiguration"));
|
||||
|
||||
GenericBeanDefinition keyspaceConfig = new GenericBeanDefinition();
|
||||
keyspaceConfig.setBeanClass(aa.getClass("keyspaceConfiguration"));
|
||||
|
||||
ConstructorArgumentValues mappingConfigArgs = new ConstructorArgumentValues();
|
||||
mappingConfigArgs.addIndexedArgumentValue(0, indexConfiguration);
|
||||
mappingConfigArgs.addIndexedArgumentValue(1, keyspaceConfig);
|
||||
|
||||
GenericBeanDefinition mappingConfigBeanDef = new GenericBeanDefinition();
|
||||
mappingConfigBeanDef.setBeanClass(MappingConfiguration.class);
|
||||
mappingConfigBeanDef.setConstructorArgumentValues(mappingConfigArgs);
|
||||
|
||||
return mappingConfigBeanDef;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.repository.config.KeyValueRepositoryConfigurationExtension#getDefaultKeyValueTemplateBeanDefinition(org.springframework.data.repository.config.RepositoryConfigurationSource)
|
||||
*/
|
||||
@Override
|
||||
protected AbstractBeanDefinition getDefaultKeyValueTemplateBeanDefinition(
|
||||
RepositoryConfigurationSource configurationSource) {
|
||||
|
||||
RootBeanDefinition keyValueTemplateDefinition = new RootBeanDefinition(RedisKeyValueTemplate.class);
|
||||
|
||||
ConstructorArgumentValues constructorArgumentValuesForKeyValueTemplate = new ConstructorArgumentValues();
|
||||
constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(0, new RuntimeBeanReference(
|
||||
REDIS_ADAPTER_BEAN_NAME));
|
||||
constructorArgumentValuesForKeyValueTemplate.addIndexedArgumentValue(1, new RuntimeBeanReference(
|
||||
MAPPING_CONTEXT_BEAN_NAME));
|
||||
|
||||
keyValueTemplateDefinition.setConstructorArgumentValues(constructorArgumentValuesForKeyValueTemplate);
|
||||
|
||||
return keyValueTemplateDefinition;
|
||||
}
|
||||
|
||||
private RootBeanDefinition createRedisConverterDefinition() {
|
||||
|
||||
RootBeanDefinition beanDef = new RootBeanDefinition();
|
||||
beanDef.setBeanClass(MappingRedisConverter.class);
|
||||
|
||||
ConstructorArgumentValues args = new ConstructorArgumentValues();
|
||||
args.addIndexedArgumentValue(0, new RuntimeBeanReference(MAPPING_CONTEXT_BEAN_NAME));
|
||||
beanDef.setConstructorArgumentValues(args);
|
||||
|
||||
MutablePropertyValues props = new MutablePropertyValues();
|
||||
props.add("referenceResolver", new RuntimeBeanReference(REDIS_REFERENCE_RESOLVER_BEAN_NAME));
|
||||
props.add("customConversions", new RuntimeBeanReference(REDIS_CUSTOM_CONVERSIONS_BEAN_NAME));
|
||||
beanDef.setPropertyValues(props);
|
||||
|
||||
return beanDef;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.query;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Simple set of operations requried to run queries against Redis.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisOperationChain {
|
||||
|
||||
Set<Object> sismember = new LinkedHashSet<Object>();
|
||||
Set<Object> orSismember = new LinkedHashSet<Object>();
|
||||
|
||||
public void sismember(Object next) {
|
||||
sismember.add(next);
|
||||
}
|
||||
|
||||
public Set<Object> getSismember() {
|
||||
return sismember;
|
||||
}
|
||||
|
||||
public void orSismember(Object next) {
|
||||
orSismember.add(next);
|
||||
}
|
||||
|
||||
public void orSismember(Collection<Object> next) {
|
||||
orSismember.addAll(next);
|
||||
}
|
||||
|
||||
public Set<Object> getOrSismember() {
|
||||
return orSismember;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.query;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
|
||||
import org.springframework.data.repository.query.ParameterAccessor;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.data.repository.query.parser.Part;
|
||||
import org.springframework.data.repository.query.parser.PartTree;
|
||||
|
||||
/**
|
||||
* Redis specific query creator.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisQueryCreator extends AbstractQueryCreator<KeyValueQuery<RedisOperationChain>, RedisOperationChain> {
|
||||
|
||||
public RedisQueryCreator(PartTree tree, ParameterAccessor parameters) {
|
||||
super(tree, parameters);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#create(org.springframework.data.repository.query.parser.Part, java.util.Iterator)
|
||||
*/
|
||||
@Override
|
||||
protected RedisOperationChain create(Part part, Iterator<Object> iterator) {
|
||||
return from(part, iterator, new RedisOperationChain());
|
||||
}
|
||||
|
||||
private RedisOperationChain from(Part part, Iterator<Object> iterator, RedisOperationChain sink) {
|
||||
|
||||
switch (part.getType()) {
|
||||
case SIMPLE_PROPERTY:
|
||||
sink.sismember(part.getProperty().toDotPath() + ":" + iterator.next());
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException(part.getType() + "is not supported for redis query derivation");
|
||||
}
|
||||
|
||||
return sink;
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#and(org.springframework.data.repository.query.parser.Part, java.lang.Object, java.util.Iterator)
|
||||
*/
|
||||
@Override
|
||||
protected RedisOperationChain and(Part part, RedisOperationChain base, Iterator<Object> iterator) {
|
||||
return from(part, iterator, base);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#or(java.lang.Object, java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
protected RedisOperationChain or(RedisOperationChain base, RedisOperationChain criteria) {
|
||||
base.orSismember(criteria.sismember);
|
||||
return base;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.parser.AbstractQueryCreator#complete(java.lang.Object, org.springframework.data.domain.Sort)
|
||||
*/
|
||||
@Override
|
||||
protected KeyValueQuery<RedisOperationChain> complete(final RedisOperationChain criteria, Sort sort) {
|
||||
|
||||
KeyValueQuery<RedisOperationChain> query = new KeyValueQuery<RedisOperationChain>(criteria);
|
||||
|
||||
if (sort != null) {
|
||||
query.setSort(sort);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.springframework.data.keyvalue.core.KeyValueOperations;
|
||||
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
|
||||
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery.QueryInitialization;
|
||||
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
|
||||
import org.springframework.data.projection.ProjectionFactory;
|
||||
import org.springframework.data.repository.core.NamedQueries;
|
||||
import org.springframework.data.repository.core.RepositoryMetadata;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
import org.springframework.data.repository.query.EvaluationContextProvider;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy;
|
||||
import org.springframework.data.repository.query.QueryLookupStrategy.Key;
|
||||
import org.springframework.data.repository.query.QueryMethod;
|
||||
import org.springframework.data.repository.query.RepositoryQuery;
|
||||
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link RepositoryFactorySupport} specific of handing Redis
|
||||
* {@link org.springframework.data.keyvalue.repository.KeyValueRepository}.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisRepositoryFactory extends KeyValueRepositoryFactory {
|
||||
|
||||
/**
|
||||
* @param keyValueOperations
|
||||
* @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations)
|
||||
*/
|
||||
public RedisRepositoryFactory(KeyValueOperations keyValueOperations) {
|
||||
super(keyValueOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param keyValueOperations
|
||||
* @param queryCreator
|
||||
* @see KeyValueRepositoryFactory#KeyValueRepositoryFactory(KeyValueOperations, Class)
|
||||
*/
|
||||
public RedisRepositoryFactory(KeyValueOperations keyValueOperations,
|
||||
Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
|
||||
super(keyValueOperations, queryCreator);
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory#getQueryLookupStrategy(org.springframework.data.repository.query.QueryLookupStrategy.Key, org.springframework.data.repository.query.EvaluationContextProvider)
|
||||
*/
|
||||
@Override
|
||||
protected QueryLookupStrategy getQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider) {
|
||||
return new RedisQueryLookupStrategy(key, evaluationContextProvider, getKeyValueOperations(), getQueryCreator());
|
||||
}
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
private static class RedisQueryLookupStrategy implements QueryLookupStrategy {
|
||||
|
||||
private EvaluationContextProvider evaluationContextProvider;
|
||||
private KeyValueOperations keyValueOperations;
|
||||
|
||||
private Class<? extends AbstractQueryCreator<?, ?>> queryCreator;
|
||||
|
||||
/**
|
||||
* Creates a new {@link RedisQueryLookupStrategy} for the given {@link Key}, {@link EvaluationContextProvider},
|
||||
* {@link KeyValueOperations} and query creator type.
|
||||
* <p>
|
||||
*
|
||||
* @param key
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @param keyValueOperations must not be {@literal null}.
|
||||
* @param queryCreator must not be {@literal null}.
|
||||
*/
|
||||
public RedisQueryLookupStrategy(Key key, EvaluationContextProvider evaluationContextProvider,
|
||||
KeyValueOperations keyValueOperations, Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
|
||||
|
||||
Assert.notNull(evaluationContextProvider, "EvaluationContextProvider must not be null!");
|
||||
Assert.notNull(keyValueOperations, "KeyValueOperations must not be null!");
|
||||
Assert.notNull(queryCreator, "Query creator type must not be null!");
|
||||
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
this.keyValueOperations = keyValueOperations;
|
||||
this.queryCreator = queryCreator;
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.query.QueryLookupStrategy#resolveQuery(java.lang.reflect.Method, org.springframework.data.repository.core.RepositoryMetadata, org.springframework.data.repository.core.NamedQueries)
|
||||
*/
|
||||
@Override
|
||||
public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory,
|
||||
NamedQueries namedQueries) {
|
||||
|
||||
QueryMethod queryMethod = new QueryMethod(method, metadata, factory);
|
||||
KeyValuePartTreeQuery partTreeQuery = new KeyValuePartTreeQuery(queryMethod, evaluationContextProvider,
|
||||
this.keyValueOperations, this.queryCreator);
|
||||
partTreeQuery.setQueryIntialization(QueryInitialization.NEW);
|
||||
return partTreeQuery;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2015 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.repository.support;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
|
||||
import org.springframework.data.repository.Repository;
|
||||
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
|
||||
|
||||
/**
|
||||
* Adapter for Springs {@link FactoryBean} interface to allow easy setup of {@link RedisRepositoryFactory} via Spring
|
||||
* configuration.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @param <T> The repository type.
|
||||
* @param <S> The repository domain type.
|
||||
* @param <ID> The repository id type.
|
||||
* @since 1.7
|
||||
*/
|
||||
public class RedisRepositoryFactoryBean<T extends Repository<S, ID>, S, ID extends Serializable> extends
|
||||
KeyValueRepositoryFactoryBean<T, S, ID> {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport#createRepositoryFactory()
|
||||
*/
|
||||
@Override
|
||||
protected RepositoryFactorySupport createRepositoryFactory() {
|
||||
return new RedisRepositoryFactory(getOperations(), getQueryCreator());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2015 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.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Some handy methods for dealing with byte arrays.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 1.7
|
||||
*/
|
||||
public final class ByteUtils {
|
||||
|
||||
private ByteUtils() {}
|
||||
|
||||
public static byte[] concat(byte[] arg1, byte[] arg2) {
|
||||
|
||||
byte[] result = Arrays.copyOf(arg1, arg1.length + arg2.length);
|
||||
System.arraycopy(arg2, 0, result, arg1.length, arg2.length);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] concatAll(byte[]... args) {
|
||||
|
||||
if (args.length == 0) {
|
||||
return new byte[] {};
|
||||
}
|
||||
if (args.length == 1) {
|
||||
return args[0];
|
||||
}
|
||||
|
||||
byte[] cur = concat(args[0], args[1]);
|
||||
for (int i = 2; i < args.length; i++) {
|
||||
cur = concat(cur, args[i]);
|
||||
}
|
||||
return cur;
|
||||
}
|
||||
|
||||
public static byte[][] split(byte[] source, int c) {
|
||||
|
||||
if (source == null || source.length == 0) {
|
||||
return new byte[][] {};
|
||||
}
|
||||
|
||||
List<byte[]> bytes = new ArrayList<byte[]>();
|
||||
int offset = 0;
|
||||
for (int i = 0; i <= source.length; i++) {
|
||||
|
||||
if (i == source.length) {
|
||||
|
||||
bytes.add(Arrays.copyOfRange(source, offset, i));
|
||||
break;
|
||||
}
|
||||
|
||||
if (source[i] == c) {
|
||||
bytes.add(Arrays.copyOfRange(source, offset, i));
|
||||
offset = i + 1;
|
||||
}
|
||||
}
|
||||
return bytes.toArray(new byte[bytes.size()][]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user