/* * Copyright 2013 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.xd.store; import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.TreeSet; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; import org.springframework.data.redis.core.BoundZSetOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.util.Assert; /** * Base implementation for a store, using Redis behind the scenes. This implementation requires a {@code repoPrefix}, * that is used in two ways: * <ul> * <li>a sorted set is stored under that exact key that tracks the entity ids this repository is responsible for, * <li> * <li>each entity is stored serialized under key {@code repoPrefix.<id>}</li> * </ul> * * @param <T> the type of things to store * @param <ID> a "primary key" to the things * @author Eric Bottard */ public abstract class AbstractRedisRepository<T, ID extends Serializable & Comparable<ID>> implements PagingAndSortingRepository<T, ID>, RangeCapableRepository<T, ID> { protected String repoPrefix; protected BoundZSetOperations<String, String> zSetOperations; protected RedisOperations<String, String> redisOperations; public AbstractRedisRepository(String repoPrefix, RedisOperations<String, String> redisOperations) { Assert.hasText(repoPrefix, "repoPrefix must not be empty or null"); Assert.notNull(redisOperations, "redisOperations must not be null"); this.redisOperations = redisOperations; setPrefix(repoPrefix); } @Override public long count() { return zSetOperations.size(); } @Override public void delete(ID id) { if (zSetOperations.remove(redisKeyFromId(id)) == 1) { redisOperations.delete(redisKeyFromId(id)); } } @Override public void delete(Iterable<? extends T> entities) { for (T entity : entities) { delete(keyFor(entity)); } } @Override public void delete(T entity) { // Note, can't call delete due to how it gets subclassed, can lead to stackoverflow. // TODO - investigate ID id = keyFor(entity); if (zSetOperations.remove(redisKeyFromId(id)) == 1) { redisOperations.delete(redisKeyFromId(id)); } } @Override public void deleteAll() { Set<String> keys = zSetOperations.range(0, -1); zSetOperations.removeRange(0, -1); redisOperations.delete(keys); } @Override public boolean exists(ID id) { return redisOperations.hasKey(redisKeyFromId(id)); } @Override public Iterable<T> findAll() { // This set is sorted Set<String> keys = zSetOperations.range(0, -1); Iterator<String> keysIt = keys.iterator(); List<T> result = new ArrayList<T>(keys.size()); List<String> values = redisOperations.opsForValue().multiGet(keys); for (String v : values) { result.add(deserialize(idFromRedisKey(keysIt.next()), v)); } return result; } @Override public Iterable<T> findAll(Iterable<ID> ids) { List<String> redisKeys = new ArrayList<String>(); for (ID id : ids) { redisKeys.add(redisKeyFromId(id)); } Iterator<String> keysIt = redisKeys.iterator(); List<T> result = new ArrayList<T>(redisKeys.size()); List<String> values = redisOperations.opsForValue().multiGet(redisKeys); for (String v : values) { result.add(deserialize(idFromRedisKey(keysIt.next()), v)); } return result; } @Override public Page<T> findAll(Pageable pageable) { Assert.isNull(pageable.getSort(), "Arbitrary sorting is not implemented"); long count = zSetOperations.size(); // redis in inclusive on right side, hence -1 long to = Math.min(count, pageable.getOffset() + pageable.getPageSize()) - 1; // But -1 means start from end, so cater for that Set<String> redisKeys = (to == -1) ? Collections.<String> emptySet() : zSetOperations.range( pageable.getOffset(), to); Iterator<String> keysIt = redisKeys.iterator(); List<T> result = new ArrayList<T>(redisKeys.size()); List<String> values = redisOperations.opsForValue().multiGet(redisKeys); for (String v : values) { result.add(deserialize(idFromRedisKey(keysIt.next()), v)); } return new PageImpl<T>(result, pageable, count); } @Override public Iterable<T> findAll(Sort sort) { throw new UnsupportedOperationException("Can't sort on arbitrary property"); } @Override public T findOne(ID id) { String redisKey = redisKeyFromId(id); String raw = redisOperations.opsForValue().get(redisKey); if (raw != null) { return deserialize(idFromRedisKey(redisKey), raw); } else { return null; } } @Override public <S extends T> S save(S entity) { String raw = serialize(entity); String redisKey = redisKeyFromId(keyFor(entity)); trackMembership(redisKey); redisOperations.opsForValue().set(redisKey, raw); return entity; } @Override public Iterable<T> findAllInRange(ID from, boolean fromInclusive, ID to, boolean toInclusive) { Set<String> keys = zSetOperations.range(0, -1); String fromRedis = redisKeyFromId(from); String toRedis = redisKeyFromId(to); Set<String> subSet = new TreeSet<String>(keys).subSet(fromRedis, fromInclusive, toRedis, toInclusive); Iterator<String> keysIt = subSet.iterator(); List<T> result = new ArrayList<T>(subSet.size()); List<String> values = redisOperations.opsForValue().multiGet(subSet); for (String v : values) { result.add(deserialize(idFromRedisKey(keysIt.next()), v)); } return result; } /** * Perform bookkeeping of entities managed by this repository. Uses a redis sorted set with a dummy value, which * happens to guarantee that keys for a given score are in sorted order. */ protected void trackMembership(String redisKey) { zSetOperations.add(redisKey, 0.0D); } @Override public <S extends T> Iterable<S> save(Iterable<S> entities) { for (S entity : entities) { save(entity); } return entities; } /** * Deserialize from the String representation to the domain object. * * @param id the entity id * @param v the serialized representation of the domain object */ protected abstract T deserialize(ID id, String v); /** * Provide a String representation of the domain entity. */ protected abstract String serialize(T entity); /** * Return the entity id for the given domain object. */ protected abstract ID keyFor(T entity); /** * Return a String representation of the domain ID. Note that order should be preserved between ID domain and String * representation. */ protected abstract String serializeId(ID id); /** * Deserialize an entity id from its String representation. */ protected abstract ID deserializeId(String string); protected String redisKeyFromId(ID id) { Assert.notNull(id); return repoPrefix + "." + serializeId(id); } protected ID idFromRedisKey(String redisKey) { // + 1 is for "." length String serialized = redisKey.substring(repoPrefix.length() + 1); return deserializeId(serialized); } /** * Change the prefix after creation. Mainly intented for testing. */ public void setPrefix(String string) { this.repoPrefix = string; this.zSetOperations = redisOperations.boundZSetOps(repoPrefix); } protected String getPrefix() { return repoPrefix; } }