package org.infinispan.interceptors.compat;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Spliterator;
import java.util.stream.StreamSupport;
import org.infinispan.Cache;
import org.infinispan.CacheSet;
import org.infinispan.CacheStream;
import org.infinispan.cache.impl.Caches;
import org.infinispan.commands.FlagAffectedCommand;
import org.infinispan.commands.MetadataAwareCommand;
import org.infinispan.commands.read.EntrySetCommand;
import org.infinispan.commands.read.GetAllCommand;
import org.infinispan.commands.read.GetCacheEntryCommand;
import org.infinispan.commands.read.GetKeyValueCommand;
import org.infinispan.commands.read.KeySetCommand;
import org.infinispan.commands.write.PutKeyValueCommand;
import org.infinispan.commands.write.PutMapCommand;
import org.infinispan.commands.write.RemoveCommand;
import org.infinispan.commands.write.ReplaceCommand;
import org.infinispan.commons.util.CloseableIterator;
import org.infinispan.commons.util.CloseableIteratorMapper;
import org.infinispan.commons.util.CloseableSpliterator;
import org.infinispan.compat.TypeConverter;
import org.infinispan.container.InternalEntryFactory;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.container.entries.ForwardingCacheEntry;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.container.versioning.VersionGenerator;
import org.infinispan.context.InvocationContext;
import org.infinispan.distribution.DistributionManager;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.interceptors.DDAsyncInterceptor;
import org.infinispan.metadata.Metadata;
import org.infinispan.stream.impl.interceptor.AbstractDelegatingEntryCacheSet;
import org.infinispan.stream.impl.interceptor.AbstractDelegatingKeyCacheSet;
import org.infinispan.stream.impl.local.EntryStreamSupplier;
import org.infinispan.stream.impl.local.KeyStreamSupplier;
import org.infinispan.stream.impl.local.LocalCacheStream;
import org.infinispan.stream.impl.spliterators.IteratorAsSpliterator;
/**
* Base implementation for an interceptor that applies type conversion to the data stored in the cache. Subclasses need
* to provide a suitable TypeConverter.
*
* @author Galder ZamarreƱo
*/
public abstract class BaseTypeConverterInterceptor<K, V> extends DDAsyncInterceptor {
protected InternalEntryFactory entryFactory;
protected VersionGenerator versionGenerator;
protected Cache<K, V> cache;
@Inject
protected void init(InternalEntryFactory entryFactory, VersionGenerator versionGenerator, Cache<K, V> cache) {
this.entryFactory = entryFactory;
this.versionGenerator = versionGenerator;
this.cache = cache;
}
/**
* Subclasses need to return a TypeConverter instance that is appropriate for a cache operation with the specified flags.
*
* @param command the command for the current cache operation
* @return the converter, never {@code null}
*/
protected abstract TypeConverter<Object, Object, Object, Object> determineTypeConverter(FlagAffectedCommand command);
@Override
public Object visitPutKeyValueCommand(InvocationContext ctx, PutKeyValueCommand command) throws Throwable {
if (!ctx.isOriginLocal()) {
return super.visitPutKeyValueCommand(ctx, command);
}
Object key = command.getKey();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
command.setKey(converter.boxKey(key));
command.setValue(converter.boxValue(command.getValue()));
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> converter.unboxValue(rv));
}
@Override
public Object visitPutMapCommand(InvocationContext ctx, PutMapCommand command) throws Throwable {
if (ctx.isOriginLocal()) {
Map<Object, Object> map = command.getMap();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
Map<Object, Object> convertedMap = new HashMap<>(map.size());
for (Entry<Object, Object> entry : map.entrySet()) {
convertedMap.put(converter.boxKey(entry.getKey()), converter.boxValue(entry.getValue()));
}
command.setMap(convertedMap);
}
// There is no return value for putAll so nothing to convert
return invokeNext(ctx, command);
}
@Override
public Object visitGetKeyValueCommand(InvocationContext ctx, GetKeyValueCommand command) throws Throwable {
if (!ctx.isOriginLocal()) return invokeNext(ctx, command);
Object key = command.getKey();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
command.setKey(converter.boxKey(key));
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
if (rv == null) {
return rv;
}
return converter.unboxValue(rv);
});
}
@Override
public Object visitGetCacheEntryCommand(InvocationContext ctx, GetCacheEntryCommand command)
throws Throwable {
if (!ctx.isOriginLocal()) return invokeNext(ctx, command);
Object key = command.getKey();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
command.setKey(converter.boxKey(key));
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
if (rv == null) {
return rv;
}
CacheEntry entry = (CacheEntry) rv;
Object returnValue = converter.unboxValue(entry.getValue());
// Create a copy of the entry to avoid modifying the internal entry
return entryFactory.create(entry.getKey(), returnValue, entry.getMetadata(), entry.getLifespan(),
entry.getMaxIdle());
});
}
@Override
public Object visitGetAllCommand(InvocationContext ctx, GetAllCommand command) throws Throwable {
if (!ctx.isOriginLocal()) return invokeNext(ctx, command);
Collection<?> keys = command.getKeys();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
Set<Object> boxedKeys = new LinkedHashSet<>(keys.size());
for (Object key : keys) {
boxedKeys.add(converter.boxKey(key));
}
command.setKeys(boxedKeys);
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
if (rv == null) {
return null;
}
if (command.isReturnEntries()) {
Map<Object, CacheEntry> map = (Map<Object, CacheEntry>) rv;
Map<Object, Object> unboxed = command.createMap();
for (Entry<Object, CacheEntry> entry : map.entrySet()) {
CacheEntry cacheEntry = entry.getValue();
if (cacheEntry == null) {
unboxed.put(entry.getKey(), null);
} else {
unboxed.put(converter.unboxKey(entry.getKey()), entryFactory
.create(entry.getKey(), converter.unboxValue(cacheEntry.getValue()),
cacheEntry.getMetadata(), cacheEntry.getLifespan(),
cacheEntry.getMaxIdle()));
}
}
return unboxed;
} else {
Map<Object, Object> map = (Map<Object, Object>) rv;
Map<Object, Object> unboxed = command.createMap();
for (Entry<Object, Object> entry : map.entrySet()) {
Object value = entry.getValue();
unboxed.put(converter.unboxKey(entry.getKey()), value == null ? null : converter.unboxValue(value));
}
return unboxed;
}
});
}
@Override
public Object visitReplaceCommand(InvocationContext ctx, ReplaceCommand command) throws Throwable {
if (!ctx.isOriginLocal()) {
return super.visitReplaceCommand(ctx, command);
}
Object key = command.getKey();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
Object oldValue = command.getOldValue();
command.setKey(converter.boxKey(key));
command.setOldValue(converter.boxValue(oldValue));
command.setNewValue(converter.boxValue(command.getNewValue()));
addVersionIfNeeded(command);
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
// Return of conditional replace is not the value type, but boolean, so
// apply an exception that applies to all servers, regardless of what's
// stored in the value side
if (oldValue != null && rv instanceof Boolean) return rv;
return converter.unboxValue(rv);
});
}
protected void addVersionIfNeeded(MetadataAwareCommand cmd) {
Metadata metadata = cmd.getMetadata();
if (metadata.version() == null) {
Metadata newMetadata = metadata.builder().version(versionGenerator.generateNew()).build();
cmd.setMetadata(newMetadata);
}
}
@Override
public Object visitRemoveCommand(InvocationContext ctx, RemoveCommand command) throws Throwable {
if (!ctx.isOriginLocal()) {
return super.visitRemoveCommand(ctx, command);
}
Object key = command.getKey();
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(command);
Object conditionalValue = command.getValue();
command.setKey(converter.boxKey(key));
command.setValue(converter.boxValue(conditionalValue));
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
// Return of conditional remove is not the value type, but boolean, so
// apply an exception that applies to all servers, regardless of what's
// stored in the value side
if (conditionalValue != null && rv instanceof Boolean) return rv;
return ctx.isOriginLocal() ? converter.unboxValue(rv) : rv;
});
}
@Override
public Object visitKeySetCommand(InvocationContext ctx, KeySetCommand command) throws Throwable {
if (!ctx.isOriginLocal()) {
return invokeNext(ctx, command);
}
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
KeySetCommand keySetCommand = (KeySetCommand) rCommand;
CacheSet<K> set = (CacheSet<K>) rv;
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(keySetCommand);
return new AbstractDelegatingKeyCacheSet<K, V>(Caches.getCacheWithFlags(cache, keySetCommand), set) {
@Override
public CloseableIterator<K> iterator() {
return new CloseableIteratorMapper<>(super.iterator(), k -> (K) converter.unboxKey(k));
}
@Override
public CloseableSpliterator<K> spliterator() {
return new IteratorAsSpliterator.Builder<>(iterator()).setEstimateRemaining(super.spliterator().estimateSize()).setCharacteristics(
Spliterator.CONCURRENT | Spliterator.DISTINCT | Spliterator.NONNULL).get();
}
@Override
protected CacheStream<K> getStream(boolean parallel) {
DistributionManager dm = cache.getAdvancedCache().getDistributionManager();
// Note the stream has to deal with the boxed values - so we can't use our spliterator
// as it already
// unboxes them
CloseableSpliterator<K> closeableSpliterator = super.spliterator();
CacheStream<K> stream = new LocalCacheStream<>(
new KeyStreamSupplier<>(cache, dm != null ? dm.getWriteConsistentHash() : null,
() -> StreamSupport.stream(closeableSpliterator, parallel)), parallel,
cache.getAdvancedCache().getComponentRegistry());
// We rely on the fact that on close returns the same instance
stream.onClose(closeableSpliterator::close);
return new TypeConverterStream(stream, converter, entryFactory);
}
};
});
}
@Override
public Object visitEntrySetCommand(InvocationContext ctx, EntrySetCommand command) throws Throwable {
if (!ctx.isOriginLocal()) return invokeNext(ctx, command);
return invokeNextThenApply(ctx, command, (rCtx, rCommand, rv) -> {
CacheSet<CacheEntry<K, V>> set = (CacheSet<CacheEntry<K, V>>) rv;
EntrySetCommand entrySetCommand = (EntrySetCommand) rCommand;
TypeConverter<Object, Object, Object, Object> converter = determineTypeConverter(entrySetCommand);
return new AbstractDelegatingEntryCacheSet<K, V>(Caches.getCacheWithFlags(cache, command), set) {
@Override
public CloseableIterator<CacheEntry<K, V>> iterator() {
return new TypeConverterIterator<>(super.iterator(), converter, entryFactory);
}
@Override
public CloseableSpliterator<CacheEntry<K, V>> spliterator() {
return new IteratorAsSpliterator.Builder<>(iterator()).setEstimateRemaining(super.spliterator().estimateSize()).setCharacteristics(
Spliterator.CONCURRENT | Spliterator.DISTINCT | Spliterator.NONNULL).get();
}
@Override
protected CacheStream<CacheEntry<K, V>> getStream(boolean parallel) {
DistributionManager dm = cache.getAdvancedCache().getDistributionManager();
// Note the stream has to deal with the boxed values - so we can't use our spliterator
// as it already
// unboxes them
CloseableSpliterator<CacheEntry<K, V>> closeableSpliterator = super.spliterator();
CacheStream<CacheEntry<K, V>> stream = new LocalCacheStream<>(
new EntryStreamSupplier<>(cache, dm != null ? dm.getWriteConsistentHash() : null,
() -> StreamSupport.stream(closeableSpliterator, parallel)), parallel,
cache.getAdvancedCache().getComponentRegistry());
// We rely on the fact that on close returns the same instance
stream.onClose(closeableSpliterator::close);
return new TypeConverterStream(stream, converter, entryFactory);
}
};
});
}
private static <K, V> CacheEntry<K, V> convert(CacheEntry<K, V> entry,
TypeConverter<Object, Object, Object, Object> converter, InternalEntryFactory entryFactory) {
K newKey = (K) converter.unboxKey(entry.getKey());
V newValue = (V) converter.unboxValue(entry.getValue());
// If either value changed then make a copy
if (newKey != entry.getKey() || newValue != entry.getValue()) {
if (entry instanceof InternalCacheEntry) {
return entryFactory.create(newKey, newValue, (InternalCacheEntry) entry);
}
return entryFactory.create(newKey, newValue, entry.getMetadata());
}
return entry;
}
/**
* Wrapper for CacheEntry(s) that can be used to update the cache when it's value is set.
* @param <K> The key type
* @param <V> The value type
*/
private static class EntryWrapper<K, V> extends ForwardingCacheEntry<K, V> {
private final CacheEntry<K, V> previousEntry;
private final CacheEntry<K, V> entry;
public EntryWrapper(CacheEntry<K, V> previousEntry, CacheEntry<K, V> entry) {
this.previousEntry = previousEntry;
this.entry = entry;
}
@Override
protected CacheEntry<K, V> delegate() {
return entry;
}
@Override
public V setValue(V value) {
previousEntry.setValue(value);
return super.setValue(value);
}
}
public static class TypeConverterIterator<K, V> implements CloseableIterator<CacheEntry<K, V>> {
private final CloseableIterator<CacheEntry<K, V>> iterator;
private final TypeConverter<Object, Object, Object, Object> converter;
private final InternalEntryFactory entryFactory;
public TypeConverterIterator(CloseableIterator<CacheEntry<K, V>> iterator,
TypeConverter<Object, Object, Object, Object> converter,
InternalEntryFactory entryFactory) {
this.iterator = iterator;
this.converter = converter;
this.entryFactory = entryFactory;
}
@Override
public void close() {
iterator.close();
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public CacheEntry<K, V> next() {
CacheEntry<K, V> entry = iterator.next();
return new EntryWrapper<>(entry, convert(entry, converter, entryFactory));
}
@Override
public void remove() {
iterator.remove();
}
}
}