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:
Christoph Strobl
2015-08-31 15:02:40 +02:00
parent 5a85ac47a3
commit bd7728e6ad
57 changed files with 9911 additions and 6 deletions

View File

@@ -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[]>();

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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() + "]";
}
}

View File

@@ -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>
* &#64;RedisHash("persons")
* class Person {
* &#64;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);
}
}
}

View File

@@ -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&#60;RedisSession&#62; sessions = template.find(new RedisCallback&#60;Set&#60;byte[]&#62;&#62;() {
* public Set&#60;byte[]&#60; 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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>
* &#64;RedisHash
* class Person {
* &#64;Id String id;
* String name;
* &#64;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;
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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>
* &#64;RedisHash("persons")
* class Person {
*
* &#64;Id String id;
* String firstname;
*
* List<String> nicknames;
* List<Person> coworkers;
*
* Address address;
* &#64;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();
}
}
}

View File

@@ -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> {
}

View File

@@ -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 + "]";
}
}

View File

@@ -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);
}

View File

@@ -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 + "]";
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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()][]);
}
}