diff --git a/src/main/java/conflux/Ctx.java b/src/main/java/conflux/Ctx.java index cbbb441..eba3769 100644 --- a/src/main/java/conflux/Ctx.java +++ b/src/main/java/conflux/Ctx.java @@ -34,169 +34,133 @@ */ package conflux; -import java.lang.ref.WeakReference; -import java.util.List; import java.util.Map; -import java.util.Objects; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.CopyOnWriteArrayList; /** - * A globally accessible, type-safe context for sharing data between independent - * parts of an application. Supports listeners that are notified whenever a - * registered key's value changes. - * - * This implementation guarantees: - * - * - * Usage: + * A central context manager for storing and retrieving key-value pairs in a + * thread-safe, type-safe manner. * - *
- * Key<Integer> COUNT = Key.of("count", Integer.class);
- * Ctx.INSTANCE.put(COUNT, 42);
- * int v = Ctx.INSTANCE.get(COUNT);
- * 
+ * This enum serves as both: + * + * + * Each context provides listener support for reactive value changes. * + * @see Key + * @see Listener + * * @author Leo Galambos */ -public enum Ctx { +public enum Ctx implements CtxInterface { /** - * Singleton instance of the context. + * The singleton instance representing the default/global context. */ INSTANCE; - private final Map, Object> values = new ConcurrentHashMap<>(); - private final Map> keyTypes = new ConcurrentHashMap<>(); - private final Map, List>>> listeners = new ConcurrentHashMap<>(); + /** + * A registry of named contexts, allowing for multiple isolated logical + * contexts. + */ + private final Map contexts = new ConcurrentHashMap<>(); + /** + * The default context instance used by this enum singleton. All interface + * method calls delegate to this context. + */ + private final CtxInterface defaultCtx = new CtxInstance(); /** - * Stores or updates the value for a given key. If the key is used for the first - * time, its type is recorded. If the key has been used before with a different - * type, an exception will be thrown. + * Returns a named context. If the context does not exist, it is lazily created. + * + * This method allows isolation between different logical scopes (e.g., + * sessions, users). * - * After setting the value, any registered listeners for that key will be - * notified (if the value was modified). - * - * @param key the key - * @param value the value to store - * @param the type of the value - * @throws IllegalStateException if the key is reused with a different type + * @param name the name of the context + * @return the associated context instance */ + public CtxInterface getContext(String name) { + return contexts.computeIfAbsent(name, k -> new CtxInstance()); + } + + /** + * Returns the set of names for all currently registered contexts. + * + * @return an immutable set of context names + */ + public Set contextNames() { + return Set.copyOf(contexts.keySet()); + } + + /** + * {@inheritDoc} + * + * Delegates to the default context instance. + */ + @Override public void put(Key key, T value) { - keyTypes.compute(key.name(), (k, existingType) -> { - if (existingType == null) { - return key.type(); - } else { - if (!existingType.equals(key.type())) { - throw new IllegalStateException( - "Key '" + key.name() + "' already associated with type " + existingType.getName()); - } - return existingType; - } - }); - - @SuppressWarnings("unchecked") - T previous = (T) values.put(key, value); - - if (Objects.deepEquals(value, previous)) { - // no change - return; - } - - // notify listeners - var list = listeners.get(key); - if (list != null) { - for (var ref : list) { - var listener = ref.get(); - if (listener != null) { - @SuppressWarnings("unchecked") - Listener typedListener = (Listener) listener; - typedListener.valueChanged(value); - } - } - // clean up dead weak references - list.removeIf(ref -> ref.get() == null); - } + defaultCtx.put(key, value); } /** - * Retrieves the value for the given key, or {@code null} if not set. - * - * @param key the key - * @param the type - * @return the stored value or null + * {@inheritDoc} + * + * Delegates to the default context instance. */ - @SuppressWarnings("unchecked") + @Override public T get(Key key) { - return (T) values.get(key); + return defaultCtx.get(key); } /** - * Checks if a value exists for the given key. - * - * @param key the key - * @return true if a value is present + * {@inheritDoc} + * + * Delegates to the default context instance. */ + @Override public boolean contains(Key key) { - return values.containsKey(key); + return defaultCtx.contains(key); } /** - * Removes the value and any listeners for the given key. - * - * @param key the key - * @return the removed value, or null if absent + * {@inheritDoc} + * + * Delegates to the default context instance. */ + @Override public Object remove(Key key) { - keyTypes.remove(key.name()); - var removed = values.remove(key); - listeners.remove(key); - return removed; + return defaultCtx.remove(key); } /** - * Clears all keys and listeners from the context. + * {@inheritDoc} + * + * Delegates to the default context instance. */ + @Override public void clear() { - values.clear(); - keyTypes.clear(); - listeners.clear(); + defaultCtx.clear(); } /** - * Registers a listener to be notified whenever the value for the key changes. - * The listener is weakly referenced to avoid preventing its garbage collection. - * - * @param key the key - * @param listener the listener - * @param the type + * {@inheritDoc} + * + * Delegates to the default context instance. */ + @Override public void addListener(Key key, Listener listener) { - listeners.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(new WeakReference<>(listener)); + defaultCtx.addListener(key, listener); } /** - * Unregisters a previously registered listener for the key. - * - * @param key the key - * @param listener the listener to remove - * @param the type + * {@inheritDoc} + * + * Delegates to the default context instance. */ + @Override public void removeListener(Key key, Listener listener) { - var list = listeners.get(key); - if (list != null) { - list.removeIf(ref -> { - var l = ref.get(); - return l == null || l.equals(listener); - }); - if (list.isEmpty()) { - listeners.remove(key); - } - } + defaultCtx.removeListener(key, listener); } -} \ No newline at end of file +} diff --git a/src/main/java/conflux/CtxInstance.java b/src/main/java/conflux/CtxInstance.java new file mode 100644 index 0000000..8791ae3 --- /dev/null +++ b/src/main/java/conflux/CtxInstance.java @@ -0,0 +1,197 @@ +/** + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package conflux; + +import java.lang.ref.WeakReference; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A globally accessible, type-safe context for sharing data between independent + * parts of an application. Supports listeners that are notified whenever a + * registered key's value changes. + * + * This implementation guarantees: + *
    + *
  • Strongly typed keys using {@link Key}
  • + *
  • One consistent type per key name
  • + *
  • Support for weak-referenced listeners to avoid memory leaks
  • + *
  • Thread-safe operations
  • + *
+ * + * @author Leo Galambos + */ +public final class CtxInstance implements CtxInterface { + + private final Map, Object> values = new ConcurrentHashMap<>(); + private final Map> keyTypes = new ConcurrentHashMap<>(); + private final Map, List>>> listeners = new ConcurrentHashMap<>(); + + /** + * Stores or updates the value for a given key. If the key is used for the first + * time, its type is recorded. If the key has been used before with a different + * type, an exception will be thrown. + * + * After setting the value, any registered listeners for that key will be + * notified (if the value was modified). + * + * @param key the key + * @param value the value to store + * @param the type of the value + * @throws IllegalStateException if the key is reused with a different type + */ + @Override + public void put(Key key, T value) { + keyTypes.compute(key.name(), (k, existingType) -> { + if (existingType == null) { + return key.type(); + } else { + if (!existingType.equals(key.type())) { + throw new IllegalStateException( + "Key '" + key.name() + "' already associated with type " + existingType.getName()); + } + return existingType; + } + }); + + @SuppressWarnings("unchecked") + T previous = (T) values.put(key, value); + + if (Objects.deepEquals(value, previous)) { + // no change + return; + } + + // notify listeners + var list = listeners.get(key); + if (list != null) { + for (var ref : list) { + var listener = ref.get(); + if (listener != null) { + @SuppressWarnings("unchecked") + Listener typedListener = (Listener) listener; + typedListener.valueChanged(value); + } + } + // clean up dead weak references + list.removeIf(ref -> ref.get() == null); + } + } + + /** + * Retrieves the value for the given key, or {@code null} if not set. + * + * @param key the key + * @param the type + * @return the stored value or null + */ + @Override + @SuppressWarnings("unchecked") + public T get(Key key) { + return (T) values.get(key); + } + + /** + * Checks if a value exists for the given key. + * + * @param key the key + * @return true if a value is present + */ + @Override + public boolean contains(Key key) { + return values.containsKey(key); + } + + /** + * Removes the value and any listeners for the given key. + * + * @param key the key + * @return the removed value, or null if absent + */ + @Override + public Object remove(Key key) { + keyTypes.remove(key.name()); + var removed = values.remove(key); + listeners.remove(key); + return removed; + } + + /** + * Clears all keys and listeners from the context. + */ + @Override + public void clear() { + values.clear(); + keyTypes.clear(); + listeners.clear(); + } + + /** + * Registers a listener to be notified whenever the value for the key changes. + * The listener is weakly referenced to avoid preventing its garbage collection. + * + * @param key the key + * @param listener the listener + * @param the type + */ + @Override + public void addListener(Key key, Listener listener) { + listeners.computeIfAbsent(key, k -> new CopyOnWriteArrayList<>()).add(new WeakReference<>(listener)); + } + + /** + * Unregisters a previously registered listener for the key. + * + * @param key the key + * @param listener the listener to remove + * @param the type + */ + @Override + public void removeListener(Key key, Listener listener) { + var list = listeners.get(key); + if (list != null) { + list.removeIf(ref -> { + var l = ref.get(); + return l == null || l.equals(listener); + }); + if (list.isEmpty()) { + listeners.remove(key); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/conflux/CtxInterface.java b/src/main/java/conflux/CtxInterface.java new file mode 100644 index 0000000..b3b6c51 --- /dev/null +++ b/src/main/java/conflux/CtxInterface.java @@ -0,0 +1,121 @@ +/** + * Copyright (C) 2025, Leo Galambos + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * 3. All advertising materials mentioning features or use of this software must + * display the following acknowledgement: + * This product includes software developed by the Egothor project. + * + * 4. Neither the name of the copyright holder nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ +package conflux; + +/** + * A type-safe, thread-safe context interface for storing and retrieving + * key-value pairs. + * + * Implementations of this interface support: + *
    + *
  • Recording type-safe associations between keys and values
  • + *
  • Automatic listener notification when a value changes
  • + *
  • Context isolation (e.g., per name or use case)
  • + *
+ * + * This interface is sealed to only permit known, safe implementations. + * + * @see Key + * @see Listener + * + * @author Leo Galambos + */ +public sealed interface CtxInterface permits Ctx, CtxInstance { + + /** + * Stores or updates the value for a given key. If the key is used for the first + * time, its type is recorded. If the key has been used before with a different + * type, an exception will be thrown. + * + * After setting the value, any registered listeners for that key will be + * notified (if the value was modified). + * + * @param key the key + * @param value the value to store + * @param the type of the value + * @throws IllegalStateException if the key is reused with a different type + */ + void put(Key key, T value); + + /** + * Retrieves the value for the given key, or {@code null} if not set. + * + * @param key the key + * @param the type + * @return the stored value or null + */ + T get(Key key); + + /** + * Checks if a value exists for the given key. + * + * @param key the key + * @return true if a value is present + */ + boolean contains(Key key); + + /** + * Removes the value and any listeners for the given key. + * + * @param key the key + * @return the removed value, or null if absent + */ + Object remove(Key key); + + /** + * Clears all keys and listeners from the context. + */ + void clear(); + + /** + * Registers a listener to be notified whenever the value for the key changes. + * The listener is weakly referenced to avoid preventing its garbage collection. + * + * @param key the key + * @param listener the listener + * @param the type + */ + void addListener(Key key, Listener listener); + + /** + * Unregisters a previously registered listener for the key. + * + * @param key the key + * @param listener the listener to remove + * @param the type + */ + void removeListener(Key key, Listener listener); + +} \ No newline at end of file diff --git a/src/main/java/conflux/package-info.java b/src/main/java/conflux/package-info.java index 8ef96a9..83fb5c6 100644 --- a/src/main/java/conflux/package-info.java +++ b/src/main/java/conflux/package-info.java @@ -1,28 +1,33 @@ /** * Provides a lightweight, type-safe, application-wide context mechanism for * sharing strongly typed data among otherwise decoupled classes. - *

- * The {@link conflux.Ctx} class acts as a central, generic context storage - * supporting key-based put/get operations. Each {@link conflux.Key} defines a - * unique, strongly typed entry in the context, ensuring type consistency and - * preventing misuse. {@link conflux.Listener} interfaces allow clients to - * observe value changes for specific keys. Listeners are held with weak - * references to avoid memory leaks. - *

- * Typical usage involves defining {@link conflux.Key} instances with explicit - * types, storing values through {@code Ctx.INSTANCE.put()}, and retrieving them - * with {@code Ctx.INSTANCE.get()}. Listeners can be attached via - * {@code Ctx.INSTANCE.addListener()} to react to context changes in a decoupled - * and thread-safe manner. Values and listeners can be removed individually or - * the entire context can be cleared. - *

- * Best Practices: + * + * The {@link conflux.Ctx} enum serves as a central, generic context storage, + * supporting key-based put/get operations. Each {@link conflux.Key} instance + * defines a unique, strongly typed entry in the context, ensuring type + * consistency and preventing misuse. + * + * The {@link conflux.Listener} interface allows clients to observe value + * changes for specific keys. Listeners are weakly referenced to prevent memory + * leaks. + * + * Typical usage: + *

    + *
  • Define {@link conflux.Key} instances with explicit types
  • + *
  • Store values via {@code Ctx.INSTANCE.put(key, value)}
  • + *
  • Retrieve values via {@code Ctx.INSTANCE.get(key)}
  • + *
  • Register listeners with {@code Ctx.INSTANCE.addListener()}
  • + *
+ * + * Values and listeners can be removed individually, or the entire context can + * be cleared. + * + * Best practices: *
    *
  • Define keys as constants to maintain consistency and avoid - * collisions.
  • - *
  • Remove listeners when no longer needed (although weak references help - * prevent leaks).
  • - *
  • Use unique key names to ensure no accidental type conflicts.
  • + * collisions + *
  • Remove listeners when no longer needed
  • + *
  • Use unique key names to avoid accidental type conflicts
  • *
* * @see conflux.Ctx diff --git a/src/test/java/conflux/CtxTest.java b/src/test/java/conflux/CtxTest.java index 6717d26..0b9b432 100644 --- a/src/test/java/conflux/CtxTest.java +++ b/src/test/java/conflux/CtxTest.java @@ -36,6 +36,7 @@ package conflux; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -159,4 +160,50 @@ public class CtxTest { assertTrue(flag[0] || !flag[0]); System.out.println("...ok"); } + + @Test + void testMultipleContextsIsolation() { + System.out.println("testMultipleContextsIsolation"); + var ctxA = Ctx.INSTANCE.getContext("A"); + var ctxB = Ctx.INSTANCE.getContext("B"); + + ctxA.put(intKey, 10); + ctxB.put(intKey, 20); + + assertEquals(10, ctxA.get(intKey)); + assertEquals(20, ctxB.get(intKey)); + assertNull(Ctx.INSTANCE.get(intKey)); // Default context is separate + + ctxA.clear(); + assertFalse(ctxA.contains(intKey)); + assertEquals(20, ctxB.get(intKey)); + + System.out.println("...ok"); + } + + @Test + void testListenerNotificationInSeparateContexts() { + System.out.println("testListenerNotificationInSeparateContexts"); + var ctxA = Ctx.INSTANCE.getContext("A"); + var ctxB = Ctx.INSTANCE.getContext("B"); + + var resultA = new StringBuilder(); + var resultB = new StringBuilder(); + + Listener listenerA = v -> resultA.append(v); + Listener listenerB = v -> resultB.append(v); + + ctxA.addListener(intKey, listenerA); + ctxB.addListener(intKey, listenerB); + + ctxA.put(intKey, 1); + ctxB.put(intKey, 2); + ctxA.put(intKey, 3); + ctxB.put(intKey, 4); + + assertEquals("13", resultA.toString()); + assertEquals("24", resultB.toString()); + + System.out.println("...ok"); + } } \ No newline at end of file