All Downloads are FREE. Search and download functionalities are using the official Maven repository.

keycloakjar.com.github.benmanes.caffeine.cache.UnboundedLocalCache Maven / Gradle / Ivy

There is a newer version: 7.22.0
Show newest version
/*
 * Copyright 2014 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.cache;

import static com.github.benmanes.caffeine.cache.Caffeine.calculateHashMapCapacity;
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newBulkMappingFunction; // NOPMD
import static com.github.benmanes.caffeine.cache.LocalLoadingCache.newMappingFunction; // NOPMD
import static java.util.Objects.requireNonNull;
import static java.util.function.Function.identity;

import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.lang.System.Logger;
import java.lang.System.Logger.Level;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.AbstractCollection;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.Spliterator;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executor;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

import org.checkerframework.checker.nullness.qual.Nullable;

import com.github.benmanes.caffeine.cache.stats.StatsCounter;
import com.google.errorprone.annotations.CanIgnoreReturnValue;

/**
 * An in-memory cache that has no capabilities for bounding the map. This implementation provides
 * a lightweight wrapper on top of {@link ConcurrentHashMap}.
 *
 * @author [email protected] (Ben Manes)
 */
@SuppressWarnings("serial")
final class UnboundedLocalCache implements LocalCache {
  static final Logger logger = System.getLogger(UnboundedLocalCache.class.getName());
  static final VarHandle REFRESHES;

  @Nullable final RemovalListener removalListener;
  final ConcurrentHashMap data;
  final StatsCounter statsCounter;
  final boolean isRecordingStats;
  final Executor executor;
  final boolean isAsync;
  final Ticker ticker;

  @Nullable Set keySet;
  @Nullable Collection values;
  @Nullable Set> entrySet;
  @Nullable volatile ConcurrentMap> refreshes;

  UnboundedLocalCache(Caffeine builder, boolean isAsync) {
    this.data = new ConcurrentHashMap<>(builder.getInitialCapacity());
    this.statsCounter = builder.getStatsCounterSupplier().get();
    this.removalListener = builder.getRemovalListener(isAsync);
    this.isRecordingStats = builder.isRecordingStats();
    this.executor = builder.getExecutor();
    this.ticker = builder.getTicker();
    this.isAsync = isAsync;
  }

  static {
    try {
      REFRESHES = MethodHandles.lookup()
          .findVarHandle(UnboundedLocalCache.class, "refreshes", ConcurrentMap.class);
    } catch (ReflectiveOperationException e) {
      throw new ExceptionInInitializerError(e);
    }
  }

  @Override
  public boolean isAsync() {
    return isAsync;
  }

  @Override
  @SuppressWarnings("NullAway")
  public Expiry expiry() {
    return null;
  }

  @Override
  @CanIgnoreReturnValue
  public Object referenceKey(K key) {
    return key;
  }

  @Override
  public boolean isPendingEviction(K key) {
    return false;
  }

  /* --------------- Cache --------------- */

  @Override
  @SuppressWarnings("SuspiciousMethodCalls")
  public @Nullable V getIfPresent(Object key, boolean recordStats) {
    V value = data.get(key);

    if (recordStats) {
      if (value == null) {
        statsCounter.recordMisses(1);
      } else {
        statsCounter.recordHits(1);
      }
    }
    return value;
  }

  @Override
  public @Nullable V getIfPresentQuietly(K key) {
    return data.get(key);
  }

  @Override
  public long estimatedSize() {
    return data.mappingCount();
  }

  @Override
  public Map getAllPresent(Iterable keys) {
    var result = new LinkedHashMap(calculateHashMapCapacity(keys));
    for (K key : keys) {
      result.put(key, null);
    }

    int uniqueKeys = result.size();
    for (var iter = result.entrySet().iterator(); iter.hasNext();) {
      Map.Entry entry = iter.next();
      V value = data.get(entry.getKey());
      if (value == null) {
        iter.remove();
      } else {
        entry.setValue(value);
      }
    }
    statsCounter.recordHits(result.size());
    statsCounter.recordMisses(uniqueKeys - result.size());

    return Collections.unmodifiableMap(result);
  }

  @Override
  public void cleanUp() {}

  @Override
  public StatsCounter statsCounter() {
    return statsCounter;
  }

  private boolean hasRemovalListener() {
    return (removalListener != null);
  }

  @Override
  @SuppressWarnings("NullAway")
  public void notifyRemoval(@Nullable K key, @Nullable V value, RemovalCause cause) {
    if (!hasRemovalListener()) {
      return;
    }
    Runnable task = () -> {
      try {
        removalListener.onRemoval(key, value, cause);
      } catch (Throwable t) {
        logger.log(Level.WARNING, "Exception thrown by removal listener", t);
      }
    };
    try {
      executor.execute(task);
    } catch (Throwable t) {
      logger.log(Level.ERROR, "Exception thrown when submitting removal listener", t);
      task.run();
    }
  }

  @Override
  public boolean isRecordingStats() {
    return isRecordingStats;
  }

  @Override
  public Executor executor() {
    return executor;
  }

  @Override
  @SuppressWarnings("NullAway")
  public ConcurrentMap> refreshes() {
    var pending = refreshes;
    if (pending == null) {
      pending = new ConcurrentHashMap<>();
      if (!REFRESHES.compareAndSet(this, null, pending)) {
        pending = refreshes;
      }
    }
    return pending;
  }

  /** Invalidate the in-flight refresh. */
  void discardRefresh(Object keyReference) {
    var pending = refreshes;
    if (pending != null) {
      pending.remove(keyReference);
    }
  }

  @Override
  public Ticker statsTicker() {
    return ticker;
  }

  /* --------------- JDK8+ Map extensions --------------- */

  @Override
  public void forEach(BiConsumer action) {
    data.forEach(action);
  }

  @Override
  public void replaceAll(BiFunction function) {
    requireNonNull(function);

    // ensures that the removal notification is processed after the removal has completed
    @SuppressWarnings({"rawtypes", "unchecked"})
    K[] notificationKey = (K[]) new Object[1];
    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] notificationValue = (V[]) new Object[1];
    data.replaceAll((key, value) -> {
      if (notificationKey[0] != null) {
        notifyRemoval(notificationKey[0], notificationValue[0], RemovalCause.REPLACED);
        notificationValue[0] = null;
        notificationKey[0] = null;
      }

      V newValue = requireNonNull(function.apply(key, value));
      if (newValue != value) {
        notificationKey[0] = key;
        notificationValue[0] = value;
      }

      return newValue;
    });
    if (notificationKey[0] != null) {
      notifyRemoval(notificationKey[0], notificationValue[0], RemovalCause.REPLACED);
    }
  }

  @Override
  public V computeIfAbsent(K key, Function mappingFunction,
      boolean recordStats, boolean recordLoad) {
    requireNonNull(mappingFunction);

    // optimistic fast path due to computeIfAbsent always locking
    V value = data.get(key);
    if (value != null) {
      if (recordStats) {
        statsCounter.recordHits(1);
      }
      return value;
    }

    boolean[] missed = new boolean[1];
    value = data.computeIfAbsent(key, k -> {
      // Do not communicate to CacheWriter on a load
      missed[0] = true;
      return recordStats
          ? statsAware(mappingFunction, recordLoad).apply(key)
          : mappingFunction.apply(key);
    });
    if (!missed[0] && recordStats) {
      statsCounter.recordHits(1);
    }
    return value;
  }

  @Override
  public @Nullable V computeIfPresent(K key,
      BiFunction remappingFunction) {
    requireNonNull(remappingFunction);

    // optimistic fast path due to computeIfAbsent always locking
    if (!data.containsKey(key)) {
      return null;
    }

    // ensures that the removal notification is processed after the removal has completed
    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] oldValue = (V[]) new Object[1];
    boolean[] replaced = new boolean[1];
    V nv = data.computeIfPresent(key, (K k, V value) -> {
      BiFunction function = statsAware(remappingFunction,
          /* recordLoad */ true, /* recordLoadFailure */ true);
      V newValue = function.apply(k, value);

      replaced[0] = (newValue != null);
      if (newValue != value) {
        oldValue[0] = value;
      }

      discardRefresh(k);
      return newValue;
    });
    if (replaced[0]) {
      notifyOnReplace(key, oldValue[0], nv);
    } else if (oldValue[0] != null) {
      notifyRemoval(key, oldValue[0], RemovalCause.EXPLICIT);
    }
    return nv;
  }

  @Override
  public V compute(K key, BiFunction remappingFunction,
      @Nullable Expiry expiry, boolean recordLoad,
      boolean recordLoadFailure) {
    requireNonNull(remappingFunction);
    return remap(key, statsAware(remappingFunction, recordLoad, recordLoadFailure));
  }

  @Override
  public V merge(K key, V value, BiFunction remappingFunction) {
    requireNonNull(remappingFunction);
    requireNonNull(value);

    return remap(key, (k, oldValue) ->
      (oldValue == null) ? value : statsAware(remappingFunction).apply(oldValue, value));
  }

  /**
   * A {@link Map#compute(Object, BiFunction)} that does not directly record any cache statistics.
   *
   * @param key key with which the specified value is to be associated
   * @param remappingFunction the function to compute a value
   * @return the new value associated with the specified key, or null if none
   */
  V remap(K key, BiFunction remappingFunction) {
    // ensures that the removal notification is processed after the removal has completed
    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] oldValue = (V[]) new Object[1];
    boolean[] replaced = new boolean[1];
    V nv = data.compute(key, (K k, V value) -> {
      V newValue = remappingFunction.apply(k, value);
      if ((value == null) && (newValue == null)) {
        return null;
      }

      replaced[0] = (newValue != null);
      if ((value != null) && (newValue != value)) {
        oldValue[0] = value;
      }

      discardRefresh(k);
      return newValue;
    });
    if (replaced[0]) {
      notifyOnReplace(key, oldValue[0], nv);
    } else if (oldValue[0] != null) {
      notifyRemoval(key, oldValue[0], RemovalCause.EXPLICIT);
    }
    return nv;
  }

  /* --------------- Concurrent Map --------------- */

  @Override
  public boolean isEmpty() {
    return data.isEmpty();
  }

  @Override
  public int size() {
    return data.size();
  }

  @Override
  public void clear() {
    if (!hasRemovalListener() && ((refreshes == null) || refreshes.isEmpty())) {
      data.clear();
      return;
    }
    for (K key : List.copyOf(data.keySet())) {
      remove(key);
    }
  }

  @Override
  public boolean containsKey(Object key) {
    return data.containsKey(key);
  }

  @Override
  public boolean containsValue(Object value) {
    return data.containsValue(value);
  }

  @Override
  public @Nullable V get(Object key) {
    return getIfPresent(key, /* recordStats */ false);
  }

  @Override
  public @Nullable V put(K key, V value) {
    V oldValue = data.put(key, value);
    notifyOnReplace(key, oldValue, value);
    return oldValue;
  }

  @Override
  public @Nullable V putIfAbsent(K key, V value) {
    return data.putIfAbsent(key, value);
  }

  @Override
  public void putAll(Map map) {
    if (hasRemovalListener()) {
      map.forEach(this::put);
    } else {
      data.putAll(map);
    }
  }

  @Override
  public @Nullable V remove(Object key) {
    @SuppressWarnings("unchecked")
    K castKey = (K) key;
    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] oldValue = (V[]) new Object[1];
    data.computeIfPresent(castKey, (k, v) -> {
      discardRefresh(k);
      oldValue[0] = v;
      return null;
    });

    if (oldValue[0] != null) {
      notifyRemoval(castKey, oldValue[0], RemovalCause.EXPLICIT);
    }

    return oldValue[0];
  }

  @Override
  public boolean remove(Object key, Object value) {
    if (value == null) {
      requireNonNull(key);
      return false;
    }

    @SuppressWarnings("unchecked")
    K castKey = (K) key;
    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] oldValue = (V[]) new Object[1];

    data.computeIfPresent(castKey, (k, v) -> {
      if (v.equals(value)) {
        discardRefresh(k);
        oldValue[0] = v;
        return null;
      }
      return v;
    });

    if (oldValue[0] != null) {
      notifyRemoval(castKey, oldValue[0], RemovalCause.EXPLICIT);
      return true;
    }
    return false;
  }

  @Override
  public @Nullable V replace(K key, V value) {
    requireNonNull(value);

    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] oldValue = (V[]) new Object[1];
    data.computeIfPresent(key, (k, v) -> {
      discardRefresh(k);
      oldValue[0] = v;
      return value;
    });

    if ((oldValue[0] != null) && (oldValue[0] != value)) {
      notifyRemoval(key, oldValue[0], RemovalCause.REPLACED);
    }
    return oldValue[0];
  }

  @Override
  public boolean replace(K key, V oldValue, V newValue) {
    return replace(key, oldValue, newValue, /* shouldDiscardRefresh */ true);
  }

  @Override
  public boolean replace(K key, V oldValue, V newValue, boolean shouldDiscardRefresh) {
    requireNonNull(oldValue);
    requireNonNull(newValue);

    @SuppressWarnings({"rawtypes", "unchecked"})
    V[] prev = (V[]) new Object[1];
    data.computeIfPresent(key, (k, v) -> {
      if (v.equals(oldValue)) {
        if (shouldDiscardRefresh) {
          discardRefresh(k);
        }
        prev[0] = v;
        return newValue;
      }
      return v;
    });

    boolean replaced = (prev[0] != null);
    if (replaced && (prev[0] != newValue)) {
      notifyRemoval(key, prev[0], RemovalCause.REPLACED);
    }
    return replaced;
  }

  @Override
  public boolean equals(Object o) {
    return (o == this) || data.equals(o);
  }

  @Override
  public int hashCode() {
    return data.hashCode();
  }

  @Override
  public String toString() {
    var result = new StringBuilder(50).append('{');
    data.forEach((key, value) -> {
      if (result.length() != 1) {
        result.append(", ");
      }
      result.append((key == this) ? "(this Map)" : key)
          .append('=')
          .append((value == this) ? "(this Map)" : value);
    });
    return result.append('}').toString();
  }

  @Override
  public Set keySet() {
    final Set ks = keySet;
    return (ks == null) ? (keySet = new KeySetView<>(this)) : ks;
  }

  @Override
  public Collection values() {
    final Collection vs = values;
    return (vs == null) ? (values = new ValuesView<>(this)) : vs;
  }

  @Override
  public Set> entrySet() {
    final Set> es = entrySet;
    return (es == null) ? (entrySet = new EntrySetView<>(this)) : es;
  }

  /** An adapter to safely externalize the keys. */
  static final class KeySetView extends AbstractSet {
    final UnboundedLocalCache cache;

    KeySetView(UnboundedLocalCache cache) {
      this.cache = requireNonNull(cache);
    }

    @Override
    public boolean isEmpty() {
      return cache.isEmpty();
    }

    @Override
    public int size() {
      return cache.size();
    }

    @Override
    public void clear() {
      cache.clear();
    }

    @Override
    @SuppressWarnings("SuspiciousMethodCalls")
    public boolean contains(Object o) {
      return cache.containsKey(o);
    }

    @Override
    public boolean removeAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      if ((collection instanceof Set) && (collection.size() > size())) {
        for (K key : this) {
          if (collection.contains(key)) {
            modified |= remove(key);
          }
        }
      } else {
        for (var o : collection) {
          modified |= (o != null) && remove(o);
        }
      }
      return modified;
    }

    @Override
    public boolean remove(Object o) {
      return (cache.remove(o) != null);
    }

    @Override
    public boolean removeIf(Predicate filter) {
      requireNonNull(filter);
      boolean modified = false;
      for (K key : this) {
        if (filter.test(key) && remove(key)) {
          modified = true;
        }
      }
      return modified;
    }

    @Override
    public boolean retainAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      for (K key : this) {
        if (!collection.contains(key) && remove(key)) {
          modified = true;
        }
      }
      return modified;
    }

    @Override
    public void forEach(Consumer action) {
      cache.data.keySet().forEach(action);
    }

    @Override
    public Iterator iterator() {
      return new KeyIterator<>(cache);
    }

    @Override
    public Spliterator spliterator() {
      return cache.data.keySet().spliterator();
    }
  }

  /** An adapter to safely externalize the key iterator. */
  static final class KeyIterator implements Iterator {
    final UnboundedLocalCache cache;
    final Iterator iterator;
    @Nullable K current;

    KeyIterator(UnboundedLocalCache cache) {
      this.iterator = cache.data.keySet().iterator();
      this.cache = cache;
    }

    @Override
    public boolean hasNext() {
      return iterator.hasNext();
    }

    @Override
    public K next() {
      current = iterator.next();
      return current;
    }

    @Override
    public void remove() {
      if (current == null) {
        throw new IllegalStateException();
      }
      cache.remove(current);
      current = null;
    }
  }

  /** An adapter to safely externalize the values. */
  static final class ValuesView extends AbstractCollection {
    final UnboundedLocalCache cache;

    ValuesView(UnboundedLocalCache cache) {
      this.cache = requireNonNull(cache);
    }

    @Override
    public boolean isEmpty() {
      return cache.isEmpty();
    }

    @Override
    public int size() {
      return cache.size();
    }

    @Override
    public void clear() {
      cache.clear();
    }

    @Override
    @SuppressWarnings("SuspiciousMethodCalls")
    public boolean contains(Object o) {
      return cache.containsValue(o);
    }

    @Override
    public boolean removeAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      for (var entry : cache.data.entrySet()) {
        if (collection.contains(entry.getValue())
            && cache.remove(entry.getKey(), entry.getValue())) {
          modified = true;
        }
      }
      return modified;
    }

    @Override
    public boolean remove(Object o) {
      if (o == null) {
        return false;
      }
      for (var entry : cache.data.entrySet()) {
        if (o.equals(entry.getValue()) && cache.remove(entry.getKey(), entry.getValue())) {
          return true;
        }
      }
      return false;
    }

    @Override
    public boolean removeIf(Predicate filter) {
      requireNonNull(filter);
      boolean removed = false;
      for (var entry : cache.data.entrySet()) {
        if (filter.test(entry.getValue())) {
          removed |= cache.remove(entry.getKey(), entry.getValue());
        }
      }
      return removed;
    }

    @Override
    public boolean retainAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      for (var entry : cache.data.entrySet()) {
        if (!collection.contains(entry.getValue())
            && cache.remove(entry.getKey(), entry.getValue())) {
          modified = true;
        }
      }
      return modified;
    }

    @Override
    public void forEach(Consumer action) {
      cache.data.values().forEach(action);
    }

    @Override
    public Iterator iterator() {
      return new ValuesIterator<>(cache);
    }

    @Override
    public Spliterator spliterator() {
      return cache.data.values().spliterator();
    }
  }

  /** An adapter to safely externalize the value iterator. */
  static final class ValuesIterator implements Iterator {
    final UnboundedLocalCache cache;
    final Iterator> iterator;
    @Nullable Entry entry;

    ValuesIterator(UnboundedLocalCache cache) {
      this.iterator = cache.data.entrySet().iterator();
      this.cache = cache;
    }

    @Override
    public boolean hasNext() {
      return iterator.hasNext();
    }

    @Override
    public V next() {
      entry = iterator.next();
      return entry.getValue();
    }

    @Override
    public void remove() {
      if (entry == null) {
        throw new IllegalStateException();
      }
      cache.remove(entry.getKey());
      entry = null;
    }
  }

  /** An adapter to safely externalize the entries. */
  static final class EntrySetView extends AbstractSet> {
    final UnboundedLocalCache cache;

    EntrySetView(UnboundedLocalCache cache) {
      this.cache = requireNonNull(cache);
    }

    @Override
    public boolean isEmpty() {
      return cache.isEmpty();
    }

    @Override
    public int size() {
      return cache.size();
    }

    @Override
    public void clear() {
      cache.clear();
    }

    @Override
    @SuppressWarnings("SuspiciousMethodCalls")
    public boolean contains(Object o) {
      if (!(o instanceof Entry)) {
        return false;
      }
      var entry = (Entry) o;
      var key = entry.getKey();
      var value = entry.getValue();
      if ((key == null) || (value == null)) {
        return false;
      }
      V cachedValue = cache.get(key);
      return (cachedValue != null) && cachedValue.equals(value);
    }

    @Override
    public boolean removeAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      if ((collection instanceof Set) && (collection.size() > size())) {
        for (var entry : this) {
          if (collection.contains(entry)) {
            modified |= remove(entry);
          }
        }
      } else {
        for (var o : collection) {
          modified |= remove(o);
        }
      }
      return modified;
    }

    @Override
    @SuppressWarnings("SuspiciousMethodCalls")
    public boolean remove(Object o) {
      if (!(o instanceof Entry)) {
        return false;
      }
      var entry = (Entry) o;
      var key = entry.getKey();
      return (key != null) && cache.remove(key, entry.getValue());
    }

    @Override
    public boolean removeIf(Predicate> filter) {
      requireNonNull(filter);
      boolean removed = false;
      for (var entry : cache.data.entrySet()) {
        if (filter.test(entry)) {
          removed |= cache.remove(entry.getKey(), entry.getValue());
        }
      }
      return removed;
    }

    @Override
    public boolean retainAll(Collection collection) {
      requireNonNull(collection);
      boolean modified = false;
      for (var entry : this) {
        if (!collection.contains(entry) && remove(entry)) {
          modified = true;
        }
      }
      return modified;
    }

    @Override
    public Iterator> iterator() {
      return new EntryIterator<>(cache);
    }

    @Override
    public Spliterator> spliterator() {
      return new EntrySpliterator<>(cache);
    }
  }

  /** An adapter to safely externalize the entry iterator. */
  static final class EntryIterator implements Iterator> {
    final UnboundedLocalCache cache;
    final Iterator> iterator;
    @Nullable Entry entry;

    EntryIterator(UnboundedLocalCache cache) {
      this.iterator = cache.data.entrySet().iterator();
      this.cache = cache;
    }

    @Override
    public boolean hasNext() {
      return iterator.hasNext();
    }

    @Override
    public Entry next() {
      entry = iterator.next();
      return new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue());
    }

    @Override
    public void remove() {
      if (entry == null) {
        throw new IllegalStateException();
      }
      cache.remove(entry.getKey());
      entry = null;
    }
  }

  /** An adapter to safely externalize the entry spliterator. */
  static final class EntrySpliterator implements Spliterator> {
    final Spliterator> spliterator;
    final UnboundedLocalCache cache;

    EntrySpliterator(UnboundedLocalCache cache) {
      this(cache, cache.data.entrySet().spliterator());
    }

    EntrySpliterator(UnboundedLocalCache cache, Spliterator> spliterator) {
      this.spliterator = requireNonNull(spliterator);
      this.cache = requireNonNull(cache);
    }

    @Override
    public void forEachRemaining(Consumer> action) {
      requireNonNull(action);
      spliterator.forEachRemaining(entry -> {
        var e = new WriteThroughEntry(cache, entry.getKey(), entry.getValue());
        action.accept(e);
      });
    }

    @Override
    public boolean tryAdvance(Consumer> action) {
      requireNonNull(action);
      return spliterator.tryAdvance(entry -> {
        var e = new WriteThroughEntry(cache, entry.getKey(), entry.getValue());
        action.accept(e);
      });
    }

    @Override
    public @Nullable EntrySpliterator trySplit() {
      Spliterator> split = spliterator.trySplit();
      return (split == null) ? null : new EntrySpliterator<>(cache, split);
    }

    @Override
    public long estimateSize() {
      return spliterator.estimateSize();
    }

    @Override
    public int characteristics() {
      return spliterator.characteristics();
    }
  }

  /* --------------- Manual Cache --------------- */

  static class UnboundedLocalManualCache implements LocalManualCache, Serializable {
    private static final long serialVersionUID = 1;

    final UnboundedLocalCache cache;
    @Nullable Policy policy;

    UnboundedLocalManualCache(Caffeine builder) {
      cache = new UnboundedLocalCache<>(builder, /* async */ false);
    }

    @Override
    public UnboundedLocalCache cache() {
      return cache;
    }

    @Override
    public Policy policy() {
      return (policy == null)
          ? (policy = new UnboundedPolicy<>(cache, identity()))
          : policy;
    }

    @SuppressWarnings("UnusedVariable")
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
      throw new InvalidObjectException("Proxy required");
    }

    Object writeReplace() {
      SerializationProxy proxy = new SerializationProxy<>();
      proxy.isRecordingStats = cache.isRecordingStats;
      proxy.removalListener = cache.removalListener;
      proxy.ticker = cache.ticker;
      return proxy;
    }
  }

  /** An eviction policy that supports no boundings. */
  static final class UnboundedPolicy implements Policy {
    final UnboundedLocalCache cache;
    final Function transformer;

    UnboundedPolicy(UnboundedLocalCache cache, Function transformer) {
      this.transformer = transformer;
      this.cache = cache;
    }
    @Override public boolean isRecordingStats() {
      return cache.isRecordingStats;
    }
    @Override public @Nullable V getIfPresentQuietly(K key) {
      return transformer.apply(cache.data.get(key));
    }
    @Override public @Nullable CacheEntry getEntryIfPresentQuietly(K key) {
      V value = transformer.apply(cache.data.get(key));
      return (value == null) ? null : SnapshotEntry.forEntry(key, value);
    }
    @Override public Map> refreshes() {
      var refreshes = cache.refreshes;
      if ((refreshes == null) || refreshes.isEmpty()) {
        @SuppressWarnings("ImmutableMapOf")
        Map> emptyMap = Collections.unmodifiableMap(Collections.emptyMap());
        return emptyMap;
      }
      @SuppressWarnings("unchecked")
      var castedRefreshes = (Map>) (Object) refreshes;
      return Collections.unmodifiableMap(new HashMap<>(castedRefreshes));
    }
    @Override public Optional> eviction() {
      return Optional.empty();
    }
    @Override public Optional> expireAfterAccess() {
      return Optional.empty();
    }
    @Override public Optional> expireAfterWrite() {
      return Optional.empty();
    }
    @Override public Optional> expireVariably() {
      return Optional.empty();
    }
    @Override public Optional> refreshAfterWrite() {
      return Optional.empty();
    }
  }

  /* --------------- Loading Cache --------------- */

  static final class UnboundedLocalLoadingCache extends UnboundedLocalManualCache
      implements LocalLoadingCache {
    private static final long serialVersionUID = 1;

    final Function mappingFunction;
    final CacheLoader cacheLoader;
    @Nullable final Function, Map> bulkMappingFunction;

    UnboundedLocalLoadingCache(Caffeine builder, CacheLoader cacheLoader) {
      super(builder);
      this.cacheLoader = cacheLoader;
      this.mappingFunction = newMappingFunction(cacheLoader);
      this.bulkMappingFunction = newBulkMappingFunction(cacheLoader);
    }

    @Override
    public AsyncCacheLoader cacheLoader() {
      return cacheLoader;
    }

    @Override
    public Function mappingFunction() {
      return mappingFunction;
    }

    @Override
    public @Nullable Function, Map>  bulkMappingFunction() {
      return bulkMappingFunction;
    }

    @Override
    Object writeReplace() {
      @SuppressWarnings("unchecked")
      var proxy = (SerializationProxy) super.writeReplace();
      proxy.cacheLoader = cacheLoader;
      return proxy;
    }

    @SuppressWarnings("UnusedVariable")
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
      throw new InvalidObjectException("Proxy required");
    }
  }

  /* --------------- Async Cache --------------- */

  static final class UnboundedLocalAsyncCache implements LocalAsyncCache, Serializable {
    private static final long serialVersionUID = 1;

    final UnboundedLocalCache> cache;

    @Nullable ConcurrentMap> mapView;
    @Nullable CacheView cacheView;
    @Nullable Policy policy;

    @SuppressWarnings("unchecked")
    UnboundedLocalAsyncCache(Caffeine builder) {
      cache = new UnboundedLocalCache<>(
          (Caffeine>) builder, /* async */ true);
    }

    @Override
    public UnboundedLocalCache> cache() {
      return cache;
    }

    @Override
    public ConcurrentMap> asMap() {
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
    }

    @Override
    public Cache synchronous() {
      return (cacheView == null) ? (cacheView = new CacheView<>(this)) : cacheView;
    }

    @Override
    public Policy policy() {
      @SuppressWarnings("unchecked")
      UnboundedLocalCache castCache = (UnboundedLocalCache) cache;
      Function, V> transformer = Async::getIfReady;
      @SuppressWarnings("unchecked")
      Function castTransformer = (Function) transformer;
      return (policy == null)
          ? (policy = new UnboundedPolicy<>(castCache, castTransformer))
          : policy;
    }

    @SuppressWarnings("UnusedVariable")
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
      throw new InvalidObjectException("Proxy required");
    }

    Object writeReplace() {
      SerializationProxy proxy = new SerializationProxy<>();
      proxy.isRecordingStats = cache.isRecordingStats;
      proxy.removalListener = cache.removalListener;
      proxy.ticker = cache.ticker;
      proxy.async = true;
      return proxy;
    }
  }

  /* --------------- Async Loading Cache --------------- */

  static final class UnboundedLocalAsyncLoadingCache
      extends LocalAsyncLoadingCache implements Serializable {
    private static final long serialVersionUID = 1;

    final UnboundedLocalCache> cache;

    @Nullable ConcurrentMap> mapView;
    @Nullable Policy policy;

    @SuppressWarnings("unchecked")
    UnboundedLocalAsyncLoadingCache(Caffeine builder, AsyncCacheLoader loader) {
      super(loader);
      cache = new UnboundedLocalCache<>(
          (Caffeine>) builder, /* async */ true);
    }

    @Override
    public LocalCache> cache() {
      return cache;
    }

    @Override
    public ConcurrentMap> asMap() {
      return (mapView == null) ? (mapView = new AsyncAsMapView<>(this)) : mapView;
    }

    @Override
    public Policy policy() {
      @SuppressWarnings("unchecked")
      UnboundedLocalCache castCache = (UnboundedLocalCache) cache;
      Function, V> transformer = Async::getIfReady;
      @SuppressWarnings("unchecked")
      Function castTransformer = (Function) transformer;
      return (policy == null)
          ? (policy = new UnboundedPolicy<>(castCache, castTransformer))
          : policy;
    }

    @SuppressWarnings("UnusedVariable")
    private void readObject(ObjectInputStream stream) throws InvalidObjectException {
      throw new InvalidObjectException("Proxy required");
    }

    Object writeReplace() {
      SerializationProxy proxy = new SerializationProxy<>();
      proxy.isRecordingStats = cache.isRecordingStats();
      proxy.removalListener = cache.removalListener;
      proxy.cacheLoader = cacheLoader;
      proxy.ticker = cache.ticker;
      proxy.async = true;
      return proxy;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy