/*
* Copyright Terracotta, Inc.
*
* 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.ehcache.core;
import org.ehcache.Cache;
import org.ehcache.config.CacheConfiguration;
import org.ehcache.core.config.BaseCacheConfiguration;
import org.ehcache.core.config.ResourcePoolsHelper;
import org.ehcache.core.events.CacheEventDispatcher;
import org.ehcache.core.exceptions.StorePassThroughException;
import org.ehcache.core.spi.store.Store;
import org.ehcache.core.spi.store.events.StoreEventSource;
import org.ehcache.spi.loaderwriter.BulkCacheLoadingException;
import org.ehcache.spi.loaderwriter.BulkCacheWritingException;
import org.ehcache.core.spi.store.StoreAccessException;
import org.ehcache.core.spi.function.BiFunction;
import org.ehcache.core.spi.function.Function;
import org.ehcache.core.spi.function.NullaryFunction;
import org.ehcache.core.internal.resilience.ResilienceStrategy;
import org.ehcache.spi.loaderwriter.CacheLoaderWriter;
import org.hamcrest.Description;
import org.hamcrest.Factory;
import org.hamcrest.Matcher;
import org.hamcrest.TypeSafeMatcher;
import org.junit.Before;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.terracotta.context.ContextManager;
import org.terracotta.context.TreeNode;
import org.terracotta.statistics.OperationStatistic;
import org.terracotta.statistics.ValueStatistic;
import java.lang.reflect.Field;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.spy;
/**
* Provides testing of basic CRUD operations on an {@code Ehcache}.
*
* @author Clifford W. Johnson
*/
public abstract class EhcacheBasicCrudBase {
protected static final CacheConfiguration<String, String> CACHE_CONFIGURATION =
new BaseCacheConfiguration<String, String>(String.class, String.class, null,
null, null, ResourcePoolsHelper.createHeapOnlyPools());
@Mock
protected Store<String, String> store;
@Mock
protected CacheEventDispatcher<String, String> cacheEventDispatcher;
/**
* Holds a {@link org.mockito.Mockito#spy(Object)}-wrapped reference to the
* {@link ResilienceStrategy ResilienceStrategy} used in the
* {@link EhcacheWithLoaderWriter Ehcache} instance being tested.
*
* @see #setResilienceStrategySpy(InternalCache)
*/
protected ResilienceStrategy<String, String> spiedResilienceStrategy;
@Before
public void initMocks() {
MockitoAnnotations.initMocks(this);
}
/**
* Validates expected {@link org.terracotta.statistics.OperationStatistic} updates for the
* indicated {@code InternalCache} instance. The statistics identified in {@code changed} are
* checked for a value of {@code 1}; all other statistics in the same enumeration class are
* checked for a value of {@code 0}.
*
* @param ehcache the {@code InternalCache} instance to check
* @param changed the statistics values that should have updated values
* @param <E> the statistics enumeration type
*/
protected static <E extends Enum<E>> void validateStats(final InternalCache<?, ?> ehcache, final EnumSet<E> changed) {
assert changed != null;
final EnumSet<E> unchanged = EnumSet.complementOf(changed);
@SuppressWarnings("unchecked")
final List<EnumSet<E>> sets = Arrays.asList(changed, unchanged);
Class<E> statsClass = null;
for (final EnumSet<E> set : sets) {
if (!set.isEmpty()) {
statsClass = set.iterator().next().getDeclaringClass();
break;
}
}
assert statsClass != null;
final OperationStatistic<E> operationStatistic = getOperationStatistic(ehcache, statsClass);
for (final E statId : changed) {
assertThat(String.format("Value for %s.%s", statId.getDeclaringClass().getName(), statId.name()),
getStatistic(operationStatistic, statId), StatisticMatcher.equalTo(1L));
}
for (final E statId : unchanged) {
assertThat(String.format("Value for %s.%s", statId.getDeclaringClass().getName(), statId.name()),
getStatistic(operationStatistic, statId), StatisticMatcher.equalTo(0L));
}
}
/**
* Gets a reference to the {@link org.terracotta.statistics.OperationStatistic} instance holding the
* class of statistics specified for the {@code InternalCache} instance provided.
*
* @param ehcache the {@code InternalCache} instance for which the {@code OperationStatistic} instance
* should be obtained
* @param statsClass the {@code Class} of statistics for which the {@code OperationStatistic} instance
* should be obtained
* @param <E> the {@code Enum} type for the statistics
*
* @return a reference to the {@code OperationStatistic} instance holding the {@code statsClass} statistics;
* may be {@code null} if {@code statsClass} statistics do not exist for {@code ehcache}
*/
private static <E extends Enum<E>> OperationStatistic<E> getOperationStatistic(final InternalCache<?, ?> ehcache, final Class<E> statsClass) {
for (final TreeNode statNode : ContextManager.nodeFor(ehcache).getChildren()) {
final Object statObj = statNode.getContext().attributes().get("this");
if (statObj instanceof OperationStatistic<?>) {
@SuppressWarnings("unchecked")
final OperationStatistic<E> statistic = (OperationStatistic<E>)statObj;
if (statistic.type().equals(statsClass)) {
return statistic;
}
}
}
return null;
}
/**
* Gets the value of the statistic indicated from an {@link org.terracotta.statistics.OperationStatistic}
* instance.
*
* @param operationStatistic the {@code OperationStatistic} instance from which the statistic is to
* be obtained
* @param statId the {@code Enum} constant identifying the statistic for which the value must be obtained
* @param <E> The {@code Enum} type for the statistics
*
* @return the value, possibly null, for {@code statId} about {@code ehcache}
*/
private static <E extends Enum<E>> Number getStatistic(final OperationStatistic<E> operationStatistic, final E statId) {
if (operationStatistic != null) {
final ValueStatistic<Long> valueStatistic = operationStatistic.statistic(statId);
return (valueStatistic == null ? null : valueStatistic.value());
}
return null;
}
/**
* Returns a Mockito {@code any} Matcher for {@link Function}.
*
* @return a Mockito {@code any} matcher for {@code Function}.
*/
@SuppressWarnings("unchecked")
protected static Function<? super String, ? extends String> getAnyFunction() {
return any(Function.class); // unchecked
}
/**
* Returns a Mockito {@code any} Matcher for {@link BiFunction}.
*
* @return a Mockito {@code any} matcher for {@code BiFunction}.
*/
@SuppressWarnings("unchecked")
protected static BiFunction<? super String, String, String> getAnyBiFunction() {
return any(BiFunction.class); // unchecked
}
/**
* Returns a Mockito {@code any} Matcher for {@link NullaryFunction NullaryFunction<Boolean>}.
*
* @return a Mockito {@code any} matcher for {@code NullaryFunction}.
*/
@SuppressWarnings("unchecked")
protected static NullaryFunction<Boolean> getBooleanNullaryFunction() {
return any(NullaryFunction.class); // unchecked
}
/**
* Replaces the {@link ResilienceStrategy ResilienceStrategy} instance in the
* {@link InternalCache Ehcache} instance provided with a
* {@link org.mockito.Mockito#spy(Object) Mockito <code>spy</code>} wrapping the original
* {@code ResilienceStrategy} instance.
*
* @param ehcache the {@code InternalCache} instance to alter
*
* @return the <code>spy</code>-wrapped {@code ResilienceStrategy} instance
*/
protected final <K, V> ResilienceStrategy<K, V> setResilienceStrategySpy(final InternalCache<K, V> ehcache) {
assert ehcache != null;
try {
final Field resilienceStrategyField = ehcache.getClass().getDeclaredField("resilienceStrategy");
resilienceStrategyField.setAccessible(true);
@SuppressWarnings("unchecked")
ResilienceStrategy<K, V> resilienceStrategy = (ResilienceStrategy<K, V>)resilienceStrategyField.get(ehcache);
if (resilienceStrategy != null) {
resilienceStrategy = spy(resilienceStrategy);
resilienceStrategyField.set(ehcache, resilienceStrategy);
}
return resilienceStrategy;
} catch (Exception e) {
throw new AssertionError(String.format("Unable to wrap ResilienceStrategy in Ehcache instance: %s", e));
}
}
/**
* Provides a basic {@link Store} implementation for testing.
* The contract implemented by this {@code Store} is not strictly conformant but
* should be sufficient for {@code Ehcache} implementation testing.
*/
protected static class FakeStore implements Store<String, String> {
private final CacheConfigurationChangeListener cacheConfigurationChangeListener = new CacheConfigurationChangeListener() {
@Override
public void cacheConfigurationChange(CacheConfigurationChangeEvent event) {
// noop
}
};
private static final NullaryFunction<Boolean> REPLACE_EQUAL_TRUE = new NullaryFunction<Boolean>() {
@Override
public Boolean apply() {
return true;
}
};
/**
* The key:value pairs served by this {@code Store}. This map may be empty.
*/
private final Map<String, FakeValueHolder> entries;
/**
* Keys for which access results in a thrown {@code Exception}. This set may be empty.
*/
private final Set<String> failingKeys;
public FakeStore(final Map<String, String> entries) {
this(entries, Collections.<String>emptySet());
}
public FakeStore(final Map<String, String> entries, final Set<String> failingKeys) {
assert failingKeys != null;
// Use of ConcurrentHashMap is required to avoid ConcurrentModificationExceptions using Iterator.remove
this.entries = new ConcurrentHashMap<String, FakeValueHolder>();
if (entries != null) {
for (final Map.Entry<String, String> entry : entries.entrySet()) {
this.entries.put(entry.getKey(), new FakeValueHolder(entry.getValue()));
}
}
this.failingKeys = Collections.unmodifiableSet(new HashSet<String>(failingKeys));
}
/**
* Gets a mapping of the entries in this {@code Store}.
*
* @return a new, unmodifiable map of the entries in this {@code Store}.
*/
protected Map<String, String> getEntryMap() {
final Map<String, String> result = new HashMap<String, String>();
for (final Map.Entry<String, FakeValueHolder> entry : this.entries.entrySet()) {
result.put(entry.getKey(), entry.getValue().value());
}
return Collections.unmodifiableMap(result);
}
@Override
public ValueHolder<String> get(final String key) throws StoreAccessException {
this.checkFailingKey(key);
final FakeValueHolder valueHolder = this.entries.get(key);
if (valueHolder != null) {
valueHolder.lastAccessTime = System.currentTimeMillis();
}
return valueHolder;
}
@Override
public boolean containsKey(final String key) throws StoreAccessException {
this.checkFailingKey(key);
return this.entries.containsKey(key);
}
@Override
public PutStatus put(final String key, final String value) throws StoreAccessException {
this.checkFailingKey(key);
FakeValueHolder toPut = new FakeValueHolder(value);
if (this.entries.put(key, toPut) != null) {
return PutStatus.UPDATE;
}
return PutStatus.PUT;
}
@Override
public ValueHolder<String> putIfAbsent(final String key, final String value) throws StoreAccessException {
this.checkFailingKey(key);
final FakeValueHolder currentValue = this.entries.get(key);
if (currentValue == null) {
this.entries.put(key, new FakeValueHolder(value));
return null;
}
currentValue.lastAccessTime = System.currentTimeMillis();
return currentValue;
}
@Override
public boolean remove(final String key) throws StoreAccessException {
this.checkFailingKey(key);
if (this.entries.remove(key) == null) {
return false;
}
return true;
}
@Override
public RemoveStatus remove(final String key, final String value) throws StoreAccessException {
this.checkFailingKey(key);
final ValueHolder<String> currentValue = this.entries.get(key);
if (currentValue == null) {
return RemoveStatus.KEY_MISSING;
} else if (!currentValue.value().equals(value)) {
return RemoveStatus.KEY_PRESENT;
}
this.entries.remove(key);
return RemoveStatus.REMOVED;
}
@Override
public ValueHolder<String> replace(final String key, final String value) throws StoreAccessException {
this.checkFailingKey(key);
final ValueHolder<String> currentValue = this.entries.get(key);
if (currentValue != null) {
this.entries.put(key, new FakeValueHolder(value));
}
return currentValue;
}
@Override
public ReplaceStatus replace(final String key, final String oldValue, final String newValue) throws StoreAccessException {
this.checkFailingKey(key);
final ValueHolder<String> currentValue = this.entries.get(key);
if (currentValue == null) {
return ReplaceStatus.MISS_NOT_PRESENT;
}
if (!currentValue.value().equals(oldValue)) {
return ReplaceStatus.MISS_PRESENT;
}
this.entries.put(key, new FakeValueHolder(newValue));
return ReplaceStatus.HIT;
}
@Override
public void clear() throws StoreAccessException {
this.entries.clear();
}
@Override
public StoreEventSource<String, String> getStoreEventSource() {
throw new UnsupportedOperationException("TODO Implement me!");
}
/**
* {@inheritDoc}
* <p>
* The {@code Iterator} returned by this method <b>does not</b> have a {@code remove}
* method. The {@code Iterator} returned by {@code FakeStore.this.entries.entrySet().iterator()}
* must not throw {@link java.util.ConcurrentModificationException ConcurrentModification}.
*/
@Override
public Iterator<Cache.Entry<String, ValueHolder<String>>> iterator() {
return new Iterator<Cache.Entry<String, ValueHolder<String>>>() {
final java.util.Iterator<Map.Entry<String, FakeValueHolder>> iterator =
FakeStore.this.entries.entrySet().iterator();
@Override
public boolean hasNext() {
return this.iterator.hasNext();
}
@Override
public Cache.Entry<String, ValueHolder<String>> next() throws StoreAccessException {
final Map.Entry<String, FakeValueHolder> cacheEntry = this.iterator.next();
FakeStore.this.checkFailingKey(cacheEntry.getKey());
cacheEntry.getValue().lastAccessTime = System.currentTimeMillis();
return new Cache.Entry<String, ValueHolder<String>>() {
@Override
public String getKey() {
return cacheEntry.getKey();
}
@Override
public ValueHolder<String> getValue() {
return cacheEntry.getValue();
}
};
}
};
}
/**
* {@inheritDoc}
* <p>
* This method is implemented as
* <code>this.{@link #compute(String, BiFunction, NullaryFunction) compute}(keys, mappingFunction, () -> { returns true; })</code>
*/
@Override
public ValueHolder<String> compute(final String key, final BiFunction<? super String, ? super String, ? extends String> mappingFunction)
throws StoreAccessException {
return this.compute(key, mappingFunction, REPLACE_EQUAL_TRUE);
}
/**
* Common core for the {@link #compute(String, BiFunction, NullaryFunction)} method.
*
* @param key the key of the entry to process
* @param currentValue the existing value, if any, for {@code key}
* @param mappingFunction the function that will produce the value. The function will be supplied
* with the key and existing value (or null if no entry exists) as parameters. The function should
* return the desired new value for the entry or null to remove the entry. If the method throws
* an unchecked exception the Store will not be modified (the caller will receive the exception)
* @param replaceEqual If the existing value in the store is {@link java.lang.Object#equals(Object)} to
* the value returned from the mappingFunction this function will be invoked. If this function
* returns {@link java.lang.Boolean#FALSE} then the existing entry in the store will not be replaced
* with a new entry and the existing entry will have its access time updated
*
* @return the new value associated with the key or null if none
*/
private FakeValueHolder computeInternal(
final String key,
final FakeValueHolder currentValue,
final BiFunction<? super String, ? super String, ? extends String> mappingFunction,
final NullaryFunction<Boolean> replaceEqual) throws StoreAccessException {
String remappedValue = null;
try {
remappedValue = mappingFunction.apply(key, (currentValue == null ? null : currentValue.value()));
} catch (StorePassThroughException cpte) {
Throwable cause = cpte.getCause();
if(cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if(cause instanceof StoreAccessException){
throw (StoreAccessException) cause;
} else {
throw new StoreAccessException(cause);
}
}
FakeValueHolder newValue = (remappedValue == null ? null : new FakeValueHolder(remappedValue));
if (newValue == null) {
/* Remove entry from store */
this.entries.remove(key);
} else if (!newValue.equals(currentValue)) {
/* New, remapped value is different */
this.entries.put(key, newValue);
} else {
/* New, remapped value is the same */
if (replaceEqual.apply()) {
/* Replace existing equal value */
this.entries.put(key, newValue);
} else {
/* Update access time of current entry */
currentValue.lastAccessTime = System.currentTimeMillis();
newValue = currentValue;
}
}
return newValue;
}
@Override
public ValueHolder<String> compute(
final String key,
final BiFunction<? super String, ? super String, ? extends String> mappingFunction,
final NullaryFunction<Boolean> replaceEqual)
throws StoreAccessException {
this.checkFailingKey(key);
return this.computeInternal(key, this.entries.get(key), mappingFunction, replaceEqual);
}
@Override
public ValueHolder<String> computeIfAbsent(final String key, final Function<? super String, ? extends String> mappingFunction)
throws StoreAccessException {
this.checkFailingKey(key);
FakeValueHolder currentValue = this.entries.get(key);
if (currentValue == null) {
String newValue = null;
try {
newValue = mappingFunction.apply(key);
} catch (StorePassThroughException cpte) {
Throwable cause = cpte.getCause();
if(cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if(cause instanceof StoreAccessException){
throw (StoreAccessException) cause;
} else {
throw new StoreAccessException(cause);
}
}
if (newValue != null) {
final FakeValueHolder newValueHolder = new FakeValueHolder(newValue);
this.entries.put(key, newValueHolder);
currentValue = newValueHolder;
}
} else {
currentValue.lastAccessTime = System.currentTimeMillis();
}
return currentValue;
}
/**
* {@inheritDoc}
* <p>
* This method is implemented as
* <code>this.{@link #bulkCompute(Set, Function, NullaryFunction)
* bulkCompute}(keys, remappingFunction, () -> { returns true; })</code>
*/
@Override
public Map<String, ValueHolder<String>> bulkCompute(
final Set<? extends String> keys,
final Function<Iterable<? extends Map.Entry<? extends String, ? extends String>>, Iterable<? extends Map.Entry<? extends String, ? extends String>>> remappingFunction)
throws StoreAccessException {
return this.bulkCompute(keys, remappingFunction, REPLACE_EQUAL_TRUE);
}
/**
* {@inheritDoc}
* <p>
* This implementation calls {@link #compute(String, BiFunction, NullaryFunction)
* compute(key, BiFunction, replaceEqual)} for each key presented in {@code keys}.
*/
@Override
public Map<String, Store.ValueHolder<String>> bulkCompute(
final Set<? extends String> keys,
final Function<Iterable<? extends Entry<? extends String, ? extends String>>, Iterable<? extends Entry<? extends String, ? extends String>>> remappingFunction,
final NullaryFunction<Boolean> replaceEqual)
throws StoreAccessException {
final Map<String, ValueHolder<String>> resultMap = new LinkedHashMap<String, ValueHolder<String>>();
for (final String key : keys) {
final ValueHolder<String> newValue = this.compute(key,
new BiFunction<String, String, String>() {
@Override
public String apply(final String key, final String oldValue) {
final Entry<String, String> entry = new AbstractMap.SimpleEntry<String, String>(key, oldValue);
final Entry<? extends String, ? extends String> remappedEntry =
remappingFunction.apply(Collections.singletonList(entry)).iterator().next();
return remappedEntry.getValue();
}
},
replaceEqual);
resultMap.put(key, newValue);
}
return resultMap;
}
/**
* {@inheritDoc}
* <p>
* This implementation is based, in part, on the implementation found in
* {@code org.ehcache.internal.store.OnHeapStore}. This implementation calls
* {@code mappingFunction} for each key through an internal function supplied
* to {@link #computeIfAbsent(String, Function) computeIfAbsent}.
*/
@Override
public Map<String, ValueHolder<String>> bulkComputeIfAbsent(final Set<? extends String> keys, final Function<Iterable<? extends String>, Iterable<? extends Map.Entry<? extends String, ? extends String>>> mappingFunction)
throws StoreAccessException {
final Map<String, ValueHolder<String>> resultMap = new LinkedHashMap<String, ValueHolder<String>>();
for (final String key : keys) {
final ValueHolder<String> newValue = this.computeIfAbsent(key, new Function<String, String>() {
@Override
public String apply(final String key) {
final Map.Entry<? extends String, ? extends String> entry =
mappingFunction.apply(Collections.singleton(key)).iterator().next();
return entry.getValue();
}
});
resultMap.put(key, newValue);
}
return resultMap;
}
@Override
public List<CacheConfigurationChangeListener> getConfigurationChangeListeners() {
List<CacheConfigurationChangeListener> configurationChangeListenerList
= new ArrayList<CacheConfigurationChangeListener>();
configurationChangeListenerList.add(this.cacheConfigurationChangeListener);
return configurationChangeListenerList;
}
private void checkFailingKey(final String key) throws StoreAccessException {
if (this.failingKeys.contains(key)) {
throw new StoreAccessException(String.format("Accessing failing key: %s", key));
}
}
/**
* A {@link Store.ValueHolder} implementation for use within
* {@link org.ehcache.core.EhcacheBasicCrudBase.FakeStore}.
*/
private static class FakeValueHolder implements ValueHolder<String> {
private final String value;
private final long creationTime;
private long lastAccessTime;
public FakeValueHolder(final String value) {
this.value = value;
this.creationTime = System.currentTimeMillis();
}
@Override
public String value() {
return this.value;
}
@Override
public long creationTime(final TimeUnit unit) {
return unit.convert(this.creationTime, TimeUnit.MICROSECONDS);
}
@Override
public long expirationTime(TimeUnit unit) {
return 0;
}
@Override
public boolean isExpired(long expirationTime, TimeUnit unit) {
return false;
}
@Override
public long lastAccessTime(final TimeUnit unit) {
return unit.convert(this.lastAccessTime, TimeUnit.MICROSECONDS);
}
@Override
public float hitRate(long now, final TimeUnit unit) {
return 0;
}
@Override
public long hits() {
return 0;
}
@Override
public long getId() {
throw new UnsupportedOperationException("Implement me!");
}
@Override
public String toString() {
return "FakeValueHolder{" +
"value='" + this.value + '\'' +
", creationTime=" + this.creationTime +
", lastAccessTime=" + this.lastAccessTime +
'}';
}
}
}
/**
* Local {@code org.hamcrest.TypeSafeMatcher} implementation for testing
* {@code org.terracotta.statistics.OperationStatistic} values.
*/
private static final class StatisticMatcher extends TypeSafeMatcher<Number> {
final Number expected;
private StatisticMatcher(final Class<?> expectedType, final Number expected) {
super(expectedType);
this.expected = expected;
}
@Override
protected boolean matchesSafely(final Number value) {
if (value != null) {
return (value.longValue() == this.expected.longValue());
} else {
return this.expected.longValue() == 0L;
}
}
@Override
public void describeTo(final Description description) {
if (this.expected.longValue() == 0L) {
description.appendText("zero or null");
} else {
description.appendValue(this.expected);
}
}
@Factory
public static Matcher<Number> equalTo(final Number expected) {
return new StatisticMatcher(Number.class, expected);
}
}
/**
* Provides a basic {@link CacheLoaderWriter} implementation for
* testing. The contract implemented by this {@code CacheLoaderWriter} may not be strictly
* conformant but should be sufficient for {@code Ehcache} implementation testing.
*/
protected static class FakeCacheLoaderWriter implements CacheLoaderWriter<String, String> {
private final Map<String, String> entries = new HashMap<String, String>();
/**
* Keys for which access results in a thrown {@code Exception}. This set may be empty.
*/
private final Set<String> failingKeys;
private boolean isBulkCacheLoadingExceptionEnabled = false;
/**
* The entry key causing the {@link #writeAll(Iterable)} and {@link #deleteAll(Iterable)}
* methods to fail by throwing an exception <i>other</i> than a
* {@link BulkCacheWritingException BulkCacheWritingException}.
*
* @see #setCompleteFailureKey
*/
private volatile String completeFailureKey = null;
public FakeCacheLoaderWriter(final Map<String, String> entries) {
this(entries, Collections.<String>emptySet());
}
public FakeCacheLoaderWriter(final Map<String, String> entries, final Set<String> failingKeys) {
if (entries != null) {
this.entries.putAll(entries);
}
this.failingKeys = (failingKeys.isEmpty()
? Collections.<String>emptySet()
: Collections.unmodifiableSet(new HashSet<String>(failingKeys)));
}
public FakeCacheLoaderWriter(final Map<String, String> entries, final Set<String> failingKeys, boolean isBulkCacheLoadingExceptionEnabled) {
this(entries, failingKeys);
this.isBulkCacheLoadingExceptionEnabled = isBulkCacheLoadingExceptionEnabled;
}
Map<String, String> getEntryMap() {
return Collections.unmodifiableMap(this.entries);
}
/**
* Sets the key causing the {@link #writeAll(Iterable)} and {@link #deleteAll(Iterable)}
* methods to throw an exception <i>other</i> that a
* {@link BulkCacheWritingException BulkCacheWritingException}.
* <p>
* If a complete failure is recognized, the cache image maintained by this instance
* is in an inconsistent state.
*
* @param completeFailureKey the key, which when processed by {@code writeAll} or
* {@code deleteAll}, causes a complete failure of the method
*/
final void setCompleteFailureKey(final String completeFailureKey) {
this.completeFailureKey = completeFailureKey;
}
@Override
public void write(final String key, final String value) throws Exception {
this.checkFailingKey(key);
this.entries.put(key, value);
}
/**
* {@inheritDoc}
* <p>
* If this method throws an exception <i>other</i> than a
* {@link BulkCacheWritingException BulkCacheWritingException}, the
* cache image maintained by this {@code CacheLoaderWriter} is in an inconsistent state.
*/
@Override
public void writeAll(final Iterable<? extends Map.Entry<? extends String, ? extends String>> entries)
throws Exception {
final Set<String> successes = new LinkedHashSet<String>();
final Map<String, Exception> failures = new LinkedHashMap<String, Exception>();
for (final Entry<? extends String, ? extends String> entry : entries) {
final String key = entry.getKey();
if (key.equals(this.completeFailureKey)) {
throw new CompleteFailureException();
}
try {
this.write(key, entry.getValue());
successes.add(key);
} catch (Exception e) {
//noinspection ThrowableResultOfMethodCallIgnored
failures.put(key, e);
}
}
if (!failures.isEmpty()) {
throw new BulkCacheWritingException(failures, successes);
}
}
@Override
public void delete(final String key) throws Exception {
this.checkFailingKey(key);
this.entries.remove(key);
}
/**
* {@inheritDoc}
* <p>
* If this method throws an exception <i>other</i> than a
* {@link BulkCacheWritingException BulkCacheWritingException}, the
* cache image maintained by this {@code CacheLoaderWriter} is in an inconsistent state.
*/
@Override
public void deleteAll(final Iterable<? extends String> keys) throws Exception {
final Set<String> successes = new LinkedHashSet<String>();
final Map<String, Exception> failures = new LinkedHashMap<String, Exception>();
for (final String key : keys) {
if (key.equals(this.completeFailureKey)) {
throw new CompleteFailureException();
}
try {
this.delete(key);
successes.add(key);
} catch (Exception e) {
//noinspection ThrowableResultOfMethodCallIgnored
failures.put(key, e);
}
}
if (!failures.isEmpty()) {
throw new BulkCacheWritingException(failures, successes);
}
}
private void checkFailingKey(final String key) throws FailedKeyException {
if (this.failingKeys.contains(key)) {
throw new FailedKeyException(String.format("Accessing failing key: %s", key));
}
}
private static final class CompleteFailureException extends Exception {
private static final long serialVersionUID = -8796858843677614631L;
public CompleteFailureException() {
}
}
@Override
public String load(final String key) throws Exception {
if (this.failingKeys.contains(key)) {
throw new FailedKeyException(key);
}
return this.entries.get(key);
}
@Override
public Map<String, String> loadAll(final Iterable<? extends String> keys) throws Exception {
if (isBulkCacheLoadingExceptionEnabled) {
Map<String, Exception> failures = new HashMap<String, Exception>();
Map<String, String> loadedKeys = new HashMap<String, String>();
Exception loadingException = new RuntimeException("Exception loading keys");
for (String key : keys) {
if (failingKeys.contains(key)) {
failures.put(key, loadingException);
} else {
loadedKeys.put(key, this.entries.get(key));
}
}
throw new BulkCacheLoadingException(failures, loadedKeys);
}
final Map<String, String> resultMap = new HashMap<String, String>();
for (final String key : keys) {
if (this.failingKeys.contains(key)) {
throw new FailedKeyException(key);
}
resultMap.put(key, this.entries.get(key));
}
return resultMap;
}
private static final class FailedKeyException extends Exception {
private static final long serialVersionUID = 1085055801147786691L;
public FailedKeyException(final String message) {
super(message);
}
}
}
}