From b117a26310a578bdad752bfcbcf2e71b2798afdf Mon Sep 17 00:00:00 2001 From: YuTian <2953516620@qq.com> Date: Thu, 6 Feb 2025 01:32:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=A4=9A=E4=B8=AA=E7=B1=BB?= =?UTF-8?q?=E4=BB=A5=E6=94=AF=E6=8C=81=E8=BF=87=E6=9C=9F=E7=AD=96=E7=95=A5?= =?UTF-8?q?=E5=92=8C=E5=BA=8F=E5=88=97=E5=8C=96=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E5=8C=85=E6=8B=ACAssert.java=E3=80=81ContainerGui.java?= =?UTF-8?q?=E3=80=81EntryLoader.java=E3=80=81ExpirationListener.java?= =?UTF-8?q?=E3=80=81ExpirationPolicy.java=E3=80=81ExpiringEntryLoader.java?= =?UTF-8?q?=E3=80=81ExpiringMap.java=E3=80=81ExpiringValue.java=E3=80=81Gu?= =?UTF-8?q?i.java=E3=80=81NamedThreadFactory.java=E3=80=81PageList.java?= =?UTF-8?q?=E3=80=81SerializeHelper.java=EF=BC=8C=E5=B9=B6=E4=BF=AE?= =?UTF-8?q?=E6=94=B9pom.xml=E4=BB=A5=E5=BC=95=E5=85=A5=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E7=9A=84=E4=BE=9D=E8=B5=96=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 5 + .../expiringmap/EntryLoader.java | 16 +- .../expiringmap/ExpirationListener.java | 16 +- .../expiringmap/ExpirationPolicy.java | 14 +- .../expiringmap/ExpiringEntryLoader.java | 14 +- .../expiringmap/ExpiringMap.java | 2669 +++++++++-------- .../expiringmap/ExpiringValue.java | 214 +- .../expiringmap/internal/Assert.java | 38 +- .../internal/NamedThreadFactory.java | 30 +- .../elementoriginlib/gui/ContainerGui.java | 66 + .../io/yutian/elementoriginlib/gui/Gui.java | 2 +- .../elementoriginlib/list/PageList.java | 58 +- .../serialize/SerializeHelper.java | 61 +- 13 files changed, 1656 insertions(+), 1547 deletions(-) create mode 100644 src/main/java/com/io/yutian/elementoriginlib/gui/ContainerGui.java diff --git a/pom.xml b/pom.xml index 03d487d..d2f7b11 100644 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,11 @@ jackson-databind 2.18.2 + + com.fasterxml.jackson.module + jackson-module-afterburner + 2.18.2 + com.github.luben zstd-jni diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java index 6903cdd..2b1a850 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/EntryLoader.java @@ -2,16 +2,16 @@ package com.io.yutian.elementoriginlib.expiringmap; /** * Loads entries on demand. - * + * * @param Key type * @param Value type */ public interface EntryLoader { - /** - * Called to load a new value for the {@code key} into an expiring map. - * - * @param key to load a value for - * @return new value to load - */ - V load(K key); + /** + * Called to load a new value for the {@code key} into an expiring map. + * + * @param key to load a value for + * @return new value to load + */ + V load(K key); } \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java index ac1275e..609d687 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationListener.java @@ -2,16 +2,16 @@ package com.io.yutian.elementoriginlib.expiringmap; /** * A listener for expired object events. - * + * * @param Key type * @param Value type */ public interface ExpirationListener { - /** - * Called when a map entry expires. - * - * @param key Expired key - * @param value Expired value - */ - void expired(K key, V value); + /** + * Called when a map entry expires. + * + * @param key Expired key + * @param value Expired value + */ + void expired(K key, V value); } \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java index 24736c9..f2d7ecb 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpirationPolicy.java @@ -1,11 +1,15 @@ package com.io.yutian.elementoriginlib.expiringmap; -/** +/** * Determines how ExpiringMap entries should be expired. */ public enum ExpirationPolicy { - /** Expires entries based on when they were last accessed */ - ACCESSED, - /** Expires entries based on when they were created */ - CREATED; + /** + * Expires entries based on when they were last accessed + */ + ACCESSED, + /** + * Expires entries based on when they were created + */ + CREATED; } \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java index aef842f..5cbf29a 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringEntryLoader.java @@ -7,11 +7,11 @@ package com.io.yutian.elementoriginlib.expiringmap; * @param Value type */ public interface ExpiringEntryLoader { - /** - * Called to load a new value for the {@code key} into an expiring map. - * - * @param key to load a value for - * @return contains new value to load along with its expiry duration - */ - ExpiringValue load(K key); + /** + * Called to load a new value for the {@code key} into an expiring map. + * + * @param key to load a value for + * @return contains new value to load along with its expiry duration + */ + ExpiringValue load(K key); } \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java index 475bfa2..89f8376 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringMap.java @@ -1,55 +1,35 @@ package com.io.yutian.elementoriginlib.expiringmap; +import com.io.yutian.elementoriginlib.expiringmap.internal.Assert; +import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory; + import java.lang.ref.WeakReference; -import java.util.AbstractCollection; -import java.util.AbstractSet; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; -import java.util.SortedSet; -import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.*; +import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReadWriteLock; import java.util.concurrent.locks.ReentrantReadWriteLock; -import com.io.yutian.elementoriginlib.expiringmap.internal.Assert; -import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory; - /** * A thread-safe map that expires entries. Optional features include expiration policies, variable entry expiration, * lazy entry loading, and expiration listeners. - * + * *

* Entries are tracked by expiration time and expired by a single thread. - * + * *

* Expiration listeners are called synchronously as entries are expired and block write operations to the map until they * completed. Asynchronous expiration listeners are called on a separate thread pool and do not block map operations. - * + * *

* When variable expiration is disabled (default), put/remove operations have a time complexity O(1). When * variable expiration is enabled, put/remove operations have time complexity of O(log n). - * + * *

* Example usages: - * + * *

  * {@code
  * Map map = ExpiringMap.create();
@@ -69,115 +49,332 @@ import com.io.yutian.elementoriginlib.expiringmap.internal.NamedThreadFactory;
  *   .build();
  * }
  * 
- * - * @author Jonathan Halterman + * * @param Key type * @param Value type + * @author Jonathan Halterman */ public class ExpiringMap implements ConcurrentMap { - static volatile ScheduledExecutorService EXPIRER; - static volatile ThreadPoolExecutor LISTENER_SERVICE; - static ThreadFactory THREAD_FACTORY; - - List> expirationListeners; - List> asyncExpirationListeners; - private AtomicLong expirationNanos; - private int maxSize; - private final AtomicReference expirationPolicy; - private final EntryLoader entryLoader; - private final ExpiringEntryLoader expiringEntryLoader; - private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); - private final Lock readLock = readWriteLock.readLock(); - private final Lock writeLock = readWriteLock.writeLock(); - /** Guarded by "readWriteLock" */ - private final EntryMap entries; - private final boolean variableExpiration; - - /** - * Sets the {@link ThreadFactory} that is used to create expiration and listener callback threads for all ExpiringMap - * instances. - * - * @param threadFactory - * @throws NullPointerException if {@code threadFactory} is null - */ - public static void setThreadFactory(ThreadFactory threadFactory) { - THREAD_FACTORY = Assert.notNull(threadFactory, "threadFactory"); - } - - /** - * Creates a new instance of ExpiringMap. - * - * @param builder The map builder - */ - private ExpiringMap(final Builder builder) { - if (EXPIRER == null) { - synchronized (ExpiringMap.class) { - if (EXPIRER == null) { - EXPIRER = Executors.newSingleThreadScheduledExecutor( - THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Expirer") : THREAD_FACTORY); - } - } - } - - if (LISTENER_SERVICE == null && builder.asyncExpirationListeners != null) - initListenerService(); - - variableExpiration = builder.variableExpiration; - entries = variableExpiration ? new EntryTreeHashMap() : new EntryLinkedHashMap(); - if (builder.expirationListeners != null) - expirationListeners = new CopyOnWriteArrayList>(builder.expirationListeners); - if (builder.asyncExpirationListeners != null) - asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners); - expirationPolicy = new AtomicReference(builder.expirationPolicy); - expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit)); - maxSize = builder.maxSize; - entryLoader = builder.entryLoader; - expiringEntryLoader = builder.expiringEntryLoader; - } - - /** - * Builds ExpiringMap instances. Defaults to ExpirationPolicy.CREATED, expiration of 60 TimeUnit.SECONDS and - * a maxSize of Integer.MAX_VALUE. - */ - public static final class Builder { - private ExpirationPolicy expirationPolicy = ExpirationPolicy.CREATED; - private List> expirationListeners; - private List> asyncExpirationListeners; - private TimeUnit timeUnit = TimeUnit.SECONDS; - private boolean variableExpiration; - private long duration = 60; - private int maxSize = Integer.MAX_VALUE; - private EntryLoader entryLoader; - private ExpiringEntryLoader expiringEntryLoader; - + static volatile ScheduledExecutorService EXPIRER; + static volatile ThreadPoolExecutor LISTENER_SERVICE; + static ThreadFactory THREAD_FACTORY; + private final AtomicReference expirationPolicy; + private final EntryLoader entryLoader; + private final ExpiringEntryLoader expiringEntryLoader; + private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); + private final Lock readLock = readWriteLock.readLock(); + private final Lock writeLock = readWriteLock.writeLock(); /** - * Creates a new Builder object. + * Guarded by "readWriteLock" */ - private Builder() { + private final EntryMap entries; + private final boolean variableExpiration; + List> expirationListeners; + List> asyncExpirationListeners; + private AtomicLong expirationNanos; + private int maxSize; + + /** + * Creates a new instance of ExpiringMap. + * + * @param builder The map builder + */ + private ExpiringMap(final Builder builder) { + if (EXPIRER == null) { + synchronized (ExpiringMap.class) { + if (EXPIRER == null) { + EXPIRER = Executors.newSingleThreadScheduledExecutor( + THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Expirer") : THREAD_FACTORY); + } + } + } + + if (LISTENER_SERVICE == null && builder.asyncExpirationListeners != null) + initListenerService(); + + variableExpiration = builder.variableExpiration; + entries = variableExpiration ? new EntryTreeHashMap() : new EntryLinkedHashMap(); + if (builder.expirationListeners != null) + expirationListeners = new CopyOnWriteArrayList>(builder.expirationListeners); + if (builder.asyncExpirationListeners != null) + asyncExpirationListeners = new CopyOnWriteArrayList>(builder.asyncExpirationListeners); + expirationPolicy = new AtomicReference(builder.expirationPolicy); + expirationNanos = new AtomicLong(TimeUnit.NANOSECONDS.convert(builder.duration, builder.timeUnit)); + maxSize = builder.maxSize; + entryLoader = builder.entryLoader; + expiringEntryLoader = builder.expiringEntryLoader; } /** - * Builds and returns an expiring map. - * - * @param Key type - * @param Value type + * Sets the {@link ThreadFactory} that is used to create expiration and listener callback threads for all ExpiringMap + * instances. + * + * @param threadFactory + * @throws NullPointerException if {@code threadFactory} is null + */ + public static void setThreadFactory(ThreadFactory threadFactory) { + THREAD_FACTORY = Assert.notNull(threadFactory, "threadFactory"); + } + + /** + * Creates an ExpiringMap builder. + * + * @return New ExpiringMap builder + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Creates a new instance of ExpiringMap with ExpirationPolicy.CREATED and an expiration of 60 seconds. */ @SuppressWarnings("unchecked") - public ExpiringMap build() { - return new ExpiringMap((Builder) this); + public static ExpiringMap create() { + return new ExpiringMap((Builder) ExpiringMap.builder()); + } + + private static Entry mapEntryFor(final ExpiringEntry entry) { + return new Entry() { + @Override + public K getKey() { + return entry.key; + } + + @Override + public V getValue() { + return entry.value; + } + + @Override + public V setValue(V value) { + throw new UnsupportedOperationException(); + } + }; } /** - * Sets the default map entry expiration. - * - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @throws NullPointerException if {@code timeUnit} is null + * Adds an expiration listener. + * + * @param listener to add + * @throws NullPointerException if {@code listener} is null */ - public Builder expiration(long duration, TimeUnit timeUnit) { - this.duration = duration; - this.timeUnit = Assert.notNull(timeUnit, "timeUnit"); - return this; + public synchronized void addExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (expirationListeners == null) + expirationListeners = new CopyOnWriteArrayList>(); + expirationListeners.add(listener); + } + + /** + * Adds an asynchronous expiration listener. + * + * @param listener to add + * @throws NullPointerException if {@code listener} is null + */ + public synchronized void addAsyncExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new CopyOnWriteArrayList>(); + asyncExpirationListeners.add(listener); + // If asyncListener was not added on Builder, LISTENER_SERVICE was not initialized and remain null + if (LISTENER_SERVICE == null) + initListenerService(); + } + + @Override + public void clear() { + writeLock.lock(); + try { + for (ExpiringEntry entry : entries.values()) + entry.cancel(); + entries.clear(); + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean containsKey(Object key) { + readLock.lock(); + try { + return entries.containsKey(key); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean containsValue(Object value) { + readLock.lock(); + try { + return entries.containsValue(value); + } finally { + readLock.unlock(); + } + } + + /** + * Returns a copy of the map's entries, which can be iterated over safely by multiple threads. + * + * @return Copied set of map entries. + */ + @Override + public Set> entrySet() { + return new AbstractSet>() { + @Override + public void clear() { + ExpiringMap.this.clear(); + } + + @Override + public boolean contains(Object entry) { + if (!(entry instanceof Map.Entry)) + return false; + Entry e = (Entry) entry; + return containsKey(e.getKey()); + } + + @Override + public Iterator> iterator() { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new EntryIterator() + : ((EntryTreeHashMap) entries).new EntryIterator(); + } + + @Override + public boolean remove(Object entry) { + if (entry instanceof Map.Entry) { + Entry e = (Entry) entry; + return ExpiringMap.this.remove(e.getKey()) != null; + } + return false; + } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + @Override + public boolean equals(Object obj) { + readLock.lock(); + try { + return entries.equals(obj); + } finally { + readLock.unlock(); + } + } + + @Override + @SuppressWarnings("unchecked") + public V get(Object key) { + ExpiringEntry entry = getEntry(key); + + if (entry == null) { + return load((K) key); + } else if (ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get())) + resetEntry(entry, false); + + return entry.getValue(); + } + + private V load(K key) { + if (entryLoader == null && expiringEntryLoader == null) + return null; + + writeLock.lock(); + try { + // Double check for entry + ExpiringEntry entry = getEntry(key); + if (entry != null) + return entry.getValue(); + + if (entryLoader != null) { + V value = entryLoader.load(key); + put(key, value); + return value; + } else { + ExpiringValue expiringValue = expiringEntryLoader.load(key); + if (expiringValue == null) { + put(key, null); + return null; + } else { + long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration(); + TimeUnit timeUnit = expiringValue.getTimeUnit() == null ? TimeUnit.NANOSECONDS : expiringValue.getTimeUnit(); + put(key, expiringValue.getValue(), expiringValue.getExpirationPolicy() == null ? expirationPolicy.get() + : expiringValue.getExpirationPolicy(), duration, timeUnit); + return expiringValue.getValue(); + } + } + } finally { + writeLock.unlock(); + } + } + + /** + * Returns the map's default expiration duration in milliseconds. + * + * @return The expiration duration (milliseconds) + */ + public long getExpiration() { + return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get()); + } + + /** + * Gets the expiration duration in milliseconds for the entry corresponding to the given key. + * + * @param key + * @return The expiration duration in milliseconds + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public long getExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return TimeUnit.NANOSECONDS.toMillis(entry.expirationNanos.get()); + } + + /** + * Gets the ExpirationPolicy for the entry corresponding to the given {@code key}. + * + * @param key + * @return The ExpirationPolicy for the {@code key} + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public ExpirationPolicy getExpirationPolicy(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return entry.expirationPolicy.get(); + } + + /** + * Gets the expected expiration, in milliseconds from the current time, for the entry corresponding to the given + * {@code key}. + * + * @param key + * @return The expiration duration in milliseconds + * @throws NullPointerException if {@code key} is null + * @throws NoSuchElementException If no entry exists for the given key + */ + public long getExpectedExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + Assert.element(entry, key); + return TimeUnit.NANOSECONDS.toMillis(entry.expectedExpiration.get() - System.nanoTime()); + } + + /** + * Gets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the + * first entry in line for expiration based on the expiration policy. + * + * @return The maximum size of the map. + */ + public int getMaxSize() { + return maxSize; } /** @@ -186,1237 +383,1049 @@ public class ExpiringMap implements ConcurrentMap { * * @param maxSize The maximum size of the map. */ - public Builder maxSize(int maxSize) { - Assert.operation(maxSize > 0, "maxSize"); - this.maxSize = maxSize; - return this; - } - - /** - * Sets the EntryLoader to use when loading entries. Either an EntryLoader or ExpiringEntryLoader may be set, not - * both. - * - * @param loader to set - * @throws NullPointerException if {@code loader} is null - * @throws IllegalStateException if an {@link #expiringEntryLoader(ExpiringEntryLoader) ExpiringEntryLoader} is set - */ - @SuppressWarnings("unchecked") - public Builder entryLoader(EntryLoader loader) { - assertNoLoaderSet(); - entryLoader = (EntryLoader) Assert.notNull(loader, "loader"); - return (Builder) this; - } - - /** - * Sets the ExpiringEntryLoader to use when loading entries and configures {@link #variableExpiration() variable - * expiration}. Either an EntryLoader or ExpiringEntryLoader may be set, not both. - * - * @param loader to set - * @throws NullPointerException if {@code loader} is null - * @throws IllegalStateException if an {@link #entryLoader(EntryLoader) EntryLoader} is set - */ - @SuppressWarnings("unchecked") - public Builder expiringEntryLoader( - ExpiringEntryLoader loader) { - assertNoLoaderSet(); - expiringEntryLoader = (ExpiringEntryLoader) Assert.notNull(loader, "loader"); - variableExpiration(); - return (Builder) this; - } - - /** - * Configures the expiration listener that will receive notifications upon each map entry's expiration. - * Notifications are delivered synchronously and block map write operations. - * - * @param listener to set - * @throws NullPointerException if {@code listener} is null - */ - @SuppressWarnings("unchecked") - public Builder expirationListener( - ExpirationListener listener) { - Assert.notNull(listener, "listener"); - if (expirationListeners == null) - expirationListeners = new ArrayList>(); - expirationListeners.add((ExpirationListener) listener); - return (Builder) this; - } - - /** - * Configures the expiration listeners which will receive notifications upon each map entry's expiration. - * Notifications are delivered synchronously and block map write operations. - * - * @param listeners to set - * @throws NullPointerException if {@code listener} is null - */ - @SuppressWarnings("unchecked") - public Builder expirationListeners( - List> listeners) { - Assert.notNull(listeners, "listeners"); - if (expirationListeners == null) - expirationListeners = new ArrayList>(listeners.size()); - for (ExpirationListener listener : listeners) - expirationListeners.add((ExpirationListener) listener); - return (Builder) this; - } - - /** - * Configures the expiration listener which will receive asynchronous notifications upon each map entry's - * expiration. - * - * @param listener to set - * @throws NullPointerException if {@code listener} is null - */ - @SuppressWarnings("unchecked") - public Builder asyncExpirationListener( - ExpirationListener listener) { - Assert.notNull(listener, "listener"); - if (asyncExpirationListeners == null) - asyncExpirationListeners = new ArrayList>(); - asyncExpirationListeners.add((ExpirationListener) listener); - return (Builder) this; - } - - /** - * Configures the expiration listeners which will receive asynchronous notifications upon each map entry's - * expiration. - * - * @param listeners to set - * @throws NullPointerException if {@code listener} is null - */ - @SuppressWarnings("unchecked") - public Builder asyncExpirationListeners( - List> listeners) { - Assert.notNull(listeners, "listeners"); - if (asyncExpirationListeners == null) - asyncExpirationListeners = new ArrayList>(listeners.size()); - for (ExpirationListener listener : listeners) - asyncExpirationListeners.add((ExpirationListener) listener); - return (Builder) this; - } - - /** - * Configures the map entry expiration policy. - * - * @param expirationPolicy - * @throws NullPointerException if {@code expirationPolicy} is null - */ - public Builder expirationPolicy(ExpirationPolicy expirationPolicy) { - this.expirationPolicy = Assert.notNull(expirationPolicy, "expirationPolicy"); - return this; - } - - /** - * Allows for map entries to have individual expirations and for expirations to be changed. - */ - public Builder variableExpiration() { - variableExpiration = true; - return this; - } - - private void assertNoLoaderSet() { - Assert.state(entryLoader == null && expiringEntryLoader == null, - "Either entryLoader or expiringEntryLoader may be set, not both"); - } - } - - /** Entry map definition. */ - private interface EntryMap extends Map> { - /** Returns the first entry in the map or null if the map is empty. */ - ExpiringEntry first(); - - /** - * Reorders the given entry in the map. - * - * @param entry to reorder - */ - void reorder(ExpiringEntry entry); - - /** Returns a values iterator. */ - Iterator> valuesIterator(); - } - - /** Entry LinkedHashMap implementation. */ - private static class EntryLinkedHashMap extends LinkedHashMap> - implements EntryMap { - private static final long serialVersionUID = 1L; - - @Override - public boolean containsValue(Object value) { - for (ExpiringEntry entry : values()) { - V v = entry.value; - if (v == value || (value != null && value.equals(v))) - return true; - } - return false; - } - - @Override - public ExpiringEntry first() { - return isEmpty() ? null : values().iterator().next(); - } - - @Override - public void reorder(ExpiringEntry value) { - remove(value.key); - value.resetExpiration(); - put(value.key, value); - } - - @Override - public Iterator> valuesIterator() { - return values().iterator(); - } - - abstract class AbstractHashIterator { - private final Iterator>> iterator; - private ExpiringEntry next; - - @SuppressWarnings({"unchecked", "rawtypes"}) - AbstractHashIterator() { - iterator = (Iterator) Arrays.asList(entrySet().toArray(new Map.Entry[0])).iterator(); - } - - public boolean hasNext() { - return iterator.hasNext(); - } - - public ExpiringEntry getNext() { - next = iterator.next().getValue(); - return next; - } - - public void remove() { - iterator.remove(); - } - } - - final class KeyIterator extends AbstractHashIterator implements Iterator { - public K next() { - return getNext().key; - } - } - - final class ValueIterator extends AbstractHashIterator implements Iterator { - public V next() { - return getNext().value; - } - } - - public final class EntryIterator extends AbstractHashIterator implements Iterator> { - public Map.Entry next() { - return mapEntryFor(getNext()); - } - } - } - - /** Entry TreeHashMap implementation for variable expiration ExpiringMap entries. */ - private static class EntryTreeHashMap extends HashMap> implements EntryMap { - private static final long serialVersionUID = 1L; - SortedSet> sortedSet = new ConcurrentSkipListSet>(); - - @Override - public void clear() { - super.clear(); - sortedSet.clear(); - } - - @Override - public boolean containsValue(Object value) { - for (ExpiringEntry entry : values()) { - V v = entry.value; - if (v == value || (value != null && value.equals(v))) - return true; - } - return false; - } - - @Override - public ExpiringEntry first() { - return sortedSet.isEmpty() ? null : sortedSet.first(); - } - - @Override - public ExpiringEntry put(K key, ExpiringEntry value) { - sortedSet.add(value); - return super.put(key, value); - } - - @Override - public ExpiringEntry remove(Object key) { - ExpiringEntry entry = super.remove(key); - if (entry != null) - sortedSet.remove(entry); - return entry; - } - - @Override - public void reorder(ExpiringEntry value) { - sortedSet.remove(value); - value.resetExpiration(); - sortedSet.add(value); - } - - @Override - public Iterator> valuesIterator() { - return new ExpiringEntryIterator(); - } - - abstract class AbstractHashIterator { - private final Iterator> iterator = sortedSet.iterator(); - protected ExpiringEntry next; - - public boolean hasNext() { - return iterator.hasNext(); - } - - public ExpiringEntry getNext() { - next = iterator.next(); - return next; - } - - public void remove() { - EntryTreeHashMap.super.remove(next.key); - iterator.remove(); - } - } - - final class ExpiringEntryIterator extends AbstractHashIterator implements Iterator> { - public final ExpiringEntry next() { - return getNext(); - } - } - - final class KeyIterator extends AbstractHashIterator implements Iterator { - public final K next() { - return getNext().key; - } - } - - final class ValueIterator extends AbstractHashIterator implements Iterator { - public final V next() { - return getNext().value; - } - } - - final class EntryIterator extends AbstractHashIterator implements Iterator> { - public final Entry next() { - return mapEntryFor(getNext()); - } - } - } - - /** Expiring map entry implementation. */ - static class ExpiringEntry implements Comparable> { - final AtomicLong expirationNanos; - /** Epoch time at which the entry is expected to expire */ - final AtomicLong expectedExpiration; - final AtomicReference expirationPolicy; - final K key; - /** Guarded by "this" */ - volatile Future entryFuture; - /** Guarded by "this" */ - V value; - /** Guarded by "this" */ - volatile boolean scheduled; - - /** - * Creates a new ExpiringEntry object. - * - * @param key for the entry - * @param value for the entry - * @param expirationPolicy for the entry - * @param expirationNanos for the entry - */ - ExpiringEntry(K key, V value, AtomicReference expirationPolicy, AtomicLong expirationNanos) { - this.key = key; - this.value = value; - this.expirationPolicy = expirationPolicy; - this.expirationNanos = expirationNanos; - this.expectedExpiration = new AtomicLong(); - resetExpiration(); - } - - @Override - public int compareTo(ExpiringEntry other) { - if (key.equals(other.key)) - return 0; - return expectedExpiration.get() < other.expectedExpiration.get() ? -1 : 1; + public void setMaxSize(int maxSize) { + Assert.operation(maxSize > 0, "maxSize"); + this.maxSize = maxSize; } @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((key == null) ? 0 : key.hashCode()); - result = prime * result + ((value == null) ? 0 : value.hashCode()); - return result; + readLock.lock(); + try { + return entries.hashCode(); + } finally { + readLock.unlock(); + } } @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - ExpiringEntry other = (ExpiringEntry) obj; - if (!key.equals(other.key)) - return false; - if (value == null) { - if (other.value != null) - return false; - } else if (!value.equals(other.value)) - return false; - return true; + public boolean isEmpty() { + readLock.lock(); + try { + return entries.isEmpty(); + } finally { + readLock.unlock(); + } + } + + /** + * Returns a copy of the map's keys, which can be iterated over safely by multiple threads. + * + * @return Copied set of map keys. + */ + @Override + public Set keySet() { + return new AbstractSet() { + @Override + public void clear() { + ExpiringMap.this.clear(); + } + + @Override + public boolean contains(Object key) { + return containsKey(key); + } + + @Override + public Iterator iterator() { + readLock.lock(); + try { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new KeyIterator() + : ((EntryTreeHashMap) entries).new KeyIterator(); + } finally { + readLock.unlock(); + } + } + + @Override + public boolean remove(Object value) { + return ExpiringMap.this.remove(value) != null; + } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + /** + * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the + * same {@code key} and {@code value}. + * + * @param key to put value for + * @param value to put for key + * @return the old value + * @throws NullPointerException if {@code key} is null + */ + @Override + public V put(K key, V value) { + Assert.notNull(key, "key"); + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + } + + /** + * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) + */ + public V put(K key, V value, ExpirationPolicy expirationPolicy) { + return put(key, value, expirationPolicy, expirationNanos.get(), TimeUnit.NANOSECONDS); + } + + /** + * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) + */ + public V put(K key, V value, long duration, TimeUnit timeUnit) { + return put(key, value, expirationPolicy.get(), duration, timeUnit); + } + + /** + * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the + * same {@code key} and {@code value}. Requires that variable expiration be enabled. + * + * @param key Key to put value for + * @param value Value to put for key + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @return the old value + * @throws UnsupportedOperationException If variable expiration is not enabled + * @throws NullPointerException if {@code key}, {@code expirationPolicy} or {@code timeUnit} are null + */ + public V put(K key, V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { + Assert.notNull(key, "key"); + Assert.notNull(expirationPolicy, "expirationPolicy"); + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + return putInternal(key, value, expirationPolicy, TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + } + + @Override + public void putAll(Map map) { + Assert.notNull(map, "map"); + long expiration = expirationNanos.get(); + ExpirationPolicy expirationPolicy = this.expirationPolicy.get(); + writeLock.lock(); + try { + for (Entry entry : map.entrySet()) + putInternal(entry.getKey(), entry.getValue(), expirationPolicy, expiration); + } finally { + writeLock.unlock(); + } + } + + @Override + public V putIfAbsent(K key, V value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + if (!entries.containsKey(key)) + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + else + return entries.get(key).getValue(); + } finally { + writeLock.unlock(); + } + } + + @Override + public V remove(Object key) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.remove(key); + if (entry == null) + return null; + if (entry.cancel()) + scheduleEntry(entries.first()); + return entry.getValue(); + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean remove(Object key, Object value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null && entry.getValue().equals(value)) { + entries.remove(key); + if (entry.cancel()) + scheduleEntry(entries.first()); + return true; + } else + return false; + } finally { + writeLock.unlock(); + } + } + + @Override + public V replace(K key, V value) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + if (entries.containsKey(key)) { + return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); + } else + return null; + } finally { + writeLock.unlock(); + } + } + + @Override + public boolean replace(K key, V oldValue, V newValue) { + Assert.notNull(key, "key"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null && entry.getValue().equals(oldValue)) { + putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get()); + return true; + } else + return false; + } finally { + writeLock.unlock(); + } + } + + /** + * Removes an expiration listener. + * + * @param listener + * @throws NullPointerException if {@code listener} is null + */ + public void removeExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + for (int i = 0; i < expirationListeners.size(); i++) { + if (expirationListeners.get(i).equals(listener)) { + expirationListeners.remove(i); + return; + } + } + } + + /** + * Removes an asynchronous expiration listener. + * + * @param listener + * @throws NullPointerException if {@code listener} is null + */ + public void removeAsyncExpirationListener(ExpirationListener listener) { + Assert.notNull(listener, "listener"); + for (int i = 0; i < asyncExpirationListeners.size(); i++) { + if (asyncExpirationListeners.get(i).equals(listener)) { + asyncExpirationListeners.remove(i); + return; + } + } + } + + /** + * Resets expiration for the entry corresponding to {@code key}. + * + * @param key to reset expiration for + * @throws NullPointerException if {@code key} is null + */ + public void resetExpiration(K key) { + Assert.notNull(key, "key"); + ExpiringEntry entry = getEntry(key); + if (entry != null) + resetEntry(entry, false); + } + + /** + * Sets the expiration duration for the entry corresponding to the given key. Supported only if variable expiration is + * enabled. + * + * @param key Key to set expiration for + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException if {@code key} or {@code timeUnit} are null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpiration(K key, long duration, TimeUnit timeUnit) { + Assert.notNull(key, "key"); + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + if (entry != null) { + entry.expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + resetEntry(entry, true); + } + } finally { + writeLock.unlock(); + } + } + + /** + * Updates the default map entry expiration. Supported only if variable expiration is enabled. + * + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException {@code timeUnit} is null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpiration(long duration, TimeUnit timeUnit) { + Assert.notNull(timeUnit, "timeUnit"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); + } + + /** + * Sets the global expiration policy for the map. Individual expiration policies may override the global policy. + * + * @param expirationPolicy + * @throws NullPointerException {@code expirationPolicy} is null + */ + public void setExpirationPolicy(ExpirationPolicy expirationPolicy) { + Assert.notNull(expirationPolicy, "expirationPolicy"); + this.expirationPolicy.set(expirationPolicy); + } + + /** + * Sets the expiration policy for the entry corresponding to the given key. + * + * @param key to set policy for + * @param expirationPolicy to set + * @throws NullPointerException if {@code key} or {@code expirationPolicy} are null + * @throws UnsupportedOperationException If variable expiration is not enabled + */ + public void setExpirationPolicy(K key, ExpirationPolicy expirationPolicy) { + Assert.notNull(key, "key"); + Assert.notNull(expirationPolicy, "expirationPolicy"); + Assert.operation(variableExpiration, "Variable expiration is not enabled"); + ExpiringEntry entry = getEntry(key); + if (entry != null) + entry.expirationPolicy.set(expirationPolicy); + } + + @Override + public int size() { + readLock.lock(); + try { + return entries.size(); + } finally { + readLock.unlock(); + } } @Override public String toString() { - return value.toString(); + readLock.lock(); + try { + return entries.toString(); + } finally { + readLock.unlock(); + } } /** - * Marks the entry as canceled. - * - * @return true if the entry was scheduled + * Returns a copy of the map's values, which can be iterated over safely by multiple threads. + * + * @return Copied set of map values. */ - synchronized boolean cancel() { - boolean result = scheduled; - if (entryFuture != null) - entryFuture.cancel(false); - - entryFuture = null; - scheduled = false; - return result; - } - - /** Gets the entry value. */ - synchronized V getValue() { - return value; - } - - /** Resets the entry's expected expiration. */ - void resetExpiration() { - expectedExpiration.set(expirationNanos.get() + System.nanoTime()); - } - - /** Marks the entry as scheduled. */ - synchronized void schedule(Future entryFuture) { - this.entryFuture = entryFuture; - scheduled = true; - } - - /** Sets the entry value. */ - synchronized void setValue(V value) { - this.value = value; - } - } - - /** - * Creates an ExpiringMap builder. - * - * @return New ExpiringMap builder - */ - public static Builder builder() { - return new Builder(); - } - - /** - * Creates a new instance of ExpiringMap with ExpirationPolicy.CREATED and an expiration of 60 seconds. - */ - @SuppressWarnings("unchecked") - public static ExpiringMap create() { - return new ExpiringMap((Builder) ExpiringMap.builder()); - } - - /** - * Adds an expiration listener. - * - * @param listener to add - * @throws NullPointerException if {@code listener} is null - */ - public synchronized void addExpirationListener(ExpirationListener listener) { - Assert.notNull(listener, "listener"); - if (expirationListeners == null) - expirationListeners = new CopyOnWriteArrayList>(); - expirationListeners.add(listener); - } - - /** - * Adds an asynchronous expiration listener. - * - * @param listener to add - * @throws NullPointerException if {@code listener} is null - */ - public synchronized void addAsyncExpirationListener(ExpirationListener listener) { - Assert.notNull(listener, "listener"); - if (asyncExpirationListeners == null) - asyncExpirationListeners = new CopyOnWriteArrayList>(); - asyncExpirationListeners.add(listener); - // If asyncListener was not added on Builder, LISTENER_SERVICE was not initialized and remain null - if (LISTENER_SERVICE == null) - initListenerService(); - } - - @Override - public void clear() { - writeLock.lock(); - try { - for (ExpiringEntry entry : entries.values()) - entry.cancel(); - entries.clear(); - } finally { - writeLock.unlock(); - } - } - - @Override - public boolean containsKey(Object key) { - readLock.lock(); - try { - return entries.containsKey(key); - } finally { - readLock.unlock(); - } - } - - @Override - public boolean containsValue(Object value) { - readLock.lock(); - try { - return entries.containsValue(value); - } finally { - readLock.unlock(); - } - } - - /** - * Returns a copy of the map's entries, which can be iterated over safely by multiple threads. - * - * @return Copied set of map entries. - */ - @Override - public Set> entrySet() { - return new AbstractSet>() { - @Override - public void clear() { - ExpiringMap.this.clear(); - } - - @Override - public boolean contains(Object entry) { - if (!(entry instanceof Map.Entry)) - return false; - Entry e = (Entry) entry; - return containsKey(e.getKey()); - } - - @Override - public Iterator> iterator() { - return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new EntryIterator() - : ((EntryTreeHashMap) entries).new EntryIterator(); - } - - @Override - public boolean remove(Object entry) { - if (entry instanceof Map.Entry) { - Entry e = (Entry) entry; - return ExpiringMap.this.remove(e.getKey()) != null; - } - return false; - } - - @Override - public int size() { - return ExpiringMap.this.size(); - } - }; - } - - @Override - public boolean equals(Object obj) { - readLock.lock(); - try { - return entries.equals(obj); - } finally { - readLock.unlock(); - } - } - - @Override - @SuppressWarnings("unchecked") - public V get(Object key) { - ExpiringEntry entry = getEntry(key); - - if (entry == null) { - return load((K) key); - } else if (ExpirationPolicy.ACCESSED.equals(entry.expirationPolicy.get())) - resetEntry(entry, false); - - return entry.getValue(); - } - - private V load(K key) { - if (entryLoader == null && expiringEntryLoader == null) - return null; - - writeLock.lock(); - try { - // Double check for entry - ExpiringEntry entry = getEntry(key); - if (entry != null) - return entry.getValue(); - - if (entryLoader != null) { - V value = entryLoader.load(key); - put(key, value); - return value; - } else { - ExpiringValue expiringValue = expiringEntryLoader.load(key); - if (expiringValue == null) { - put(key, null); - return null; - } else { - long duration = expiringValue.getTimeUnit() == null ? expirationNanos.get() : expiringValue.getDuration(); - TimeUnit timeUnit = expiringValue.getTimeUnit() == null ? TimeUnit.NANOSECONDS : expiringValue.getTimeUnit(); - put(key, expiringValue.getValue(), expiringValue.getExpirationPolicy() == null ? expirationPolicy.get() - : expiringValue.getExpirationPolicy(), duration, timeUnit); - return expiringValue.getValue(); - } - } - } finally { - writeLock.unlock(); - } - } - - /** - * Returns the map's default expiration duration in milliseconds. - * - * @return The expiration duration (milliseconds) - */ - public long getExpiration() { - return TimeUnit.NANOSECONDS.toMillis(expirationNanos.get()); - } - - /** - * Gets the expiration duration in milliseconds for the entry corresponding to the given key. - * - * @param key - * @return The expiration duration in milliseconds - * @throws NullPointerException if {@code key} is null - * @throws NoSuchElementException If no entry exists for the given key - */ - public long getExpiration(K key) { - Assert.notNull(key, "key"); - ExpiringEntry entry = getEntry(key); - Assert.element(entry, key); - return TimeUnit.NANOSECONDS.toMillis(entry.expirationNanos.get()); - } - - /** - * Gets the ExpirationPolicy for the entry corresponding to the given {@code key}. - * - * @param key - * @return The ExpirationPolicy for the {@code key} - * @throws NullPointerException if {@code key} is null - * @throws NoSuchElementException If no entry exists for the given key - */ - public ExpirationPolicy getExpirationPolicy(K key) { - Assert.notNull(key, "key"); - ExpiringEntry entry = getEntry(key); - Assert.element(entry, key); - return entry.expirationPolicy.get(); - } - - /** - * Gets the expected expiration, in milliseconds from the current time, for the entry corresponding to the given - * {@code key}. - * - * @param key - * @return The expiration duration in milliseconds - * @throws NullPointerException if {@code key} is null - * @throws NoSuchElementException If no entry exists for the given key - */ - public long getExpectedExpiration(K key) { - Assert.notNull(key, "key"); - ExpiringEntry entry = getEntry(key); - Assert.element(entry, key); - return TimeUnit.NANOSECONDS.toMillis(entry.expectedExpiration.get() - System.nanoTime()); - } - - /** - * Gets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the - * first entry in line for expiration based on the expiration policy. - * - * @return The maximum size of the map. - */ - public int getMaxSize() { - return maxSize; - } - - @Override - public int hashCode() { - readLock.lock(); - try { - return entries.hashCode(); - } finally { - readLock.unlock(); - } - } - - @Override - public boolean isEmpty() { - readLock.lock(); - try { - return entries.isEmpty(); - } finally { - readLock.unlock(); - } - } - - /** - * Returns a copy of the map's keys, which can be iterated over safely by multiple threads. - * - * @return Copied set of map keys. - */ - @Override - public Set keySet() { - return new AbstractSet() { - @Override - public void clear() { - ExpiringMap.this.clear(); - } - - @Override - public boolean contains(Object key) { - return containsKey(key); - } - - @Override - public Iterator iterator() { - readLock.lock(); - try { - return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new KeyIterator() - : ((EntryTreeHashMap) entries).new KeyIterator(); - } finally { - readLock.unlock(); - } - } - - @Override - public boolean remove(Object value) { - return ExpiringMap.this.remove(value) != null; - } - - @Override - public int size() { - return ExpiringMap.this.size(); - } - }; - } - - /** - * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the - * same {@code key} and {@code value}. - * - * @param key to put value for - * @param value to put for key - * @return the old value - * @throws NullPointerException if {@code key} is null - */ - @Override - public V put(K key, V value) { - Assert.notNull(key, "key"); - return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); - } - - /** - * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) - */ - public V put(K key, V value, ExpirationPolicy expirationPolicy) { - return put(key, value, expirationPolicy, expirationNanos.get(), TimeUnit.NANOSECONDS); - } - - /** - * @see #put(Object, Object, ExpirationPolicy, long, TimeUnit) - */ - public V put(K key, V value, long duration, TimeUnit timeUnit) { - return put(key, value, expirationPolicy.get(), duration, timeUnit); - } - - /** - * Puts {@code value} in the map for {@code key}. Resets the entry's expiration unless an entry already exists for the - * same {@code key} and {@code value}. Requires that variable expiration be enabled. - * - * @param key Key to put value for - * @param value Value to put for key - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @return the old value - * @throws UnsupportedOperationException If variable expiration is not enabled - * @throws NullPointerException if {@code key}, {@code expirationPolicy} or {@code timeUnit} are null - */ - public V put(K key, V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { - Assert.notNull(key, "key"); - Assert.notNull(expirationPolicy, "expirationPolicy"); - Assert.notNull(timeUnit, "timeUnit"); - Assert.operation(variableExpiration, "Variable expiration is not enabled"); - return putInternal(key, value, expirationPolicy, TimeUnit.NANOSECONDS.convert(duration, timeUnit)); - } - - @Override - public void putAll(Map map) { - Assert.notNull(map, "map"); - long expiration = expirationNanos.get(); - ExpirationPolicy expirationPolicy = this.expirationPolicy.get(); - writeLock.lock(); - try { - for (Entry entry : map.entrySet()) - putInternal(entry.getKey(), entry.getValue(), expirationPolicy, expiration); - } finally { - writeLock.unlock(); - } - } - - @Override - public V putIfAbsent(K key, V value) { - Assert.notNull(key, "key"); - writeLock.lock(); - try { - if (!entries.containsKey(key)) - return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); - else - return entries.get(key).getValue(); - } finally { - writeLock.unlock(); - } - } - - @Override - public V remove(Object key) { - Assert.notNull(key, "key"); - writeLock.lock(); - try { - ExpiringEntry entry = entries.remove(key); - if (entry == null) - return null; - if (entry.cancel()) - scheduleEntry(entries.first()); - return entry.getValue(); - } finally { - writeLock.unlock(); - } - } - - @Override - public boolean remove(Object key, Object value) { - Assert.notNull(key, "key"); - writeLock.lock(); - try { - ExpiringEntry entry = entries.get(key); - if (entry != null && entry.getValue().equals(value)) { - entries.remove(key); - if (entry.cancel()) - scheduleEntry(entries.first()); - return true; - } else - return false; - } finally { - writeLock.unlock(); - } - } - - @Override - public V replace(K key, V value) { - Assert.notNull(key, "key"); - writeLock.lock(); - try { - if (entries.containsKey(key)) { - return putInternal(key, value, expirationPolicy.get(), expirationNanos.get()); - } else - return null; - } finally { - writeLock.unlock(); - } - } - - @Override - public boolean replace(K key, V oldValue, V newValue) { - Assert.notNull(key, "key"); - writeLock.lock(); - try { - ExpiringEntry entry = entries.get(key); - if (entry != null && entry.getValue().equals(oldValue)) { - putInternal(key, newValue, expirationPolicy.get(), expirationNanos.get()); - return true; - } else - return false; - } finally { - writeLock.unlock(); - } - } - - /** - * Removes an expiration listener. - * - * @param listener - * @throws NullPointerException if {@code listener} is null - */ - public void removeExpirationListener(ExpirationListener listener) { - Assert.notNull(listener, "listener"); - for (int i = 0; i < expirationListeners.size(); i++) { - if (expirationListeners.get(i).equals(listener)) { - expirationListeners.remove(i); - return; - } - } - } - - /** - * Removes an asynchronous expiration listener. - * - * @param listener - * @throws NullPointerException if {@code listener} is null - */ - public void removeAsyncExpirationListener(ExpirationListener listener) { - Assert.notNull(listener, "listener"); - for (int i = 0; i < asyncExpirationListeners.size(); i++) { - if (asyncExpirationListeners.get(i).equals(listener)) { - asyncExpirationListeners.remove(i); - return; - } - } - } - - /** - * Resets expiration for the entry corresponding to {@code key}. - * - * @param key to reset expiration for - * @throws NullPointerException if {@code key} is null - */ - public void resetExpiration(K key) { - Assert.notNull(key, "key"); - ExpiringEntry entry = getEntry(key); - if (entry != null) - resetEntry(entry, false); - } - - /** - * Sets the expiration duration for the entry corresponding to the given key. Supported only if variable expiration is - * enabled. - * - * @param key Key to set expiration for - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @throws NullPointerException if {@code key} or {@code timeUnit} are null - * @throws UnsupportedOperationException If variable expiration is not enabled - */ - public void setExpiration(K key, long duration, TimeUnit timeUnit) { - Assert.notNull(key, "key"); - Assert.notNull(timeUnit, "timeUnit"); - Assert.operation(variableExpiration, "Variable expiration is not enabled"); - writeLock.lock(); - try { - ExpiringEntry entry = entries.get(key); - if (entry != null) { - entry.expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); - resetEntry(entry, true); - } - } finally { - writeLock.unlock(); - } - } - - /** - * Updates the default map entry expiration. Supported only if variable expiration is enabled. - * - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @throws NullPointerException {@code timeUnit} is null - * @throws UnsupportedOperationException If variable expiration is not enabled - */ - public void setExpiration(long duration, TimeUnit timeUnit) { - Assert.notNull(timeUnit, "timeUnit"); - Assert.operation(variableExpiration, "Variable expiration is not enabled"); - expirationNanos.set(TimeUnit.NANOSECONDS.convert(duration, timeUnit)); - } - - /** - * Sets the global expiration policy for the map. Individual expiration policies may override the global policy. - * - * @param expirationPolicy - * @throws NullPointerException {@code expirationPolicy} is null - */ - public void setExpirationPolicy(ExpirationPolicy expirationPolicy) { - Assert.notNull(expirationPolicy, "expirationPolicy"); - this.expirationPolicy.set(expirationPolicy); - } - - /** - * Sets the expiration policy for the entry corresponding to the given key. - * - * @param key to set policy for - * @param expirationPolicy to set - * @throws NullPointerException if {@code key} or {@code expirationPolicy} are null - * @throws UnsupportedOperationException If variable expiration is not enabled - */ - public void setExpirationPolicy(K key, ExpirationPolicy expirationPolicy) { - Assert.notNull(key, "key"); - Assert.notNull(expirationPolicy, "expirationPolicy"); - Assert.operation(variableExpiration, "Variable expiration is not enabled"); - ExpiringEntry entry = getEntry(key); - if (entry != null) - entry.expirationPolicy.set(expirationPolicy); - } - - /** - * Sets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the - * first entry in line for expiration based on the expiration policy. - * - * @param maxSize The maximum size of the map. - */ - public void setMaxSize(int maxSize) { - Assert.operation(maxSize > 0, "maxSize"); - this.maxSize = maxSize; - } - - @Override - public int size() { - readLock.lock(); - try { - return entries.size(); - } finally { - readLock.unlock(); - } - } - - @Override - public String toString() { - readLock.lock(); - try { - return entries.toString(); - } finally { - readLock.unlock(); - } - } - - /** - * Returns a copy of the map's values, which can be iterated over safely by multiple threads. - * - * @return Copied set of map values. - */ - @Override - public Collection values() { - return new AbstractCollection() { - @Override - public void clear() { - ExpiringMap.this.clear(); - } - - @Override - public boolean contains(Object value) { - return containsValue(value); - } - - @Override - public Iterator iterator() { - readLock.lock(); - try { - return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new ValueIterator() - : ((EntryTreeHashMap) entries).new ValueIterator(); - } finally { - readLock.unlock(); - } - } - - @Override - public int size() { - return ExpiringMap.this.size(); - } - }; - } - - /** - * Notifies expiration listeners that the given entry expired. Must not be called from within a locked context. - * - * @param entry Entry to expire - */ - void notifyListeners(final ExpiringEntry entry) { - if (asyncExpirationListeners != null) - for (final ExpirationListener listener : asyncExpirationListeners) { - LISTENER_SERVICE.execute(new Runnable() { - public void run() { - try { - listener.expired(entry.key, entry.getValue()); - } catch (Exception ignoreUserExceptions) { - } - } - }); - } - - if (expirationListeners != null) - for (final ExpirationListener listener : expirationListeners) { - try { - listener.expired(entry.key, entry.getValue()); - } catch (Exception ignoreUserExceptions) { - } - } - } - - /** - * Returns the internal ExpiringEntry for the {@code key}, obtaining a read lock. - */ - ExpiringEntry getEntry(Object key) { - readLock.lock(); - try { - return entries.get(key); - } finally { - readLock.unlock(); - } - } - - /** - * Puts the given key/value in storage, scheduling the new entry for expiration if needed. If a previous value existed - * for the given key, it is first cancelled and the entries reordered to reflect the new expiration. - */ - V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) { - writeLock.lock(); - try { - ExpiringEntry entry = entries.get(key); - V oldValue = null; - - if (entry == null) { - entry = new ExpiringEntry(key, value, - variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy, - variableExpiration ? new AtomicLong(expirationNanos) : this.expirationNanos); - if (entries.size() >= maxSize) { - ExpiringEntry expiredEntry = entries.first(); - entries.remove(expiredEntry.key); - notifyListeners(expiredEntry); - } - entries.put(key, entry); - if (entries.size() == 1 || entries.first().equals(entry)) - scheduleEntry(entry); - } else { - oldValue = entry.getValue(); - if (!ExpirationPolicy.ACCESSED.equals(expirationPolicy) - && ((oldValue == null && value == null) || (oldValue != null && oldValue.equals(value)))) - return value; - - entry.setValue(value); - resetEntry(entry, false); - } - - return oldValue; - } finally { - writeLock.unlock(); - } - } - - /** - * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the - * internal map. Schedules the next entry in the map if the given {@code entry} was scheduled or if - * {@code scheduleNext} is true. - * - * @param entry to reset - * @param scheduleFirstEntry whether the first entry should be automatically scheduled - */ - void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) { - writeLock.lock(); - try { - boolean scheduled = entry.cancel(); - entries.reorder(entry); - - if (scheduled || scheduleFirstEntry) - scheduleEntry(entries.first()); - } finally { - writeLock.unlock(); - } - } - - /** - * Schedules an entry for expiration. Guards against concurrent schedule/schedule, cancel/schedule and schedule/cancel - * calls. - * - * @param entry Entry to schedule - */ - void scheduleEntry(ExpiringEntry entry) { - if (entry == null || entry.scheduled) - return; - - Runnable runnable = null; - synchronized (entry) { - if (entry.scheduled) - return; - - final WeakReference> entryReference = new WeakReference>(entry); - runnable = new Runnable() { - @Override - public void run() { - ExpiringEntry entry = entryReference.get(); - - writeLock.lock(); - try { - if (entry != null && entry.scheduled) { - entries.remove(entry.key); - notifyListeners(entry); + @Override + public Collection values() { + return new AbstractCollection() { + @Override + public void clear() { + ExpiringMap.this.clear(); } - try { - // Expires entries and schedules the next entry - Iterator> iterator = entries.valuesIterator(); - boolean schedulePending = true; + @Override + public boolean contains(Object value) { + return containsValue(value); + } - while (iterator.hasNext() && schedulePending) { - ExpiringEntry nextEntry = iterator.next(); - if (nextEntry.expectedExpiration.get() <= System.nanoTime()) { - iterator.remove(); - notifyListeners(nextEntry); - } else { - scheduleEntry(nextEntry); - schedulePending = false; + @Override + public Iterator iterator() { + readLock.lock(); + try { + return (entries instanceof EntryLinkedHashMap) ? ((EntryLinkedHashMap) entries).new ValueIterator() + : ((EntryTreeHashMap) entries).new ValueIterator(); + } finally { + readLock.unlock(); } - } - } catch (NoSuchElementException ignored) { } - } finally { - writeLock.unlock(); - } + + @Override + public int size() { + return ExpiringMap.this.size(); + } + }; + } + + /** + * Notifies expiration listeners that the given entry expired. Must not be called from within a locked context. + * + * @param entry Entry to expire + */ + void notifyListeners(final ExpiringEntry entry) { + if (asyncExpirationListeners != null) + for (final ExpirationListener listener : asyncExpirationListeners) { + LISTENER_SERVICE.execute(new Runnable() { + public void run() { + try { + listener.expired(entry.key, entry.getValue()); + } catch (Exception ignoreUserExceptions) { + } + } + }); + } + + if (expirationListeners != null) + for (final ExpirationListener listener : expirationListeners) { + try { + listener.expired(entry.key, entry.getValue()); + } catch (Exception ignoreUserExceptions) { + } + } + } + + /** + * Returns the internal ExpiringEntry for the {@code key}, obtaining a read lock. + */ + ExpiringEntry getEntry(Object key) { + readLock.lock(); + try { + return entries.get(key); + } finally { + readLock.unlock(); } - }; - - Future entryFuture = EXPIRER.schedule(runnable, entry.expectedExpiration.get() - System.nanoTime(), - TimeUnit.NANOSECONDS); - entry.schedule(entryFuture); } - } - private static Entry mapEntryFor(final ExpiringEntry entry) { - return new Entry() { - @Override - public K getKey() { - return entry.key; - } + /** + * Puts the given key/value in storage, scheduling the new entry for expiration if needed. If a previous value existed + * for the given key, it is first cancelled and the entries reordered to reflect the new expiration. + */ + V putInternal(K key, V value, ExpirationPolicy expirationPolicy, long expirationNanos) { + writeLock.lock(); + try { + ExpiringEntry entry = entries.get(key); + V oldValue = null; - @Override - public V getValue() { - return entry.value; - } + if (entry == null) { + entry = new ExpiringEntry(key, value, + variableExpiration ? new AtomicReference(expirationPolicy) : this.expirationPolicy, + variableExpiration ? new AtomicLong(expirationNanos) : this.expirationNanos); + if (entries.size() >= maxSize) { + ExpiringEntry expiredEntry = entries.first(); + entries.remove(expiredEntry.key); + notifyListeners(expiredEntry); + } + entries.put(key, entry); + if (entries.size() == 1 || entries.first().equals(entry)) + scheduleEntry(entry); + } else { + oldValue = entry.getValue(); + if (!ExpirationPolicy.ACCESSED.equals(expirationPolicy) + && ((oldValue == null && value == null) || (oldValue != null && oldValue.equals(value)))) + return value; - @Override - public V setValue(V value) { - throw new UnsupportedOperationException(); - } - }; - } + entry.setValue(value); + resetEntry(entry, false); + } - private void initListenerService() { - synchronized (ExpiringMap.class) { - if (LISTENER_SERVICE == null) { - LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool( - THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY); - } + return oldValue; + } finally { + writeLock.unlock(); + } + } + + /** + * Resets the given entry's schedule canceling any existing scheduled expiration and reordering the entry in the + * internal map. Schedules the next entry in the map if the given {@code entry} was scheduled or if + * {@code scheduleNext} is true. + * + * @param entry to reset + * @param scheduleFirstEntry whether the first entry should be automatically scheduled + */ + void resetEntry(ExpiringEntry entry, boolean scheduleFirstEntry) { + writeLock.lock(); + try { + boolean scheduled = entry.cancel(); + entries.reorder(entry); + + if (scheduled || scheduleFirstEntry) + scheduleEntry(entries.first()); + } finally { + writeLock.unlock(); + } + } + + /** + * Schedules an entry for expiration. Guards against concurrent schedule/schedule, cancel/schedule and schedule/cancel + * calls. + * + * @param entry Entry to schedule + */ + void scheduleEntry(ExpiringEntry entry) { + if (entry == null || entry.scheduled) + return; + + Runnable runnable = null; + synchronized (entry) { + if (entry.scheduled) + return; + + final WeakReference> entryReference = new WeakReference>(entry); + runnable = new Runnable() { + @Override + public void run() { + ExpiringEntry entry = entryReference.get(); + + writeLock.lock(); + try { + if (entry != null && entry.scheduled) { + entries.remove(entry.key); + notifyListeners(entry); + } + + try { + // Expires entries and schedules the next entry + Iterator> iterator = entries.valuesIterator(); + boolean schedulePending = true; + + while (iterator.hasNext() && schedulePending) { + ExpiringEntry nextEntry = iterator.next(); + if (nextEntry.expectedExpiration.get() <= System.nanoTime()) { + iterator.remove(); + notifyListeners(nextEntry); + } else { + scheduleEntry(nextEntry); + schedulePending = false; + } + } + } catch (NoSuchElementException ignored) { + } + } finally { + writeLock.unlock(); + } + } + }; + + Future entryFuture = EXPIRER.schedule(runnable, entry.expectedExpiration.get() - System.nanoTime(), + TimeUnit.NANOSECONDS); + entry.schedule(entryFuture); + } + } + + private void initListenerService() { + synchronized (ExpiringMap.class) { + if (LISTENER_SERVICE == null) { + LISTENER_SERVICE = (ThreadPoolExecutor) Executors.newCachedThreadPool( + THREAD_FACTORY == null ? new NamedThreadFactory("ExpiringMap-Listener-%s") : THREAD_FACTORY); + } + } + } + + /** + * Entry map definition. + */ + private interface EntryMap extends Map> { + /** + * Returns the first entry in the map or null if the map is empty. + */ + ExpiringEntry first(); + + /** + * Reorders the given entry in the map. + * + * @param entry to reorder + */ + void reorder(ExpiringEntry entry); + + /** + * Returns a values iterator. + */ + Iterator> valuesIterator(); + } + + /** + * Builds ExpiringMap instances. Defaults to ExpirationPolicy.CREATED, expiration of 60 TimeUnit.SECONDS and + * a maxSize of Integer.MAX_VALUE. + */ + public static final class Builder { + private ExpirationPolicy expirationPolicy = ExpirationPolicy.CREATED; + private List> expirationListeners; + private List> asyncExpirationListeners; + private TimeUnit timeUnit = TimeUnit.SECONDS; + private boolean variableExpiration; + private long duration = 60; + private int maxSize = Integer.MAX_VALUE; + private EntryLoader entryLoader; + private ExpiringEntryLoader expiringEntryLoader; + + /** + * Creates a new Builder object. + */ + private Builder() { + } + + /** + * Builds and returns an expiring map. + * + * @param Key type + * @param Value type + */ + @SuppressWarnings("unchecked") + public ExpiringMap build() { + return new ExpiringMap((Builder) this); + } + + /** + * Sets the default map entry expiration. + * + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException if {@code timeUnit} is null + */ + public Builder expiration(long duration, TimeUnit timeUnit) { + this.duration = duration; + this.timeUnit = Assert.notNull(timeUnit, "timeUnit"); + return this; + } + + /** + * Sets the maximum size of the map. Once this size has been reached, adding an additional entry will expire the + * first entry in line for expiration based on the expiration policy. + * + * @param maxSize The maximum size of the map. + */ + public Builder maxSize(int maxSize) { + Assert.operation(maxSize > 0, "maxSize"); + this.maxSize = maxSize; + return this; + } + + /** + * Sets the EntryLoader to use when loading entries. Either an EntryLoader or ExpiringEntryLoader may be set, not + * both. + * + * @param loader to set + * @throws NullPointerException if {@code loader} is null + * @throws IllegalStateException if an {@link #expiringEntryLoader(ExpiringEntryLoader) ExpiringEntryLoader} is set + */ + @SuppressWarnings("unchecked") + public Builder entryLoader(EntryLoader loader) { + assertNoLoaderSet(); + entryLoader = (EntryLoader) Assert.notNull(loader, "loader"); + return (Builder) this; + } + + /** + * Sets the ExpiringEntryLoader to use when loading entries and configures {@link #variableExpiration() variable + * expiration}. Either an EntryLoader or ExpiringEntryLoader may be set, not both. + * + * @param loader to set + * @throws NullPointerException if {@code loader} is null + * @throws IllegalStateException if an {@link #entryLoader(EntryLoader) EntryLoader} is set + */ + @SuppressWarnings("unchecked") + public Builder expiringEntryLoader( + ExpiringEntryLoader loader) { + assertNoLoaderSet(); + expiringEntryLoader = (ExpiringEntryLoader) Assert.notNull(loader, "loader"); + variableExpiration(); + return (Builder) this; + } + + /** + * Configures the expiration listener that will receive notifications upon each map entry's expiration. + * Notifications are delivered synchronously and block map write operations. + * + * @param listener to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder expirationListener( + ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (expirationListeners == null) + expirationListeners = new ArrayList>(); + expirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listeners which will receive notifications upon each map entry's expiration. + * Notifications are delivered synchronously and block map write operations. + * + * @param listeners to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder expirationListeners( + List> listeners) { + Assert.notNull(listeners, "listeners"); + if (expirationListeners == null) + expirationListeners = new ArrayList>(listeners.size()); + for (ExpirationListener listener : listeners) + expirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listener which will receive asynchronous notifications upon each map entry's + * expiration. + * + * @param listener to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder asyncExpirationListener( + ExpirationListener listener) { + Assert.notNull(listener, "listener"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new ArrayList>(); + asyncExpirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the expiration listeners which will receive asynchronous notifications upon each map entry's + * expiration. + * + * @param listeners to set + * @throws NullPointerException if {@code listener} is null + */ + @SuppressWarnings("unchecked") + public Builder asyncExpirationListeners( + List> listeners) { + Assert.notNull(listeners, "listeners"); + if (asyncExpirationListeners == null) + asyncExpirationListeners = new ArrayList>(listeners.size()); + for (ExpirationListener listener : listeners) + asyncExpirationListeners.add((ExpirationListener) listener); + return (Builder) this; + } + + /** + * Configures the map entry expiration policy. + * + * @param expirationPolicy + * @throws NullPointerException if {@code expirationPolicy} is null + */ + public Builder expirationPolicy(ExpirationPolicy expirationPolicy) { + this.expirationPolicy = Assert.notNull(expirationPolicy, "expirationPolicy"); + return this; + } + + /** + * Allows for map entries to have individual expirations and for expirations to be changed. + */ + public Builder variableExpiration() { + variableExpiration = true; + return this; + } + + private void assertNoLoaderSet() { + Assert.state(entryLoader == null && expiringEntryLoader == null, + "Either entryLoader or expiringEntryLoader may be set, not both"); + } + } + + /** + * Entry LinkedHashMap implementation. + */ + private static class EntryLinkedHashMap extends LinkedHashMap> + implements EntryMap { + private static final long serialVersionUID = 1L; + + @Override + public boolean containsValue(Object value) { + for (ExpiringEntry entry : values()) { + V v = entry.value; + if (v == value || (value != null && value.equals(v))) + return true; + } + return false; + } + + @Override + public ExpiringEntry first() { + return isEmpty() ? null : values().iterator().next(); + } + + @Override + public void reorder(ExpiringEntry value) { + remove(value.key); + value.resetExpiration(); + put(value.key, value); + } + + @Override + public Iterator> valuesIterator() { + return values().iterator(); + } + + abstract class AbstractHashIterator { + private final Iterator>> iterator; + private ExpiringEntry next; + + @SuppressWarnings({"unchecked", "rawtypes"}) + AbstractHashIterator() { + iterator = (Iterator) Arrays.asList(entrySet().toArray(new Map.Entry[0])).iterator(); + } + + public boolean hasNext() { + return iterator.hasNext(); + } + + public ExpiringEntry getNext() { + next = iterator.next().getValue(); + return next; + } + + public void remove() { + iterator.remove(); + } + } + + final class KeyIterator extends AbstractHashIterator implements Iterator { + public K next() { + return getNext().key; + } + } + + final class ValueIterator extends AbstractHashIterator implements Iterator { + public V next() { + return getNext().value; + } + } + + public final class EntryIterator extends AbstractHashIterator implements Iterator> { + public Map.Entry next() { + return mapEntryFor(getNext()); + } + } + } + + /** + * Entry TreeHashMap implementation for variable expiration ExpiringMap entries. + */ + private static class EntryTreeHashMap extends HashMap> implements EntryMap { + private static final long serialVersionUID = 1L; + SortedSet> sortedSet = new ConcurrentSkipListSet>(); + + @Override + public void clear() { + super.clear(); + sortedSet.clear(); + } + + @Override + public boolean containsValue(Object value) { + for (ExpiringEntry entry : values()) { + V v = entry.value; + if (v == value || (value != null && value.equals(v))) + return true; + } + return false; + } + + @Override + public ExpiringEntry first() { + return sortedSet.isEmpty() ? null : sortedSet.first(); + } + + @Override + public ExpiringEntry put(K key, ExpiringEntry value) { + sortedSet.add(value); + return super.put(key, value); + } + + @Override + public ExpiringEntry remove(Object key) { + ExpiringEntry entry = super.remove(key); + if (entry != null) + sortedSet.remove(entry); + return entry; + } + + @Override + public void reorder(ExpiringEntry value) { + sortedSet.remove(value); + value.resetExpiration(); + sortedSet.add(value); + } + + @Override + public Iterator> valuesIterator() { + return new ExpiringEntryIterator(); + } + + abstract class AbstractHashIterator { + private final Iterator> iterator = sortedSet.iterator(); + protected ExpiringEntry next; + + public boolean hasNext() { + return iterator.hasNext(); + } + + public ExpiringEntry getNext() { + next = iterator.next(); + return next; + } + + public void remove() { + EntryTreeHashMap.super.remove(next.key); + iterator.remove(); + } + } + + final class ExpiringEntryIterator extends AbstractHashIterator implements Iterator> { + public final ExpiringEntry next() { + return getNext(); + } + } + + final class KeyIterator extends AbstractHashIterator implements Iterator { + public final K next() { + return getNext().key; + } + } + + final class ValueIterator extends AbstractHashIterator implements Iterator { + public final V next() { + return getNext().value; + } + } + + final class EntryIterator extends AbstractHashIterator implements Iterator> { + public final Entry next() { + return mapEntryFor(getNext()); + } + } + } + + /** + * Expiring map entry implementation. + */ + static class ExpiringEntry implements Comparable> { + final AtomicLong expirationNanos; + /** + * Epoch time at which the entry is expected to expire + */ + final AtomicLong expectedExpiration; + final AtomicReference expirationPolicy; + final K key; + /** + * Guarded by "this" + */ + volatile Future entryFuture; + /** + * Guarded by "this" + */ + V value; + /** + * Guarded by "this" + */ + volatile boolean scheduled; + + /** + * Creates a new ExpiringEntry object. + * + * @param key for the entry + * @param value for the entry + * @param expirationPolicy for the entry + * @param expirationNanos for the entry + */ + ExpiringEntry(K key, V value, AtomicReference expirationPolicy, AtomicLong expirationNanos) { + this.key = key; + this.value = value; + this.expirationPolicy = expirationPolicy; + this.expirationNanos = expirationNanos; + this.expectedExpiration = new AtomicLong(); + resetExpiration(); + } + + @Override + public int compareTo(ExpiringEntry other) { + if (key.equals(other.key)) + return 0; + return expectedExpiration.get() < other.expectedExpiration.get() ? -1 : 1; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((key == null) ? 0 : key.hashCode()); + result = prime * result + ((value == null) ? 0 : value.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + ExpiringEntry other = (ExpiringEntry) obj; + if (!key.equals(other.key)) + return false; + if (value == null) { + if (other.value != null) + return false; + } else if (!value.equals(other.value)) + return false; + return true; + } + + @Override + public String toString() { + return value.toString(); + } + + /** + * Marks the entry as canceled. + * + * @return true if the entry was scheduled + */ + synchronized boolean cancel() { + boolean result = scheduled; + if (entryFuture != null) + entryFuture.cancel(false); + + entryFuture = null; + scheduled = false; + return result; + } + + /** + * Gets the entry value. + */ + synchronized V getValue() { + return value; + } + + /** + * Sets the entry value. + */ + synchronized void setValue(V value) { + this.value = value; + } + + /** + * Resets the entry's expected expiration. + */ + void resetExpiration() { + expectedExpiration.set(expirationNanos.get() + System.nanoTime()); + } + + /** + * Marks the entry as scheduled. + */ + synchronized void schedule(Future entryFuture) { + this.entryFuture = entryFuture; + scheduled = true; + } } - } } diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java index 7e97e0f..f19d8fb 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/ExpiringValue.java @@ -4,119 +4,119 @@ import java.util.concurrent.TimeUnit; /** * A value which should be stored in an {@link ExpiringMap} with optional control over its expiration. - * + * * @param the type of value being stored */ public final class ExpiringValue { - private static final long UNSET_DURATION = -1L; - private final V value; - private final ExpirationPolicy expirationPolicy; - private final long duration; - private final TimeUnit timeUnit; + private static final long UNSET_DURATION = -1L; + private final V value; + private final ExpirationPolicy expirationPolicy; + private final long duration; + private final TimeUnit timeUnit; - /** - * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default values for - * {@link ExpirationPolicy expiration policy} and {@link ExpiringMap#getExpiration()} expiration} will be used. - * - * @param value the value to store - * @see ExpiringMap#put(Object, Object) - */ - public ExpiringValue(V value) { - this(value, UNSET_DURATION, null, null); - } - - /** - * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default - * {@link ExpiringMap#getExpiration()} expiration} will be used. - * - * @param value the value to store - * @param expirationPolicy the expiration policy for the value - * @see ExpiringMap#put(Object, Object, ExpirationPolicy) - */ - public ExpiringValue(V value, ExpirationPolicy expirationPolicy) { - this(value, UNSET_DURATION, null, expirationPolicy); - } - - /** - * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default {@link ExpirationPolicy - * expiration policy} will be used. - * - * @param value the value to store - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @see ExpiringMap#put(Object, Object, long, TimeUnit) - * @throws NullPointerException on null timeUnit - */ - public ExpiringValue(V value, long duration, TimeUnit timeUnit) { - this(value, duration, timeUnit, null); - if (timeUnit == null) { - throw new NullPointerException(); - } - } - - /** - * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. - * - * @param value the value to store - * @param duration the length of time after an entry is created that it should be removed - * @param timeUnit the unit that {@code duration} is expressed in - * @param expirationPolicy the expiration policy for the value - * @see ExpiringMap#put(Object, Object, ExpirationPolicy, long, TimeUnit) - * @throws NullPointerException on null timeUnit - */ - public ExpiringValue(V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { - this(value, duration, timeUnit, expirationPolicy); - if (timeUnit == null) { - throw new NullPointerException(); - } - } - - private ExpiringValue(V value, long duration, TimeUnit timeUnit, ExpirationPolicy expirationPolicy) { - this.value = value; - this.expirationPolicy = expirationPolicy; - this.duration = duration; - this.timeUnit = timeUnit; - } - - public V getValue() { - return value; - } - - public ExpirationPolicy getExpirationPolicy() { - return expirationPolicy; - } - - public long getDuration() { - return duration; - } - - public TimeUnit getTimeUnit() { - return timeUnit; - } - - @Override - public int hashCode() { - return value != null ? value.hashCode() : 0; - } - - @Override - public boolean equals(Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default values for + * {@link ExpirationPolicy expiration policy} and {@link ExpiringMap#getExpiration()} expiration} will be used. + * + * @param value the value to store + * @see ExpiringMap#put(Object, Object) + */ + public ExpiringValue(V value) { + this(value, UNSET_DURATION, null, null); } - ExpiringValue that = (ExpiringValue) o; - return !(value != null ? !value.equals(that.value) : that.value != null) - && expirationPolicy == that.expirationPolicy && duration == that.duration && timeUnit == that.timeUnit; + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default + * {@link ExpiringMap#getExpiration()} expiration} will be used. + * + * @param value the value to store + * @param expirationPolicy the expiration policy for the value + * @see ExpiringMap#put(Object, Object, ExpirationPolicy) + */ + public ExpiringValue(V value, ExpirationPolicy expirationPolicy) { + this(value, UNSET_DURATION, null, expirationPolicy); + } - } + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. The map's default {@link ExpirationPolicy + * expiration policy} will be used. + * + * @param value the value to store + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @throws NullPointerException on null timeUnit + * @see ExpiringMap#put(Object, Object, long, TimeUnit) + */ + public ExpiringValue(V value, long duration, TimeUnit timeUnit) { + this(value, duration, timeUnit, null); + if (timeUnit == null) { + throw new NullPointerException(); + } + } - @Override - public String toString() { - return "ExpiringValue{" + "value=" + value + ", expirationPolicy=" + expirationPolicy + ", duration=" + duration - + ", timeUnit=" + timeUnit + '}'; - } + /** + * Creates an ExpiringValue to be stored in an {@link ExpiringMap}. + * + * @param value the value to store + * @param duration the length of time after an entry is created that it should be removed + * @param timeUnit the unit that {@code duration} is expressed in + * @param expirationPolicy the expiration policy for the value + * @throws NullPointerException on null timeUnit + * @see ExpiringMap#put(Object, Object, ExpirationPolicy, long, TimeUnit) + */ + public ExpiringValue(V value, ExpirationPolicy expirationPolicy, long duration, TimeUnit timeUnit) { + this(value, duration, timeUnit, expirationPolicy); + if (timeUnit == null) { + throw new NullPointerException(); + } + } + + private ExpiringValue(V value, long duration, TimeUnit timeUnit, ExpirationPolicy expirationPolicy) { + this.value = value; + this.expirationPolicy = expirationPolicy; + this.duration = duration; + this.timeUnit = timeUnit; + } + + public V getValue() { + return value; + } + + public ExpirationPolicy getExpirationPolicy() { + return expirationPolicy; + } + + public long getDuration() { + return duration; + } + + public TimeUnit getTimeUnit() { + return timeUnit; + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + ExpiringValue that = (ExpiringValue) o; + return !(value != null ? !value.equals(that.value) : that.value != null) + && expirationPolicy == that.expirationPolicy && duration == that.duration && timeUnit == that.timeUnit; + + } + + @Override + public String toString() { + return "ExpiringValue{" + "value=" + value + ", expirationPolicy=" + expirationPolicy + ", duration=" + duration + + ", timeUnit=" + timeUnit + '}'; + } } diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java index f30ee71..6125a3b 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/Assert.java @@ -6,27 +6,27 @@ import java.util.NoSuchElementException; * @author Jonathan Halterman */ public final class Assert { - private Assert() { - } + private Assert() { + } - public static T notNull(T reference, String parameterName) { - if (reference == null) - throw new NullPointerException(parameterName + " cannot be null"); - return reference; - } + public static T notNull(T reference, String parameterName) { + if (reference == null) + throw new NullPointerException(parameterName + " cannot be null"); + return reference; + } - public static void operation(boolean condition, String message) { - if (!condition) - throw new UnsupportedOperationException(message); - } + public static void operation(boolean condition, String message) { + if (!condition) + throw new UnsupportedOperationException(message); + } - public static void state(boolean expression, String errorMessageFormat, Object... args) { - if (!expression) - throw new IllegalStateException(String.format(errorMessageFormat, args)); - } + public static void state(boolean expression, String errorMessageFormat, Object... args) { + if (!expression) + throw new IllegalStateException(String.format(errorMessageFormat, args)); + } - public static void element(Object element, Object key) { - if (element == null) - throw new NoSuchElementException(key.toString()); - } + public static void element(Object element, Object key) { + if (element == null) + throw new NoSuchElementException(key.toString()); + } } \ No newline at end of file diff --git a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java index 52f0705..196b27b 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java +++ b/src/main/java/com/io/yutian/elementoriginlib/expiringmap/internal/NamedThreadFactory.java @@ -7,21 +7,21 @@ import java.util.concurrent.atomic.AtomicInteger; * Named thread factory. */ public class NamedThreadFactory implements ThreadFactory { - private final AtomicInteger threadNumber = new AtomicInteger(1); - private final String nameFormat; + private final AtomicInteger threadNumber = new AtomicInteger(1); + private final String nameFormat; - /** - * Creates a thread factory that names threads according to the {@code nameFormat} by supplying a - * single argument to the format representing the thread number. - */ - public NamedThreadFactory(String nameFormat) { - this.nameFormat = nameFormat; - } + /** + * Creates a thread factory that names threads according to the {@code nameFormat} by supplying a + * single argument to the format representing the thread number. + */ + public NamedThreadFactory(String nameFormat) { + this.nameFormat = nameFormat; + } - @Override - public Thread newThread(Runnable r) { - Thread thread = new Thread(r, String.format(nameFormat, threadNumber.getAndIncrement())); - thread.setDaemon(true); - return thread; - } + @Override + public Thread newThread(Runnable r) { + Thread thread = new Thread(r, String.format(nameFormat, threadNumber.getAndIncrement())); + thread.setDaemon(true); + return thread; + } } diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/ContainerGui.java b/src/main/java/com/io/yutian/elementoriginlib/gui/ContainerGui.java new file mode 100644 index 0000000..078721a --- /dev/null +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/ContainerGui.java @@ -0,0 +1,66 @@ +package com.io.yutian.elementoriginlib.gui; + +import com.io.yutian.elementoriginlib.gui.button.Button; +import com.io.yutian.elementoriginlib.gui.button.ClickType; +import net.kyori.adventure.text.Component; +import org.bukkit.Sound; +import org.bukkit.entity.Player; +import org.bukkit.event.inventory.InventoryAction; +import org.bukkit.event.inventory.InventoryClickEvent; + +import java.util.Set; + +public abstract class ContainerGui extends Gui { + + private Set slots; + + public ContainerGui(Player player, Component title, int size) { + super(player, title, size); + } + + public void addSlot(int slot) { + if (slot > inventory.getSize()) { + return; + } + slots.add(slot); + } + + @Override + public void handler(Player player, int slot, InventoryClickEvent event) { + if (buttons.containsKey(slot)) { + Button button = buttons.get(slot); + if (button == null) { + if (slot < inventory.getSize()) { + event.setCancelled(true); + } else { + if (event.getAction().equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + event.setCancelled(true); + } + } + return; + } + event.setCancelled(true); + clickButton(event, slot, button); + if (button.isPlaySound()) { + player.playSound(player.getLocation(), Sound.UI_BUTTON_CLICK, 1.0f, 1.0f); + } + InventoryAction action = event.getAction(); + ClickType clickType = ClickType.LEFT_CLICK; + if (action.equals(InventoryAction.PICKUP_ALL)) { + clickType = ClickType.LEFT_CLICK; + } else if (action.equals(InventoryAction.PICKUP_HALF)) { + clickType = ClickType.RIGHT_CLICK; + } else if (action.equals(InventoryAction.MOVE_TO_OTHER_INVENTORY)) { + clickType = ClickType.SHIFT_CLICK; + } + if (button.getClickConsumer() != null) { + button.getClickConsumer().accept(player, clickType); + } + } else { + if (slot < inventory.getSize()) { + event.setCancelled(true); + } + } + } + +} diff --git a/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java b/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java index 3190b95..326728e 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java +++ b/src/main/java/com/io/yutian/elementoriginlib/gui/Gui.java @@ -15,7 +15,7 @@ import java.util.Map; public class Gui extends IGui { - private Map buttons = new HashMap<>(); + public Map buttons = new HashMap<>(); public Gui(Player player, Component title, int size) { super(player, title, size); diff --git a/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java b/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java index 90421b5..0a59320 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java +++ b/src/main/java/com/io/yutian/elementoriginlib/list/PageList.java @@ -4,50 +4,42 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.IntStream; public class PageList { - - private List list; + private Map> map = new HashMap<>(); - private int amount; - + private final int totalPages; + public PageList(List list, int amount) { - this.list = list; - this.amount = amount; - if (list == null) { - return; - } - if (list.size() <= amount) { - List newList = new ArrayList(); - list.forEach(o-> newList.add(o)); - map.put(1, newList); + if (list == null || list.isEmpty() || amount <= 0) { + totalPages = 0; } else { - int x = 0; - int c = list.size() / amount; - for (int j = 0; j <= c;j++) { - int min = j * amount; - int max = (j+1) * amount; - List newList = new ArrayList(); - for (int k = min; k < max; k++) { - if (k >= list.size()) { - break; - } - newList.add(list.get(k)); - } - map.put(j+1, newList); - } + totalPages = (list.size() - 1) / amount + 1; + IntStream.rangeClosed(1, totalPages).forEach(page -> { + int start = (page - 1) * amount; + int end = Math.min(page * amount, list.size()); + map.put(page, new ArrayList<>(list.subList(start, end))); + }); } } - + public int size() { - return map.size(); + return totalPages; } - + public List getList(int page) { - if (page <= 0) { - page = 1; + if (page < 1 || page > totalPages) { + throw new IllegalArgumentException("Invalid page number: " + page); } return map.get(page); } - + + public boolean hasNextPage(int page) { + return page < totalPages; + } + + public int getTotalPages() { + return totalPages; + } } diff --git a/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java index fe5120c..57d9a28 100644 --- a/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java +++ b/src/main/java/com/io/yutian/elementoriginlib/serialize/SerializeHelper.java @@ -2,24 +2,27 @@ package com.io.yutian.elementoriginlib.serialize; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.introspect.VisibilityChecker; import com.fasterxml.jackson.databind.module.SimpleModule; -import com.io.yutian.elementoriginlib.serialize.serializers.ItemStackDeserializer; -import com.io.yutian.elementoriginlib.serialize.serializers.ItemStackSerializer; -import com.io.yutian.elementoriginlib.serialize.serializers.UUIDDeserializer; -import com.io.yutian.elementoriginlib.serialize.serializers.UUIDSerializer; +import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +import com.io.yutian.elementoriginlib.logger.Logger; +import com.io.yutian.elementoriginlib.serialize.serializers.*; import org.bukkit.inventory.ItemStack; import java.util.UUID; public class SerializeHelper { + private static final Logger LOGGER = Logger.getLogger(SerializeHelper.class); + private static final ObjectMapper objectMapper; + private static ObjectWriter objectWriter; + private static ObjectReader objectReader; static { objectMapper = new ObjectMapper(); - objectMapper.setVisibility( VisibilityChecker.Std.defaultInstance() .withFieldVisibility(JsonAutoDetect.Visibility.ANY) @@ -29,31 +32,61 @@ public class SerializeHelper { .withCreatorVisibility(JsonAutoDetect.Visibility.NONE) ); + objectMapper.registerModule(new AfterburnerModule()); + + objectMapper.disable(MapperFeature.AUTO_DETECT_GETTERS); + objectMapper.disable(MapperFeature.AUTO_DETECT_SETTERS); + objectMapper.disable(MapperFeature.AUTO_DETECT_IS_GETTERS); + objectMapper.disable(MapperFeature.USE_GETTERS_AS_SETTERS); + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + objectMapper.disable(SerializationFeature.INDENT_OUTPUT); + SimpleModule module = new SimpleModule(); module.addSerializer(UUID.class, new UUIDSerializer()); module.addDeserializer(UUID.class, new UUIDDeserializer()); - module.addSerializer(ItemStack.class, new ItemStackSerializer()); module.addDeserializer(ItemStack.class, new ItemStackDeserializer()); objectMapper.registerModule(module); + objectWriter = objectMapper.writer(); + objectReader = objectMapper.reader(); + } + + public static void registerModule(Module module) { + objectMapper.registerModule(module); + + objectWriter = objectMapper.writer(); + objectReader = objectMapper.reader(); + } + + public static void registerSerializer(Class clazz, JsonSerializer serializer) { + SimpleModule module = new SimpleModule(); + module.addSerializer(clazz, serializer); + registerModule(module); + } + + public static void registerDeserializer(Class clazz, JsonDeserializer deserializer) { + SimpleModule module = new SimpleModule(); + module.addDeserializer(clazz, deserializer); + registerModule(module); } public static String serialize(Object obj) { try { - return objectMapper.writeValueAsString(obj); + return objectWriter.writeValueAsString(obj); } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to serialize object: " + obj, e); + LOGGER.error("Failed to serialize object: " + obj, e); + return null; } } public static T deserialize(String json, Class clazz) { try { - return objectMapper.readValue(json, clazz); + return objectReader.forType(clazz).readValue(json); } catch (JsonProcessingException e) { - throw new RuntimeException("Failed to deserialize JSON: " + json, e); + LOGGER.error("Failed to deserialize object: " + json, e); + return null; } } - -} - +} \ No newline at end of file