package org.cache2k.jcache.provider; /* * #%L * cache2k JCache provider * %% * Copyright (C) 2000 - 2017 headissue GmbH, Munich * %% * 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. * #L% */ import org.cache2k.CacheEntry; import org.cache2k.core.InternalCache; import org.cache2k.expiry.ExpiryTimeValues; import org.cache2k.processor.EntryProcessor; import org.cache2k.processor.MutableCacheEntry; import javax.cache.Cache; import javax.cache.CacheManager; import javax.cache.configuration.CacheEntryListenerConfiguration; import javax.cache.configuration.Configuration; import javax.cache.expiry.Duration; import javax.cache.expiry.ExpiryPolicy; import javax.cache.integration.CompletionListener; import javax.cache.processor.EntryProcessorException; import javax.cache.processor.EntryProcessorResult; import javax.cache.processor.MutableEntry; import java.util.AbstractCollection; import java.util.AbstractSet; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * Adapter to add required semantics for JSR107 with a custom expiry policy. The JCacheAdapter is * wrapped again and the expiry policy is called when needed. * * <p>There are multiple requirements which makes cache operations with expiry policy very inefficient. These are: * <ul> * <li>The TCK checks that the access policy is called and adjusted on each cache request</li> * <li>The TCK has some tests that use a zero duration on expiry, so an entry is expired after the first access</li> * <li>The TCK does not allow that the expiry policy methods are called in the configuration phase</li> * <li>In case the expiry policy methods return null, this means, that the expiry is not changed</li> * </ul> * * <p> * * @author Jens Wilke */ public class TouchyJCacheAdapter<K, V> implements Cache<K, V> { InternalCache<K, V> c2kCache; JCacheAdapter<K, V> cache; ExpiryPolicy expiryPolicy; public TouchyJCacheAdapter(JCacheAdapter<K,V> _cache, ExpiryPolicy _expiryPolicy) { expiryPolicy = _expiryPolicy; cache = _cache; c2kCache = _cache.cacheImpl; } @Override public V get(K key) { return returnValue(key, cache.get(key)); } @Override public Map<K, V> getAll(Set<? extends K> keys) { final Map<K, V> map = cache.getAll(keys); return new Map<K, V>() { @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public boolean containsKey(Object key) { return map.containsKey(key); } @Override public boolean containsValue(Object value) { return map.containsValue(value); } @SuppressWarnings("unchecked") @Override public V get(Object key) { return returnValue((K) key, map.get(key)); } @Override public V put(K key, V value) { throw new UnsupportedOperationException("read only"); } @Override public V remove(Object key) { return map.remove(key); } @Override public void putAll(Map<? extends K, ? extends V> m) { throw new UnsupportedOperationException("read only"); } @Override public void clear() { throw new UnsupportedOperationException("read only"); } @Override public Set<K> keySet() { return map.keySet(); } @Override public Collection<V> values() { return new AbstractCollection<V>() { @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public Iterator<V> iterator() { final Iterator<Entry<K, V>> it = map.entrySet().iterator(); return new Iterator<V>() { @Override public boolean hasNext() { return it.hasNext(); } @Override public V next() { Entry<K, V> e = it.next(); return returnValue(e.getKey(), e.getValue()); } @Override public void remove() { throw new UnsupportedOperationException(); } }; } }; } @Override public Set<Entry<K, V>> entrySet() { final Iterator<Entry<K, V>> it = map.entrySet().iterator(); return new AbstractSet<Entry<K, V>>() { @Override public Iterator<Entry<K, V>> iterator() { return new Iterator<Entry<K, V>>() { @Override public boolean hasNext() { return it.hasNext(); } @Override public Entry<K, V> next() { final Entry<K, V> e = it.next(); return new Entry<K, V>() { @Override public K getKey() { return e.getKey(); } @Override public V getValue() { return returnValue(e.getKey(), e.getValue()); } @Override public V setValue(V value) { throw new UnsupportedOperationException(); } }; } @Override public void remove() { } }; } @Override public int size() { return map.size(); } }; } }; } @Override public boolean containsKey(K key) { return cache.containsKey(key); } @Override public void loadAll(Set<? extends K> keys, boolean replaceExistingValues, CompletionListener completionListener) { cache.loadAll(keys, replaceExistingValues, completionListener); } @Override public void put(K key, V value) { cache.put(key, value); } @Override public V getAndPut(K key, V value) { checkClosed(); return cache.getAndPut(key,value); } @Override public void putAll(Map<? extends K, ? extends V> map) { cache.putAll(map); } @Override public boolean putIfAbsent(K key, V value) { return cache.putIfAbsent(key, value); } @Override public boolean remove(K key) { return cache.remove(key); } @Override public boolean remove(final K key, final V oldValue) { checkClosed(); checkNullValue(oldValue); if (key == null) { throw new NullPointerException(); } EntryProcessor<K,V,Boolean> ep = new EntryProcessor<K, V, Boolean>() { @Override public Boolean process(final MutableCacheEntry<K, V> e) throws Exception { if (!e.exists()) { return false; } V _existingValue = e.getValue(); if (_existingValue.equals(oldValue)) { e.remove(); return true; } Duration d = expiryPolicy.getExpiryForAccess(); if (d != null) { e.setExpiry(calculateExpiry(d)); } return false; } }; return c2kCache.invoke(key, ep); } @Override public V getAndRemove(K key) { return cache.getAndRemove(key); } final CacheEntry<K, V> DUMMY_ENTRY = new CacheEntry<K, V>() { @Override public K getKey() { return null; } @Override public V getValue() { return null; } @Override public Throwable getException() { return null; } @Override public long getLastModification() { return 0; } }; @Override public boolean replace(K key, V oldValue, V newValue) { checkClosed(); checkNullValue(newValue); checkNullValue(oldValue); CacheEntry<K, V> e = c2kCache.replaceOrGet( key, oldValue, newValue, DUMMY_ENTRY); if (e != null && e != DUMMY_ENTRY) { touchEntry(key); } return e == null; } @Override public boolean replace(K key, V value) { checkClosed(); checkNullValue(value); return c2kCache.replace(key, value); } @Override public V getAndReplace(K key, V value) { return cache.getAndReplace(key, value); } @Override public void removeAll(Set<? extends K> keys) { cache.removeAll(keys); } @Override public void removeAll() { cache.removeAll(); } @Override public void clear() { cache.clear(); } @SuppressWarnings("unchecked") @Override public <C extends Configuration<K, V>> C getConfiguration(Class<C> clazz) { return cache.getConfiguration(clazz); } @Override public <T> T invoke(K key, javax.cache.processor.EntryProcessor<K,V, T> entryProcessor, Object... arguments) throws EntryProcessorException { return cache.invoke(key, wrapEntryProcessor(entryProcessor), arguments); } @Override public <T> Map<K, EntryProcessorResult<T>> invokeAll(Set<? extends K> keys, javax.cache.processor.EntryProcessor<K,V,T> entryProcessor, Object... arguments) { return cache.invokeAll(keys, wrapEntryProcessor(entryProcessor), arguments); } @Override public String getName() { return cache.getName(); } @Override public CacheManager getCacheManager() { return cache.getCacheManager(); } @Override public void close() { cache.close(); } @Override public boolean isClosed() { return cache.isClosed(); } @Override public <T> T unwrap(Class<T> clazz) { return c2kCache.requestInterface(clazz); } @Override public void registerCacheEntryListener(CacheEntryListenerConfiguration<K, V> cfg) { cache.registerCacheEntryListener(cfg); } @Override public void deregisterCacheEntryListener(CacheEntryListenerConfiguration<K, V> cfg) { cache.deregisterCacheEntryListener(cfg); } @Override public Iterator<Cache.Entry<K, V>> iterator() { final Iterator<Cache.Entry<K, V>> it = cache.iterator(); return new Iterator<Entry<K, V>>() { @Override public boolean hasNext() { return it.hasNext(); } @Override public Entry<K, V> next() { final Entry<K, V> e = it.next(); return returnEntry(e); } @Override public void remove() { it.remove(); } }; } private <T> javax.cache.processor.EntryProcessor<K,V,T> wrapEntryProcessor(final javax.cache.processor.EntryProcessor<K,V,T> ep) { if (ep == null) { throw new NullPointerException("processor is null"); } return new javax.cache.processor.EntryProcessor<K,V, T>() { boolean freshOrJustLoaded = false; @Override public T process(final MutableEntry<K, V> e0, Object... _args) throws EntryProcessorException { MutableEntry<K, V> me = new MutableEntry<K, V>() { @Override public boolean exists() { return e0.exists(); } @Override public void remove() { e0.remove(); } @Override public void setValue(V value) { checkNullValue(value); freshOrJustLoaded = true; e0.setValue(value); } @Override public K getKey() { return e0.getKey(); } @Override public V getValue() { boolean _doNotCountCacheAccessIfEntryGetsLoaded = !exists(); boolean _doNotCountCacheAccessIfEntryIsFresh = freshOrJustLoaded; if (_doNotCountCacheAccessIfEntryIsFresh || _doNotCountCacheAccessIfEntryGetsLoaded) { if (!cache.readThrough && !exists()) { return null; } freshOrJustLoaded = true; return e0.getValue(); } return returnValue(e0.getKey(), e0.getValue()); } @Override public <X> X unwrap(Class<X> clazz) { return null; } }; return ep.process(me, _args); } }; } /** * Entry is accessed update expiry if needed. */ private Entry<K, V> returnEntry(final Entry<K, V> e) { touchEntry(e.getKey()); return e; } /** * Entry was accessed update expiry if value is non null. */ private V returnValue(K key, V _value) { if (_value != null) { Duration d = expiryPolicy.getExpiryForAccess(); if (d != null) { c2kCache.expireAt(key, calculateExpiry(d)); } return _value; } return null; } private static long calculateExpiry(final Duration d) { if (Duration.ZERO.equals(d)) { return ExpiryTimeValues.NO_CACHE; } else if (Duration.ETERNAL.equals(d)) { return ExpiryTimeValues.ETERNAL; } return System.currentTimeMillis() + d.getTimeUnit().toMillis(d.getDurationAmount()); } private void touchEntry(K key) { Duration d = expiryPolicy.getExpiryForAccess(); if (d != null) { c2kCache.expireAt(key, calculateExpiry(d)); } } private void checkClosed() { if (cache.isClosed()) { throw new IllegalStateException("cache is closed"); } } private void checkNullValue(V _value) { if (_value == null) { throw new NullPointerException("value is null"); } } @Override public String toString() { return getClass().getSimpleName() + "@" + c2kCache.toString(); } public static class ExpiryPolicyAdapter<K, V> implements org.cache2k.expiry.ExpiryPolicy<K, V> { javax.cache.expiry.ExpiryPolicy policy; public ExpiryPolicyAdapter(javax.cache.expiry.ExpiryPolicy policy) { this.policy = policy; } @Override public long calculateExpiryTime(K _key, V _value, long _loadTime, CacheEntry<K, V> _oldEntry) { if (_value == null) { return NO_CACHE; } Duration d; if (_oldEntry == null || _oldEntry.getException() != null) { d = policy.getExpiryForCreation(); } else { d = policy.getExpiryForUpdate(); } if (d == null) { return ExpiryTimeValues.NEUTRAL; } if (d.equals(Duration.ETERNAL)) { return ExpiryTimeValues.ETERNAL; } if (d.equals(Duration.ZERO)) { return ExpiryTimeValues.NO_CACHE; } return _loadTime + d.getTimeUnit().toMillis(d.getDurationAmount()); } } }