/* * Copyright 2015 Ben Manes. All Rights Reserved. * * 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 com.github.benmanes.caffeine.jcache; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; import java.util.stream.Collectors; import javax.cache.Cache; import javax.cache.CacheException; import javax.cache.CacheManager; import javax.cache.expiry.ExpiryPolicy; import javax.cache.integration.CacheLoader; import javax.cache.integration.CompletionListener; import com.github.benmanes.caffeine.cache.LoadingCache; import com.github.benmanes.caffeine.cache.Ticker; import com.github.benmanes.caffeine.jcache.configuration.CaffeineConfiguration; import com.github.benmanes.caffeine.jcache.event.EventDispatcher; import com.github.benmanes.caffeine.jcache.management.JCacheStatisticsMXBean; /** * An implementation of JSR-107 {@link Cache} backed by a Caffeine loading cache. * * @author ben.manes@gmail.com (Ben Manes) */ public final class LoadingCacheProxy<K, V> extends CacheProxy<K, V> { private final LoadingCache<K, Expirable<V>> cache; @SuppressWarnings("PMD.ExcessiveParameterList") public LoadingCacheProxy(String name, Executor executor, CacheManager cacheManager, CaffeineConfiguration<K, V> configuration, LoadingCache<K, Expirable<V>> cache, EventDispatcher<K, V> dispatcher, CacheLoader<K, V> cacheLoader, ExpiryPolicy expiry, Ticker ticker, JCacheStatisticsMXBean statistics) { super(name, executor, cacheManager, configuration, cache, dispatcher, Optional.of(cacheLoader), expiry, ticker, statistics); this.cache = cache; } @Override @SuppressWarnings("PMD.AvoidCatchingNPE") public V get(K key) { requireNotClosed(); try { return getOrLoad(key); } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) { throw e; } catch (RuntimeException e) { throw new CacheException(e); } finally { dispatcher.awaitSynchronous(); } } /** Retrieves the value from the cache, loading it if necessary. */ @SuppressWarnings("PMD.AvoidDeeplyNestedIfStmts") private V getOrLoad(K key) { boolean statsEnabled = statistics.isEnabled(); long start = statsEnabled ? ticker.read() : 0L; long millis = 0L; Expirable<V> expirable = cache.getIfPresent(key); if ((expirable != null) && !expirable.isEternal()) { millis = nanosToMillis((start == 0L) ? ticker.read() : start); if (expirable.hasExpired(millis)) { Expirable<V> expired = expirable; cache.asMap().computeIfPresent(key, (k, e) -> { if (e == expired) { dispatcher.publishExpired(this, key, expired.get()); statistics.recordEvictions(1); return null; } return e; }); expirable = null; } } if (expirable == null) { expirable = cache.get(key); statistics.recordMisses(1L); } else { statistics.recordHits(1L); } V value = null; if (expirable != null) { setAccessExpirationTime(expirable, millis); value = copyValue(expirable); } if (statsEnabled) { statistics.recordGetTime(ticker.read() - start); } return value; } @Override public Map<K, V> getAll(Set<? extends K> keys) { return getAll(keys, true); } /** Returns the entries, loading if necessary, and optionally updates their access expiry time. */ @SuppressWarnings("PMD.AvoidCatchingNPE") private Map<K, V> getAll(Set<? extends K> keys, boolean updateAccessTime) { requireNotClosed(); boolean statsEnabled = statistics.isEnabled(); long start = statsEnabled ? ticker.read() : 0L; try { Map<K, Expirable<V>> entries = getAndFilterExpiredEntries(keys, updateAccessTime); if (entries.size() != keys.size()) { List<K> keysToLoad = keys.stream() .filter(key -> !entries.containsKey(key)) .collect(Collectors.<K>toList()); entries.putAll(cache.getAll(keysToLoad)); } Map<K, V> result = copyMap(entries); if (statsEnabled) { statistics.recordGetTime(ticker.read() - start); } return result; } catch (NullPointerException | IllegalStateException | ClassCastException | CacheException e) { throw e; } catch (RuntimeException e) { throw new CacheException(e); } finally { dispatcher.awaitSynchronous(); } } @Override public void loadAll(Set<? extends K> keys, boolean replaceExistingValues, CompletionListener completionListener) { requireNotClosed(); keys.forEach(Objects::requireNonNull); CompletionListener listener = (completionListener == null) ? NullCompletionListener.INSTANCE : completionListener; executor.execute(() -> { try { if (replaceExistingValues) { int[] ignored = { 0 }; Map<K, V> loaded = cacheLoader.get().loadAll(keys); for (Map.Entry<? extends K, ? extends V> entry : loaded.entrySet()) { putNoCopyOrAwait(entry.getKey(), entry.getValue(), /* publishToWriter */ false, ignored); } } else { getAll(keys, /* updateAccessTime */ false); } listener.onCompletion(); } catch (Exception e) { listener.onException(e); } finally { dispatcher.ignoreSynchronous(); } }); } }