From b6b2c494920214f00144312b6c8756106fda2a8e Mon Sep 17 00:00:00 2001 From: Leon Linhart Date: Sun, 4 Jun 2023 23:52:48 +0200 Subject: [PATCH] feat(quitte): refactor collection listeners Closes #10 Closes #11 --- docs/changelog/0.8.0.md | 21 +- .../quitte/kotlinx/coroutines/Flow.kt | 10 +- .../collections/AbstractObservableDeque.java | 64 ++-- .../collections/AbstractObservableList.java | 117 ++++--- .../collections/AbstractObservableMap.java | 228 ++++++++++--- .../collections/AbstractObservableSet.java | 32 +- .../collections/DequeChangeListener.java | 185 +++++++++++ .../collections/ListChangeListener.java | 255 ++++++++++++++ .../quitte/collections/MapChangeListener.java | 109 ++++++ .../collections/ObservableCollection.java | 52 +-- .../quitte/collections/ObservableDeque.java | 223 +++---------- .../quitte/collections/ObservableList.java | 312 +++--------------- .../quitte/collections/ObservableMap.java | 114 +++---- .../quitte/collections/ObservableSet.java | 53 ++- ...geListener.java => SetChangeListener.java} | 58 +++- .../collections/WeakDequeChangeListener.java | 99 ++++++ .../collections/WeakListChangeListener.java | 99 ++++++ .../collections/WeakMapChangeListener.java | 100 ++++++ ...stener.java => WeakSetChangeListener.java} | 31 +- .../quitte/internal/binding/DequeBinding.java | 50 ++- .../quitte/internal/binding/ListBinding.java | 60 +++- .../quitte/internal/binding/MapBinding.java | 22 +- .../quitte/internal/binding/SetBinding.java | 20 +- .../UnmodifiableObservableDeque.java | 6 +- .../UnmodifiableObservableList.java | 6 +- .../UnmodifiableObservableMap.java | 18 +- .../UnmodifiableObservableSet.java | 6 +- .../collections/WrappingObservableDeque.java | 5 +- .../collections/WrappingObservableMap.java | 10 +- .../quitte/property/DequeProperty.java | 25 +- .../quitte/property/ListProperty.java | 35 +- .../osmerion/quitte/property/MapProperty.java | 38 +-- .../osmerion/quitte/property/SetProperty.java | 3 +- .../collections/MockDequeChangeListener.java | 28 +- .../collections/MockListChangeListener.java | 71 +++- .../collections/MockMapChangeListener.java | 34 +- .../collections/MockSetChangeListener.java | 47 +-- .../collections/ObservableDequeTest.java | 6 +- .../collections/ObservableListTest.java | 11 + .../quitte/collections/ObservableMapTest.java | 36 ++ 40 files changed, 1741 insertions(+), 958 deletions(-) create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/DequeChangeListener.java create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/ListChangeListener.java create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/MapChangeListener.java rename modules/quitte/src/main/java/com/osmerion/quitte/collections/{CollectionChangeListener.java => SetChangeListener.java} (54%) create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakDequeChangeListener.java create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakListChangeListener.java create mode 100644 modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakMapChangeListener.java rename modules/quitte/src/main/java/com/osmerion/quitte/collections/{WeakCollectionChangeListener.java => WeakSetChangeListener.java} (74%) diff --git a/docs/changelog/0.8.0.md b/docs/changelog/0.8.0.md index b1837fb..5c2e538 100644 --- a/docs/changelog/0.8.0.md +++ b/docs/changelog/0.8.0.md @@ -6,23 +6,14 @@ _Not Released Yet_ - Added an explicit module descriptor (`module-info.java`) to `quitte-kotlinx-coroutines`. +- Refactored the collection listener API for improved usability. + - Local updates for lists now contain the old elements. [[GH-5](https://github.com/Osmerion/Quitte/issues/10)] + - `ObservableMap::entrySet` does now return an `ObservableSet`. [[GH-11](https://github.com/Osmerion/Quitte/issues/11)] - Various JavaDoc improvements. -#### Deprecations - -- Deprecations in `ObservableMap.Change`: - - Deprecated `getOldValue()` in favor of the canonical record accessor `oldValue()`. - - Deprecated `getNewValue()` in favor of the canonical record accessor `newValue()`. - #### Breaking Changes -- Removals in `ObservableMap.Change`: - - `getAddedElements()` was removed. Use `addedElements()` instead. - - `getRemovedElements()` was removed. Use `removedElements()` instead. - - `getUpdatedElements()` was removed. Use `updatedElements()` instead. - - `copy(BiFunction)` was removed without replacement. -- Removals in `ObservableSet.Change`: - - `getAddedElements()` was removed. Use `addedElements()` instead. - - `getRemovedElements()` was removed. Use `removedElements()` instead. - - `copy(Function)` was removed without replacement. +- The collection listener API was refactored for improved usability. + Consequentially, all deprecated methods were removed and a few additional API + were made. - Updated [kotlinx.coroutines](https://github.com/Kotlin/kotlinx.coroutines) dependency to `1.7.1`. \ No newline at end of file diff --git a/modules/quitte-kotlinx-coroutines/src/main/java/com/osmerion/quitte/kotlinx/coroutines/Flow.kt b/modules/quitte-kotlinx-coroutines/src/main/java/com/osmerion/quitte/kotlinx/coroutines/Flow.kt index 31fb4d6..eba5403 100644 --- a/modules/quitte-kotlinx-coroutines/src/main/java/com/osmerion/quitte/kotlinx/coroutines/Flow.kt +++ b/modules/quitte-kotlinx-coroutines/src/main/java/com/osmerion/quitte/kotlinx/coroutines/Flow.kt @@ -31,10 +31,12 @@ package com.osmerion.quitte.kotlinx.coroutines import com.osmerion.quitte.InvalidationListener -import com.osmerion.quitte.collections.CollectionChangeListener +import com.osmerion.quitte.collections.ListChangeListener +import com.osmerion.quitte.collections.MapChangeListener import com.osmerion.quitte.collections.ObservableList import com.osmerion.quitte.collections.ObservableMap import com.osmerion.quitte.collections.ObservableSet +import com.osmerion.quitte.collections.SetChangeListener import com.osmerion.quitte.value.ObservableValue import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -80,7 +82,7 @@ public fun ObservableValue.asFlow(): Flow = callbackFlow { * @since 0.3.0 */ public fun ObservableList.asFlow(): Flow> = callbackFlow { - val listener = CollectionChangeListener> { + val listener = ListChangeListener { _, _ -> trySend(this@asFlow.toList()) } @@ -103,7 +105,7 @@ public fun ObservableList.asFlow(): Flow> = callbackFlow { * @since 0.3.0 */ public fun ObservableMap.asFlow(): Flow> = callbackFlow { - val listener = CollectionChangeListener> { + val listener = MapChangeListener { _, _ -> trySend(this@asFlow.toMap()) } @@ -126,7 +128,7 @@ public fun ObservableMap.asFlow(): Flow> = callbackFlow { * @since 0.3.0 */ public fun ObservableSet.asFlow(): Flow> = callbackFlow { - val listener = CollectionChangeListener> { + val listener = SetChangeListener { _, _ -> trySend(this@asFlow.toSet()) } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableDeque.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableDeque.java index e977d28..8eadc98 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableDeque.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableDeque.java @@ -30,13 +30,7 @@ */ package com.osmerion.quitte.collections; -import java.util.AbstractCollection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; @@ -53,7 +47,7 @@ */ public abstract class AbstractObservableDeque extends AbstractCollection implements ObservableDeque { - private transient final CopyOnWriteArraySet>> changeListeners = new CopyOnWriteArraySet<>(); + private transient final CopyOnWriteArraySet> changeListeners = new CopyOnWriteArraySet<>(); private transient final CopyOnWriteArraySet invalidationListeners = new CopyOnWriteArraySet<>(); @Nullable @@ -65,7 +59,7 @@ public abstract class AbstractObservableDeque extends AbstractCollection i * @since 0.1.0 */ @Override - public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean addChangeListener(DequeChangeListener listener) { return this.changeListeners.add(Objects.requireNonNull(listener)); } @@ -75,7 +69,7 @@ public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean removeChangeListener(DequeChangeListener listener) { return this.changeListeners.remove(Objects.requireNonNull(listener)); } @@ -120,7 +114,7 @@ public final void addFirst(@Nullable E element) { this.addFirstImpl(element); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logAdd(ObservableDeque.Site.HEAD, element); + changeBuilder.logAdd(DequeChangeListener.Site.HEAD, element); } } @@ -131,7 +125,7 @@ public final void addLast(E element) { this.addLastImpl(element); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logAdd(ObservableDeque.Site.TAIL, element); + changeBuilder.logAdd(DequeChangeListener.Site.TAIL, element); } } @@ -141,7 +135,7 @@ public final void addLast(E element) { public final boolean offerFirst(E element) { if (this.offerFirstImpl(element)) { try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logAdd(ObservableDeque.Site.HEAD, element); + changeBuilder.logAdd(DequeChangeListener.Site.HEAD, element); } return true; @@ -156,7 +150,7 @@ public final boolean offerFirst(E element) { public final boolean offerLast(@Nullable E element) { if (this.offerLastImpl(element)) { try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logAdd(ObservableDeque.Site.TAIL, element); + changeBuilder.logAdd(DequeChangeListener.Site.TAIL, element); } return true; @@ -174,7 +168,7 @@ public final E removeFirst() { E element = this.removeFirstImpl(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.HEAD, element); + changeBuilder.logRemove(DequeChangeListener.Site.HEAD, element); } return element; @@ -189,7 +183,7 @@ public final E removeLast() { E element = this.removeLastImpl(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.TAIL, element); + changeBuilder.logRemove(DequeChangeListener.Site.TAIL, element); } return element; @@ -204,7 +198,7 @@ public final E pollFirst() { E element = this.pollFirstImpl(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.HEAD, element); + changeBuilder.logRemove(DequeChangeListener.Site.HEAD, element); } return element; @@ -218,7 +212,7 @@ public final E pollLast() { E element = this.pollLastImpl(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.TAIL, element); + changeBuilder.logRemove(DequeChangeListener.Site.TAIL, element); } return element; @@ -235,7 +229,7 @@ public final boolean removeFirstOccurrence(@Nullable Object object) { itr.remove(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, element); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, element); } return true; @@ -256,7 +250,7 @@ public final boolean removeLastOccurrence(@Nullable Object object) { itr.remove(); try (ChangeBuilder changeBuilder = this.beginChange()) { - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, element); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, element); } return true; @@ -334,7 +328,7 @@ public final E pop() { */ protected final class ChangeBuilder implements AutoCloseable { - private final List> localChanges = new ArrayList<>(1); + private final List> localChanges = new ArrayList<>(1); private int depth = 0; @@ -358,7 +352,7 @@ public void close() { AbstractObservableDeque.this.changeBuilder = null; if (this.localChanges.isEmpty()) return; - var change = new ObservableDeque.Change<>(Collections.unmodifiableList(this.localChanges)); + var change = new DequeChangeListener.Change<>(Collections.unmodifiableList(this.localChanges)); for (var listener : AbstractObservableDeque.this.changeListeners) { if (listener.isInvalid()) { @@ -366,7 +360,7 @@ public void close() { continue; } - listener.onChanged(change); + listener.onChanged(AbstractObservableDeque.this, change); if (listener.isInvalid()) AbstractObservableDeque.this.changeListeners.remove(listener); } @@ -390,16 +384,16 @@ public void close() { * * @since 0.1.0 */ - public void logAdd(ObservableDeque.Site site, @Nullable E element) { + public void logAdd(DequeChangeListener.Site site, @Nullable E element) { if (!this.localChanges.isEmpty()) { int lastIndex = this.localChanges.size() - 1; - ObservableDeque.LocalChange lastLocalChange = this.localChanges.get(lastIndex); + DequeChangeListener.LocalChange lastLocalChange = this.localChanges.get(lastIndex); - if (lastLocalChange instanceof ObservableDeque.LocalChange.Insertion && site == lastLocalChange.getSite()) { - ArrayList elements = new ArrayList<>(lastLocalChange.getElements()); + if (lastLocalChange instanceof DequeChangeListener.LocalChange.Insertion && site == lastLocalChange.site()) { + ArrayList elements = new ArrayList<>(lastLocalChange.elements()); elements.add(element); - this.localChanges.set(lastIndex, new ObservableDeque.LocalChange.Insertion<>(site, Collections.unmodifiableList(elements))); + this.localChanges.set(lastIndex, new DequeChangeListener.LocalChange.Insertion<>(site, Collections.unmodifiableList(elements))); return; } } @@ -407,7 +401,7 @@ public void logAdd(ObservableDeque.Site site, @Nullable E element) { ArrayList elements = new ArrayList<>(); elements.add(element); - this.localChanges.add(new ObservableDeque.LocalChange.Insertion<>(site, Collections.unmodifiableList(elements))); + this.localChanges.add(new DequeChangeListener.LocalChange.Insertion<>(site, Collections.unmodifiableList(elements))); } /** @@ -418,16 +412,16 @@ public void logAdd(ObservableDeque.Site site, @Nullable E element) { * * @since 0.1.0 */ - public void logRemove(ObservableDeque.Site site, @Nullable E element) { + public void logRemove(DequeChangeListener.Site site, @Nullable E element) { if (!this.localChanges.isEmpty()) { int lastIndex = this.localChanges.size() - 1; - ObservableDeque.LocalChange lastLocalChange = this.localChanges.get(lastIndex); + DequeChangeListener.LocalChange lastLocalChange = this.localChanges.get(lastIndex); - if (lastLocalChange instanceof ObservableDeque.LocalChange.Removal && site == lastLocalChange.getSite()) { - ArrayList elements = new ArrayList<>(lastLocalChange.getElements()); + if (lastLocalChange instanceof DequeChangeListener.LocalChange.Removal && site == lastLocalChange.site()) { + ArrayList elements = new ArrayList<>(lastLocalChange.elements()); elements.add(element); - this.localChanges.set(lastIndex, new ObservableDeque.LocalChange.Removal<>(site, Collections.unmodifiableList(elements))); + this.localChanges.set(lastIndex, new DequeChangeListener.LocalChange.Removal<>(site, Collections.unmodifiableList(elements))); return; } } @@ -435,7 +429,7 @@ public void logRemove(ObservableDeque.Site site, @Nullable E element) { ArrayList elements = new ArrayList<>(); elements.add(element); - this.localChanges.add(new ObservableDeque.LocalChange.Removal<>(site, Collections.unmodifiableList(elements))); + this.localChanges.add(new DequeChangeListener.LocalChange.Removal<>(site, Collections.unmodifiableList(elements))); } } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableList.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableList.java index 104570e..fec3527 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableList.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableList.java @@ -61,7 +61,7 @@ */ public abstract class AbstractObservableList extends AbstractList implements ObservableList { - private transient final CopyOnWriteArraySet>> changeListeners = new CopyOnWriteArraySet<>(); + private transient final CopyOnWriteArraySet> changeListeners = new CopyOnWriteArraySet<>(); private transient final CopyOnWriteArraySet invalidationListeners = new CopyOnWriteArraySet<>(); @Nullable @@ -73,7 +73,7 @@ public abstract class AbstractObservableList extends AbstractList implemen * @since 0.1.0 */ @Override - public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean addChangeListener(ListChangeListener listener) { return this.changeListeners.add(Objects.requireNonNull(listener)); } @@ -83,7 +83,7 @@ public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean removeChangeListener(ListChangeListener listener) { return this.changeListeners.remove(Objects.requireNonNull(listener)); } @@ -423,7 +423,7 @@ public void close() { AbstractObservableList.this.changeBuilder = null; if (this.localChanges.isEmpty()) return; - ObservableList.Change change = null; + ListChangeListener.Change change = null; /* * Compressing changes is a non-trivial task and to keep the implementation relatively simple, only @@ -541,15 +541,33 @@ public void close() { * the resulting list just happened to be equal in size to the original ist. */ if (expectedAdditions.values().stream().allMatch(List::isEmpty) && expectedRemovals.values().stream().allMatch(List::isEmpty)) { - change = new ObservableList.Change.Permutation<>(IntStream.of(permutation).boxed().toList()); + boolean hasChanged = false; + + for (int i = 0; i < permutation.length; i++) { + if (i != permutation[i]) { + hasChanged = true; + break; + } + } + + /* + * If the permutation is the identity function, the list has not changed, and we do not notify + * listeners. + */ + if (!hasChanged) { + return; + } + + change = new ListChangeListener.Change.Permutation<>(IntStream.of(permutation).boxed().toList()); } } /* We couldn't reconstruct a permutation and need to "compress" local changes. */ if (change == null) { - List> localChanges = new ArrayList<>(this.localChanges.size()); + List> localChanges = new ArrayList<>(this.localChanges.size()); - List updateElements = new ArrayList<>(); + List oldUpdateElements = new ArrayList<>(); + List newUpdateElements = new ArrayList<>(); int updateFrom = -1; List batchElements = new ArrayList<>(); @@ -559,112 +577,91 @@ public void close() { int batchType = 0; for (WorkingLocalChange wlc : this.localChanges) { - if (wlc instanceof WorkingLocalChange.Insertion) { + if (wlc instanceof WorkingLocalChange.Insertion wlInsert) { if (batchType != 1) { if (batchType == 2) { - if (batchFrom == wlc.from && batchElements.size() == ((WorkingLocalChange.Insertion) wlc).elements.size()) { - if (!updateElements.isEmpty()) { - if (wlc.from <= updateFrom + updateElements.size() && updateFrom <= wlc.to) { - int offset = abs(wlc.from - updateFrom); - - updateFrom = min(wlc.from, updateFrom); - updateElements.addAll(offset, ((WorkingLocalChange.Insertion) wlc).elements); - } else { - localChanges.add(new LocalChange.Update<>(updateFrom, new ArrayList<>(updateElements))); - updateElements.clear(); - - updateFrom = batchFrom; - updateElements.addAll(((WorkingLocalChange.Insertion) wlc).elements); - } - } else { - updateFrom = batchFrom; - updateElements.addAll(((WorkingLocalChange.Insertion) wlc).elements); - } + if (batchFrom == wlc.from && batchElements.size() == wlInsert.elements.size()) { + updateFrom = batchFrom; + newUpdateElements.addAll(wlInsert.elements); + oldUpdateElements.addAll(batchElements); batchType = 0; batchElements.clear(); continue; } else { - if (!updateElements.isEmpty()) { - localChanges.add(new LocalChange.Update<>(updateFrom, new ArrayList<>(updateElements))); - updateFrom = -1; - updateElements.clear(); - } - - localChanges.add(new LocalChange.Removal<>(batchFrom, new ArrayList<>(batchElements))); + localChanges.add(new ListChangeListener.LocalChange.Removal<>(batchFrom, new ArrayList<>(batchElements))); batchElements.clear(); } } batchFrom = wlc.from; batchType = 1; - batchElements.addAll(((WorkingLocalChange.Insertion) wlc).elements); + batchElements.addAll(wlInsert.elements); } else if (wlc.from <= batchFrom + batchElements.size() && batchFrom <= wlc.to) { int offset = abs(wlc.from - batchFrom); batchFrom = min(wlc.from, batchFrom); - batchElements.addAll(offset, ((WorkingLocalChange.Insertion) wlc).elements); + batchElements.addAll(offset, wlInsert.elements); } else { - if (!updateElements.isEmpty()) { - localChanges.add(new LocalChange.Update<>(updateFrom, new ArrayList<>(updateElements))); + if (!newUpdateElements.isEmpty()) { + localChanges.add(new ListChangeListener.LocalChange.Update<>(updateFrom, new ArrayList<>(oldUpdateElements), new ArrayList<>(newUpdateElements))); updateFrom = -1; - updateElements.clear(); + newUpdateElements.clear(); } - localChanges.add(new LocalChange.Insertion<>(batchFrom, new ArrayList<>(batchElements))); + localChanges.add(new ListChangeListener.LocalChange.Insertion<>(batchFrom, new ArrayList<>(batchElements))); batchElements.clear(); batchFrom = wlc.from; - batchType = 1; - batchElements.addAll(((WorkingLocalChange.Insertion) wlc).elements); + batchElements.addAll(wlInsert.elements); } - } else if (wlc instanceof WorkingLocalChange.Removal) { + } else if (wlc instanceof WorkingLocalChange.Removal wlRemove) { if (batchType != 2) { - if (!updateElements.isEmpty()) { - localChanges.add(new LocalChange.Update<>(updateFrom, new ArrayList<>(updateElements))); + if (!newUpdateElements.isEmpty()) { + localChanges.add(new ListChangeListener.LocalChange.Update<>(updateFrom, new ArrayList<>(oldUpdateElements), new ArrayList<>(newUpdateElements))); updateFrom = -1; - updateElements.clear(); + newUpdateElements.clear(); } - if (!batchElements.isEmpty()) localChanges.add(new LocalChange.Insertion<>(batchFrom, new ArrayList<>(batchElements))); + if (!batchElements.isEmpty()) localChanges.add(new ListChangeListener.LocalChange.Insertion<>(batchFrom, new ArrayList<>(batchElements))); } else if (wlc.from <= batchFrom + batchElements.size() && batchFrom <= wlc.to) { int offset = abs(wlc.from - batchFrom); batchFrom = min(wlc.from, batchFrom); - batchElements.addAll(offset, ((WorkingLocalChange.Removal) wlc).elements); + batchElements.addAll(offset, wlRemove.elements); continue; } else { - if (!updateElements.isEmpty()) { - localChanges.add(new LocalChange.Update<>(updateFrom, new ArrayList<>(updateElements))); - updateFrom = -1; - updateElements.clear(); - } - - localChanges.add(new LocalChange.Removal<>(batchFrom, new ArrayList<>(batchElements))); + localChanges.add(new ListChangeListener.LocalChange.Removal<>(batchFrom, new ArrayList<>(batchElements))); batchElements.clear(); } batchFrom = wlc.from; batchType = 2; - batchElements.addAll(((WorkingLocalChange.Removal) wlc).elements); + batchElements.addAll(wlRemove.elements); } else { throw new IllegalStateException(); } } - if (!updateElements.isEmpty()) localChanges.add(new LocalChange.Update<>(updateFrom, updateElements)); + if (!newUpdateElements.isEmpty()) { + localChanges.add(new ListChangeListener.LocalChange.Update<>( + updateFrom, + new ArrayList<>(oldUpdateElements), + new ArrayList<>(newUpdateElements) + )); + } if (!batchElements.isEmpty()) { localChanges.add(switch (batchType) { - case 1 -> new LocalChange.Insertion<>(batchFrom, batchElements); - case 2 -> new LocalChange.Removal<>(batchFrom, batchElements); + case 1 -> new ListChangeListener.LocalChange.Insertion<>(batchFrom, batchElements); + case 2 -> new ListChangeListener.LocalChange.Removal<>(batchFrom, batchElements); default -> throw new IllegalStateException(); }); } - change = new ObservableList.Change.Update<>(List.copyOf(localChanges)); + change = new ListChangeListener.Change.Update<>(List.copyOf(localChanges)); } for (var listener : AbstractObservableList.this.changeListeners) { @@ -673,7 +670,7 @@ public void close() { continue; } - listener.onChanged(change); + listener.onChanged(AbstractObservableList.this, change); if (listener.isInvalid()) AbstractObservableList.this.changeListeners.remove(listener); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableMap.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableMap.java index 1186ff3..bf52ee9 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableMap.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableMap.java @@ -30,13 +30,7 @@ */ package com.osmerion.quitte.collections; -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Objects; -import java.util.Set; +import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import javax.annotation.Nullable; @@ -54,7 +48,7 @@ */ public abstract class AbstractObservableMap extends AbstractMap implements ObservableMap { - private transient final CopyOnWriteArraySet>> changeListeners = new CopyOnWriteArraySet<>(); + private transient final CopyOnWriteArraySet> changeListeners = new CopyOnWriteArraySet<>(); private transient final CopyOnWriteArraySet invalidationListeners = new CopyOnWriteArraySet<>(); @Nullable @@ -66,7 +60,7 @@ public abstract class AbstractObservableMap extends AbstractMap impl * @since 0.1.0 */ @Override - public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean addChangeListener(MapChangeListener listener) { return this.changeListeners.add(Objects.requireNonNull(listener)); } @@ -76,7 +70,7 @@ public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean removeChangeListener(MapChangeListener listener) { return this.changeListeners.remove(Objects.requireNonNull(listener)); } @@ -114,6 +108,155 @@ protected final ChangeBuilder beginChange() { return this.changeBuilder; } + @Nullable + private transient ObservableSet> entrySet; + + /** + * {@inheritDoc} + * + * @since 0.8.0 + */ + @Override + public ObservableSet> entrySet() { + ObservableSet> entrySet = this.entrySet; + + if (entrySet == null) { + entrySet = new WrappingObservableEntrySet(this.entrySetImpl()) { + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final MapChangeListener changeListener; + + { + AbstractObservableMap.this.addChangeListener(new WeakMapChangeListener<>(this.changeListener = (observable, change) -> { + try (ChangeBuilder changeBuilder = this.beginChange()) { + change.addedElements().forEach((key, value) -> changeBuilder.logAdd(new SimpleEntry<>(key, value))); + change.removedElements().forEach((key, value) -> changeBuilder.logRemove(new SimpleEntry<>(key, value))); + change.updatedElements().forEach((key, update) -> { + changeBuilder.logRemove(new SimpleEntry<>(key, update.oldValue())); + changeBuilder.logAdd(new SimpleEntry<>(key, update.newValue())); + }); + } + })); + } + + @Override + protected boolean addImpl(@Nullable Entry element) { + return false; + } + + @SuppressWarnings("unchecked") + @Override + protected boolean removeImpl(@Nullable Object element) { + Objects.requireNonNull(element); + + if (AbstractObservableMap.this.entrySetImpl().remove((Map.Entry) element)) { + try (AbstractObservableMap.ChangeBuilder changeBuilder = AbstractObservableMap.this.beginChange()) { + /* + * Technically, this cast is wrong and there is a tiny chance that it could fail if the EntrySet of + * the backing map implementation does not perform identity-checks in it's Set#remove(Object) + * method. + * + * However, since the possibility of encountering such an edge-case is extremely tiny and arguably + * a misuse of the API, we will not provide a workaround at this time. Should this ever become a + * more serious problem, this implementation will need to be reconsidered. + */ + Entry entry = (Entry) element; + changeBuilder.logRemove(entry.getKey(), entry.getValue()); + } + + return true; + } + + return false; + } + + }; + + this.entrySet = entrySet; + } + + return entrySet; + } + + @Nullable + private transient ObservableSet keySet; + + /** + * {@inheritDoc} + * + * @since 0.8.0 + */ + @Override + public ObservableSet keySet() { + ObservableSet keySet = this.keySet; + + if (keySet == null) { + keySet = new AbstractObservableSet<>() { + + @SuppressWarnings({"FieldCanBeLocal", "unused"}) + private final MapChangeListener changeListener; + + { + AbstractObservableMap.this.addChangeListener(new WeakMapChangeListener<>(this.changeListener = (observable, change) -> { + try (ChangeBuilder changeBuilder = this.beginChange()) { + change.addedElements().keySet().forEach(changeBuilder::logAdd); + change.removedElements().keySet().forEach(changeBuilder::logRemove); + } + })); + } + + @Override + protected boolean addImpl(@Nullable K element) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean removeImpl(@Nullable Object element) { + int s = AbstractObservableMap.this.size(); + AbstractObservableMap.this.remove(element); + + return s != AbstractObservableMap.this.size(); + } + + @Override + public Iterator iterator() { + return new Iterator<>() { + + private final Iterator> impl = AbstractObservableMap.this.entrySetImpl().iterator(); + + @Override + public boolean hasNext() { + return this.impl.hasNext(); + } + + @Override + public K next() { + return this.impl.next().getKey(); + } + + @Override + public void remove() { + this.impl.remove(); + } + + }; + } + + @Override + public int size() { + return AbstractObservableMap.this.size(); + } + + }; + + this.keySet = keySet; + } + + return keySet; + } + + protected abstract Set> entrySetImpl(); + @Nullable protected abstract V putImpl(@Nullable K key, @Nullable V value); @@ -155,7 +298,7 @@ protected final class ChangeBuilder implements AutoCloseable { private HashMap added, removed; @Nullable - private HashMap> updated; + private HashMap> updated; private int depth = 0; @@ -181,15 +324,15 @@ public void close() { (this.removed == null || this.removed.isEmpty()) && (this.updated == null || this.updated.isEmpty())) return; - var change = new Change<>(this.added, this.removed, this.updated); + var change = new MapChangeListener.Change<>(this.added, this.removed, this.updated); - for (CollectionChangeListener> listener : AbstractObservableMap.this.changeListeners) { + for (MapChangeListener listener : AbstractObservableMap.this.changeListeners) { if (listener.isInvalid()) { AbstractObservableMap.this.changeListeners.remove(listener); continue; } - listener.onChanged(change); + listener.onChanged(AbstractObservableMap.this, change); if (listener.isInvalid()) AbstractObservableMap.this.changeListeners.remove(listener); } @@ -258,7 +401,7 @@ public void logUpdate(@Nullable K key, @Nullable V oldValue, @Nullable V newValu } if (this.updated == null) this.updated = new HashMap<>(); - this.updated.put(key, new Change.Update<>(oldValue, newValue)); + this.updated.put(key, new MapChangeListener.Change.Update<>(oldValue, newValue)); } } @@ -268,7 +411,7 @@ public void logUpdate(@Nullable K key, @Nullable V oldValue, @Nullable V newValu * * @since 0.1.0 */ - protected abstract class AbstractObservableEntrySet extends AbstractSet> { + protected abstract class AbstractObservableEntrySet extends AbstractObservableSet> implements ObservableSet> { protected final Set> impl; @@ -308,6 +451,11 @@ public void remove() { }; } + @Override + protected boolean addImpl(@Nullable Entry element) { + throw new UnsupportedOperationException(); + } + @SuppressWarnings("unchecked") @Override public final Object[] toArray() { @@ -341,35 +489,35 @@ public final T[] toArray(T[] a) { * * @since 0.1.0 */ - protected final class WrappingObservableEntrySet extends AbstractObservableEntrySet { + protected abstract class WrappingObservableEntrySet extends AbstractObservableEntrySet { public WrappingObservableEntrySet(Set> impl) { super(impl); } - @SuppressWarnings("unchecked") - @Override - public boolean remove(Object element) { - if (this.impl.remove(element)) { - try (ChangeBuilder changeBuilder = AbstractObservableMap.this.beginChange()) { - /* - * Technically, this cast is wrong and there is a tiny chance that it could fail if the EntrySet of - * the backing map implementation does not perform identity-checks in it's Set#remove(Object) - * method. - * - * However, since the possibility of encountering such an edge-case is extremely tiny and arguably - * a misuse of the API, we will not provide a workaround at this time. Should this ever become a - * more serious problem, this implementation will need to be reconsidered. - */ - Entry entry = (Entry) element; - changeBuilder.logRemove(entry.getKey(), entry.getValue()); - } - - return true; - } - - return false; - } +// @SuppressWarnings("unchecked") +// @Override +// public boolean remove(Object element) { +// if (this.impl.remove(element)) { +// try (ChangeBuilder changeBuilder = AbstractObservableMap.this.beginChange()) { +// /* +// * Technically, this cast is wrong and there is a tiny chance that it could fail if the EntrySet of +// * the backing map implementation does not perform identity-checks in it's Set#remove(Object) +// * method. +// * +// * However, since the possibility of encountering such an edge-case is extremely tiny and arguably +// * a misuse of the API, we will not provide a workaround at this time. Should this ever become a +// * more serious problem, this implementation will need to be reconsidered. +// */ +// Entry entry = (Entry) element; +// changeBuilder.logRemove(entry.getKey(), entry.getValue()); +// } +// +// return true; +// } +// +// return false; +// } @Override public Entry wrap(Entry entry) { diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableSet.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableSet.java index 0f4357f..7aa1f2f 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableSet.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/AbstractObservableSet.java @@ -50,7 +50,7 @@ */ public abstract class AbstractObservableSet extends AbstractSet implements ObservableSet { - private transient final CopyOnWriteArraySet>> changeListeners = new CopyOnWriteArraySet<>(); + private transient final CopyOnWriteArraySet> changeListeners = new CopyOnWriteArraySet<>(); private transient final CopyOnWriteArraySet invalidationListeners = new CopyOnWriteArraySet<>(); @Nullable @@ -59,20 +59,20 @@ public abstract class AbstractObservableSet extends AbstractSet implements /** * {@inheritDoc} * - * @since 0.1.0 + * @since 0.8.0 */ @Override - public final boolean addChangeListener(CollectionChangeListener> listener) { + public final boolean addChangeListener(SetChangeListener listener) { return this.changeListeners.add(Objects.requireNonNull(listener)); } /** * {@inheritDoc} * - * @since 0.1.0 + * @since 0.8.0 */ @Override - public final boolean removeChangeListener(CollectionChangeListener> listener) { + public final boolean removeChangeListener(SetChangeListener listener) { return this.changeListeners.remove(Objects.requireNonNull(listener)); } @@ -115,7 +115,7 @@ protected final ChangeBuilder beginChange() { * * @param element the element to add * - * @return whether or not this set was modified as result of this operation + * @return whether this set was modified as result of this operation * * @since 0.1.0 */ @@ -126,7 +126,7 @@ protected final ChangeBuilder beginChange() { * * @param element the element to remove * - * @return whether or not this set was modified as result of this operation + * @return whether this set was modified as result of this operation * * @since 0.1.0 */ @@ -134,12 +134,11 @@ protected final ChangeBuilder beginChange() { @Override public final boolean add(E e) { - if (this.addImpl(e)) { - try (ChangeBuilder changeBuilder = this.beginChange()) { + try (ChangeBuilder changeBuilder = this.beginChange()) { + if (this.addImpl(e)) { changeBuilder.logAdd(e); + return true; } - - return true; } return false; @@ -148,12 +147,11 @@ public final boolean add(E e) { @Override @SuppressWarnings("unchecked") public final boolean remove(Object o) { - if (this.removeImpl(o)) { - try (ChangeBuilder changeBuilder = this.beginChange()) { + try (ChangeBuilder changeBuilder = this.beginChange()) { + if (this.removeImpl(o)) { changeBuilder.logRemove((E) o); + return true; } - - return true; } return false; @@ -194,7 +192,7 @@ public void close() { AbstractObservableSet.this.changeBuilder = null; if ((this.added == null || this.added.isEmpty()) && (this.removed == null || this.removed.isEmpty())) return; - var change = new ObservableSet.Change<>(this.added, this.removed); + var change = new SetChangeListener.Change<>(this.added, this.removed); for (var listener : AbstractObservableSet.this.changeListeners) { if (listener.isInvalid()) { @@ -202,7 +200,7 @@ public void close() { continue; } - listener.onChanged(change); + listener.onChanged(AbstractObservableSet.this, change); if (listener.isInvalid()) AbstractObservableSet.this.changeListeners.remove(listener); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/DequeChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/DequeChangeListener.java new file mode 100644 index 0000000..52f72a6 --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/DequeChangeListener.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Function; + +/** + * A listener that may be used to subscribe to changes to one or more {@link ObservableDeque observable deque}. + * + * @param the type of the deque's elements + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public interface DequeChangeListener { + + /** + * Processes changes to an {@link ObservableDeque observable deque} this listener is attached to. + * + * @param observable the observable deque + * @param change the change to process + * + * @since 0.8.0 + */ + void onChanged(ObservableDeque observable, DequeChangeListener.Change change); + + /** + * {@return whether this listener is invalid} + * + *

Once an observable collection discovers that a listener is invalid, it will stop notifying the listener of + * updates and release all strong references to the listener.

+ * + *

Once this method returned {@code true}, it must never return {@code false} again for the same instance. + * Breaking this contract may result in unexpected behavior.

+ * + * @since 0.8.0 + */ + default boolean isInvalid() { + return false; + } + + /** + * A change to a deque consists of one or more {@link LocalChange local updates} that apply to a specific + * {@link Site site} of the deque. + * + * @param the type of the deque's elements + * + * @since 0.8.0 + */ + record Change(List> localChanges) { + + public Change { + localChanges = List.copyOf(localChanges); + } + + } + + /** + * A change to a deque. This might either be an {@link Insertion}, or a {@link Removal}. + * + *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working + * with {@code LocalChange} objects.

+ * + * @param the type of the deque's elements + * + * @since 0.8.0 + */ + sealed interface LocalChange { + + /** + * A list of elements related to this change. How this list should be interpreted is defined by an implementing + * class. + * + * @return a list of elements related to this change + * + * @since 0.8.0 + */ + List elements(); + + /** + * {@return {@link Site site} of the deque to which the change applies} + * + * @since 0.8.0 + */ + Site site(); + + /** + * Represents insertion of one or more subsequent {@link #elements() elements} starting from a given + * {@link #site() site}. + * + * @since 0.8.0 + */ + record Insertion( + Site site, + List elements + ) implements LocalChange { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with List::copyOf because the maps might contain null keys and values + public Insertion { + elements = Collections.unmodifiableList(new ArrayList<>(elements)); + } + + } + + /** + * Represents removal of one or more subsequent {@link #elements() elements} starting from a given + * {@link #site() site}. + * + * @since 0.8.0 + */ + record Removal( + Site site, + List elements + ) implements LocalChange { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with List::copyOf because the maps might contain null keys and values + public Removal { + elements = Collections.unmodifiableList(new ArrayList<>(elements)); + } + + } + + } + + /** + * A modifiable site of a deque. + * + * @since 0.1.0 + */ + enum Site { + /** + * The "head" (or "front") of the deque. + * + * @since 0.1.0 + */ + HEAD, + /** + * The "tail" (or "back") of the deque. + * + * @since 0.1.0 + */ + TAIL, + /** + * An opaque position in the deque. + * + *

Using operations which produce "opaque change" is discouraged.

+ * + * @since 0.1.0 + */ + OPAQUE + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ListChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ListChangeListener.java new file mode 100644 index 0000000..b11c46e --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ListChangeListener.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A listener that may be used to subscribe to changes to one or more {@link ObservableList observable list}. + * + * @param the type of the list elements + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public interface ListChangeListener { + + /** + * Processes changes to an {@link ObservableList observable list} this listener is attached to. + * + * @param observable the observable list + * @param change the change to process + * + * @since 0.8.0 + */ + void onChanged(ObservableList observable, Change change); + + /** + * {@return whether this listener is invalid} + * + *

Once an observable collection discovers that a listener is invalid, it will stop notifying the listener of + * updates and release all strong references to the listener.

+ * + *

Once this method returned {@code true}, it must never return {@code false} again for the same instance. + * Breaking this contract may result in unexpected behavior.

+ * + * @since 0.8.0 + */ + default boolean isInvalid() { + return false; + } + + /** + * A change to a list may either be a {@link Permutation permutation}, or one or more local updates to parts of the + * list (represented as {@link LocalChange}). + * + *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working + * with {@code Change} objects.

+ * + * @param the type of the list's elements + * + * @since 0.8.0 + */ + sealed interface Change { + + /** + * A change to a list in which its elements are rearranged. + * + * @since 0.8.0 + */ + record Permutation( + List indices + ) implements Change { + + public Permutation { + indices = List.copyOf(indices); + } + + } + + /** + * A change to a list that consists of one or more local changes to the list. + * + * @since 0.8.0 + */ + record Update( + List> localChanges + ) implements Change { + + public Update { + localChanges = List.copyOf(localChanges); + } + + } + + } + + /** + * A change to a list. This might either be an {@link Insertion}, a {@link Removal}, or an {@link Update}. + * + *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working + * with {@code LocalChange} objects.

+ * + * @param the type of the list's elements + * + * @since 0.8.0 + */ + sealed interface LocalChange { + + /** + * Returns the index of the first element affected by this change. + * + * @return the index of the first element affected by this change + * + * @since 0.8.0 + */ + int index(); + + /** + * Represents insertion of one or more subsequent {@link #elements() elements} starting from a given + * {@link #index() index}. + * + *

Example:

+ *
+         * Initial
+         * Indices:   0 1 2 3 4 5
+         * Elements:  A B C D E F
+         *              ^
+         * Insertion    |
+         * Index:       1
+         * Elements:    X Y Z
+         *
+         * Result
+         * Indices:   0 1 2 3 4 5 6 7 8
+         * Elements:  A X Y Z B C D E F
+         *
+         * 
+ * + * @param index the index of the first element affected by this change + * @param elements the list of inserted elements + * + * @since 0.8.0 + */ + record Insertion( + int index, + List elements + ) implements LocalChange { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with List::copyOf because the maps might contain null keys and values + public Insertion { + elements = Collections.unmodifiableList(new ArrayList<>(elements)); + } + + } + + /** + * Represents removal of one or more subsequent {@link #elements() elements} starting from a given + * {@link #index() index}. + * + *

Example:

+ *
+         * Initial
+         * Indices:   0 1 2 3 4 5
+         * Elements:  A B C D E F
+         *              ^
+         * Removal      |
+         * Index:       1
+         * Elements:    B C D
+         *
+         * Result
+         * Indices:   0 1 2
+         * Elements:  A E F
+         *
+         * 
+ * + * @param index the index of the first element affected by this change + * @param elements the list of removed elements + * + * @since 0.8.0 + */ + record Removal( + int index, + List elements + ) implements LocalChange { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with List::copyOf because the maps might contain null keys and values + public Removal { + elements = Collections.unmodifiableList(new ArrayList<>(elements)); + } + + } + + /** + * Represents an update of one or more subsequent {@link #newElements() elements} starting from a given + * {@link #index() index}. + * + *

Example:

+ *
+         * Initial
+         * Indices:   0 1 2 3 4 5
+         * Elements:  A B C D E F
+         *              ^
+         * Update       |
+         * Index:       1
+         * Elements:    X Y Z
+         *
+         * Result
+         * Indices:   0 1 2 3 4 5
+         * Elements:  A X Y Z E F
+         *
+         * 
+ * + * @param index the index of the first element affected by this change + * @param oldElements the list of previous elements + * @param newElements the list of updated elements + * + * @since 0.8.0 + */ + record Update( + int index, + List oldElements, + List newElements + ) implements LocalChange { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with List::copyOf because the maps might contain null keys and values + public Update { + oldElements = Collections.unmodifiableList(new ArrayList<>(oldElements)); + newElements = Collections.unmodifiableList(new ArrayList<>(newElements)); + } + + } + + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/MapChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/MapChangeListener.java new file mode 100644 index 0000000..5b74e6d --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/MapChangeListener.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * A listener that may be used to subscribe to changes to one or more {@link ObservableMap observable map}. + * + * @param the type of the map elements + * @param the type of the map elements + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public interface MapChangeListener { + + /** + * Processes changes to an {@link ObservableMap observable map} this listener is attached to. + * + * @param observable the observable map + * @param change the change to process + * + * @since 0.8.0 + */ + void onChanged(ObservableMap observable, Change change); + + /** + * {@return whether this listener is invalid} + * + *

Once an observable map discovers that a listener is invalid, it will stop notifying the listener of updates + * and release all strong references to the listener.

+ * + *

Once this method returned {@code true}, it must never return {@code false} again for the same instance. + * Breaking this contract may result in unexpected behavior.

+ * + * @since 0.8.0 + */ + default boolean isInvalid() { + return false; + } + + /** + * A change done to an {@link ObservableMap}. + * + * @param the type of the map's elements + * @param the type of the map's elements + * @param addedElements the elements added to the map + * @param removedElements the elements removed from the map + * @param updatedElements the elements in the map that were updated + * + * + * @since 0.8.0 + */ + record Change( + Map addedElements, + Map removedElements, + Map> updatedElements + ) { + + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with Map::copyOf because the maps might contain null keys and values + public Change(@Nullable Map addedElements, @Nullable Map removedElements, @Nullable Map> updatedElements) { + this.addedElements = (addedElements != null) ? Collections.unmodifiableMap(new HashMap<>(addedElements)) : Map.of(); + this.removedElements = (removedElements != null) ? Collections.unmodifiableMap(new HashMap<>(removedElements)) : Map.of(); + this.updatedElements = (updatedElements != null) ? Collections.unmodifiableMap(new HashMap<>(updatedElements)) : Map.of(); + } + + /** + * Describes an update to a map entry's value. + * + * @since 0.8.0 + */ + public record Update(@Nullable V oldValue, @Nullable V newValue) {} + + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableCollection.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableCollection.java index aebb104..679071d 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableCollection.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableCollection.java @@ -32,59 +32,13 @@ import com.osmerion.quitte.Observable; +import java.util.Collection; + /** * An observable collection. * - * @param the type of change produced by this collection - * * @since 0.1.0 * * @author Leon Linhart */ -public interface ObservableCollection extends Observable { - - /** - * Attaches the given {@link CollectionChangeListener change listener} to this map. - * - *

If the given listener is already attached to this map, this method does nothing and returns {@code false}.

- * - *

While an {@code MapChangeListener} is attached to a map, it will be {@link CollectionChangeListener#onChanged(Object)} - * notified} whenever the map is updated.

- * - *

This map stores a strong reference to the given listener until the listener is either removed explicitly by - * calling {@link #removeChangeListener(CollectionChangeListener)} or implicitly when this map discovers that the - * listener has become {@link CollectionChangeListener#isInvalid() invalid}. Generally, it is recommended to use an - * instance of {@link WeakCollectionChangeListener} when possible to avoid leaking instances.

- * - * @param listener the listener to be attached to this map - * - * @return {@code true} if the listener was not previously attached to this map and has been successfully attached, - * or {@code false} otherwise - * - * @throws NullPointerException if the given listener is {@code null} - * - * @see #removeChangeListener(CollectionChangeListener) - * - * @since 0.1.0 - */ - boolean addChangeListener(CollectionChangeListener listener); - - /** - * Detaches the given {@link CollectionChangeListener change listener} from this map. - * - *

If the given listener is not attached to this map, this method does nothing and returns {@code false}.

- * - * @param listener the listener to be detached from this map - * - * @return {@code true} if the listener was attached to and has been detached from this map, or {@code false} - * otherwise - * - * @throws NullPointerException if the given listener is {@code null} - * - * @see #addChangeListener(CollectionChangeListener) - * - * @since 0.1.0 - */ - boolean removeChangeListener(CollectionChangeListener listener); - -} \ No newline at end of file +public interface ObservableCollection extends Collection, Observable {} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableDeque.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableDeque.java index 01f65a2..1d21081 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableDeque.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableDeque.java @@ -32,8 +32,6 @@ import java.util.Deque; import java.util.Iterator; -import java.util.List; -import java.util.function.Function; import javax.annotation.Nullable; import com.osmerion.quitte.internal.collections.UnmodifiableObservableDeque; @@ -48,7 +46,7 @@ * * @author Leon Linhart */ -public interface ObservableDeque extends Deque, ObservableCollection> { +public interface ObservableDeque extends Deque, ObservableCollection { /** * Returns an observable view of the specified deque. Query operations on the returned deque "read and write @@ -88,11 +86,56 @@ static ObservableDeque unmodifiableViewOf(ObservableDeque deque) { return new UnmodifiableObservableDeque<>(deque); } + /** + * Attaches the given {@link DequeChangeListener change listener} to this deque. + * + *

If the given listener is already attached to this deque, this method does nothing and returns {@code false}. + *

+ * + *

While an {@code DequeChangeListener} is attached to a deque, it will be {@link DequeChangeListener#onChanged(ObservableDeque, DequeChangeListener.Change)} + * notified} whenever the deque is updated.

+ * + *

This deque stores a strong reference to the given listener until the listener is either removed explicitly by + * calling {@link #removeChangeListener(DequeChangeListener)} or implicitly when this list discovers that the + * listener has become {@link DequeChangeListener#isInvalid() invalid}. Generally, it is recommended to use an + * instance of {@link WeakDequeChangeListener} when possible to avoid leaking instances.

+ * + * @param listener the listener to be attached to this deque + * + * @return {@code true} if the listener was not previously attached to this deque and has been successfully + * attached, or {@code false} otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #removeChangeListener(DequeChangeListener) + * + * @since 0.8.0 + */ + boolean addChangeListener(DequeChangeListener listener); + + /** + * Detaches the given {@link DequeChangeListener change listener} from this deque. + * + *

If the given listener is not attached to this deque, this method does nothing and returns {@code false}.

+ * + * @param listener the listener to be detached from this deque + * + * @return {@code true} if the listener was attached to and has been detached from this deque, or {@code false} + * otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #addChangeListener(DequeChangeListener) + * + * @since 0.8.0 + */ + boolean removeChangeListener(DequeChangeListener listener); + /** * {@inheritDoc} * *

Modifications to an observable deque using the returned iterator should be made with caution as they - * produce {@link Site#OPAQUE opaque changes}. + * produce {@link DequeChangeListener.Site#OPAQUE opaque changes}. *

*/ @Override @@ -102,7 +145,7 @@ static ObservableDeque unmodifiableViewOf(ObservableDeque deque) { * {@inheritDoc} * *

Modifications to an observable deque using the returned iterator should be made with caution as they - * produce {@link Site#OPAQUE opaque changes}.

+ * produce {@link DequeChangeListener.Site#OPAQUE opaque changes}.

*/ @Override Iterator descendingIterator(); @@ -110,7 +153,7 @@ static ObservableDeque unmodifiableViewOf(ObservableDeque deque) { /** * {@inheritDoc} * - *

This method should be used with caution as it produces {@link Site#OPAQUE opaque changes}.

+ *

This method should be used with caution as it produces {@link DequeChangeListener.Site#OPAQUE opaque changes}.

*/ @Override boolean remove(@Nullable Object o); @@ -118,7 +161,7 @@ static ObservableDeque unmodifiableViewOf(ObservableDeque deque) { /** * {@inheritDoc} * - *

This method should be used with caution as it produces {@link Site#OPAQUE opaque changes}.

+ *

This method should be used with caution as it produces {@link DequeChangeListener.Site#OPAQUE opaque changes}.

*/ @Override boolean removeFirstOccurrence(Object o); @@ -126,173 +169,9 @@ static ObservableDeque unmodifiableViewOf(ObservableDeque deque) { /** * {@inheritDoc} * - *

This method should be used with caution as it produces {@link Site#OPAQUE opaque changes}.

+ *

This method should be used with caution as it produces {@link DequeChangeListener.Site#OPAQUE opaque changes}.

*/ @Override boolean removeLastOccurrence(Object o); - /** - * A change to a deque consists of one or more {@link LocalChange local updates} that apply to a specific - * {@link Site site} of the deque. - * - * @param the type of the deque's elements - * - * @since 0.1.0 - */ - record Change(List> localChanges) { - - public Change { - localChanges = List.copyOf(localChanges); - } - - /** - * Creates a copy of this change using the given {@code transform} to map the elements. - * - * @param the new type for the elements - * @param transform the transform function to be applied to the elements - * - * @return a copy of this change - * - * @deprecated This is an unsupported method that may be removed at any time. - * - * @since 0.1.0 - */ - @Deprecated - public Change copy(Function transform) { - return new Change<>(this.localChanges.stream().map(it -> it.copy(transform)).toList()); - } - - /** - * Returns a list of changes that are local to parts of the deque. - * - *

It is important to process local changes in order, since the order of the elements matters.

- * - * @return a list of changes that are local to parts of the deque - * - * @deprecated Deprecated in favor of canonical record accessor {@link #localChanges()}. - * - * @since 0.1.0 - */ - @Deprecated(since = "0.3.0", forRemoval = true) - public List> getLocalChanges() { - return this.localChanges; - } - - } - - /** - * A change to a deque. This might either be an {@link Insertion}, or a {@link Removal}. - * - *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working - * with {@code LocalChange} objects.

- * - * @param the type of the deque's elements - * - * @since 0.1.0 - */ - abstract class LocalChange { - - private final Site site; - private final List elements; - - // TODO This is an ideal candidate for a sealed record hierarchy. - private LocalChange(Site site, List elements) { - this.site = site; - this.elements = elements; - } - - @Deprecated - abstract LocalChange copy(Function transform); - - /** - * A list of elements related to this change. How this list should be interpreted is defined by an implementing - * class. - * - * @return a list of elements related to this change - * - * @since 0.1.0 - */ - public final List getElements() { - return this.elements; - } - - /** - * The {@link Site site} of the deque to which the change applies. - * - * @return site of the deque to which the change applies - * - * @since 0.1.0 - */ - public final Site getSite() { - return this.site; - } - - /** - * Represents insertion of one or more subsequent {@link #getElements() elements} starting from a given - * {@link #getSite() site}. - * - * @since 0.1.0 - */ - public static final class Insertion extends LocalChange { - - Insertion(Site site, List elements) { - super(site, elements); - } - - @Override - LocalChange copy(Function transform) { - return new Insertion<>(this.getSite(), this.getElements().stream().map(transform).toList()); - } - - } - - /** - * Represents removal of one or more subsequent {@link #getElements() elements} starting from a given - * {@link #getSite() site}. - * - * @since 0.1.0 - */ - public static final class Removal extends LocalChange { - - Removal(Site site, List elements) { - super(site, elements); - } - - @Override - LocalChange copy(Function transform) { - return new Removal<>(this.getSite(), this.getElements().stream().map(transform).toList()); - } - - } - - } - - /** - * A modifiable site of a deque. - * - * @since 0.1.0 - */ - enum Site { - /** - * The "head" (or "front") of the deque. - * - * @since 0.1.0 - */ - HEAD, - /** - * The "tail" (or "back") of the deque. - * - * @since 0.1.0 - */ - TAIL, - /** - * An opaque position in the deque. - * - *

Using operations which produce "opaque change" is discouraged.

- * - * @since 0.1.0 - */ - OPAQUE - } - } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableList.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableList.java index 04988e4..e5f9d23 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableList.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableList.java @@ -32,10 +32,8 @@ import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.RandomAccess; -import java.util.function.Function; import com.osmerion.quitte.internal.collections.UnmodifiableObservableList; import com.osmerion.quitte.internal.collections.UnmodifiableRandomAccessObservableList; @@ -51,7 +49,7 @@ * * @author Leon Linhart */ -public interface ObservableList extends List, ObservableCollection> { +public interface ObservableList extends List, ObservableCollection { /** * Returns an observable view of the specified list. Query operations on the returned list "read and write through" @@ -91,6 +89,51 @@ static ObservableList unmodifiableViewOf(ObservableList list) { return list instanceof RandomAccess ? new UnmodifiableRandomAccessObservableList<>(list) : new UnmodifiableObservableList<>(list); } + /** + * Attaches the given {@link ListChangeListener change listener} to this list. + * + *

If the given listener is already attached to this list, this method does nothing and returns {@code false}. + *

+ * + *

While an {@code ListChangeListener} is attached to a list, it will be {@link ListChangeListener#onChanged(ObservableList, ListChangeListener.Change)} + * notified} whenever the list is updated.

+ * + *

This list stores a strong reference to the given listener until the listener is either removed explicitly by + * calling {@link #removeChangeListener(ListChangeListener)} or implicitly when this list discovers that the + * listener has become {@link ListChangeListener#isInvalid() invalid}. Generally, it is recommended to use an + * instance of {@link WeakListChangeListener} when possible to avoid leaking instances.

+ * + * @param listener the listener to be attached to this list + * + * @return {@code true} if the listener was not previously attached to this list and has been successfully + * attached, or {@code false} otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #removeChangeListener(ListChangeListener) + * + * @since 0.8.0 + */ + boolean addChangeListener(ListChangeListener listener); + + /** + * Detaches the given {@link ListChangeListener change listener} from this list. + * + *

If the given listener is not attached to this list, this method does nothing and returns {@code false}.

+ * + * @param listener the listener to be detached from this list + * + * @return {@code true} if the listener was attached to and has been detached from this list, or {@code false} + * otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #addChangeListener(ListChangeListener) + * + * @since 0.8.0 + */ + boolean removeChangeListener(ListChangeListener listener); + /** * See {@link #addAll(Collection)}. * @@ -192,267 +235,4 @@ default boolean setAll(E... elements) { */ boolean setAll(Collection elements); - /** - * A change to a list may either be a {@link Permutation permutation}, or one or more local updates to parts of the - * list (represented as {@link LocalChange}). - * - *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working - * with {@code Change} objects.

- * - * @param the type of the list's elements - * - * @since 0.1.0 - */ - abstract class Change { - - private Change() {} // TODO This is an ideal candidate for a sealed record hierarchy. - - /** - * Creates a copy of this change using the given {@code transform} to map the elements. - * - * @param the new type for the elements - * @param transform the transform function to be applied to the elements - * - * @return a copy of this change - * - * @deprecated This is an unsupported method that may be removed at any time. - * - * @since 0.1.0 - */ - @Deprecated - public abstract Change copy(Function transform); - - /** - * A change to a list in which its elements are rearranged. - * - * @since 0.1.0 - */ - public static final class Permutation extends Change { - - private final List indices; - - Permutation(List indices) { - this.indices = indices; - } - - /** - * {@inheritDoc} - * - * @since 0.1.0 - */ - @SuppressWarnings("unchecked") - @Deprecated - @Override - public Change copy(Function transform) { - return (Permutation) this; - } - - /** - * A list of integers that represents the mapping of indices used to create the current permutation. - * - * @return a list of integers that represents the mapping of indices used to create the current permutation - * - * @since 0.1.0 - */ - public List getIndices() { - return this.indices; - } - - } - - /** - * A change to a list that consists of one or more local changes to the list. - * - * @since 0.1.0 - */ - public static final class Update extends Change { - - private final List> localChanges; - - Update(List> localChanges) { - this.localChanges = localChanges; - } - - /** - * {@inheritDoc} - * - * @since 0.1.0 - */ - @Deprecated - @Override - public Change copy(Function transform) { - return new Update<>(this.localChanges.stream().map(it -> it.copy(transform)).toList()); - } - - /** - * Returns a list of changes that are local to parts of the list. - * - *

It is important to process local changes in order, since their indices depend on another.

- * - * @return a list of changes that are local to parts of the list - * - * @since 0.1.0 - */ - public List> getLocalChanges() { - return this.localChanges; - } - - } - - } - - /** - * A change to a list. This might either be an {@link Insertion}, a {@link Removal}, or an {@link Update}. - * - *

Using {@code instanceof} checks (or similar future pattern matching mechanisms) is recommended when working - * with {@code LocalChange} objects.

- * - * @param the type of the list's elements - * - * @since 0.1.0 - */ - abstract class LocalChange { - - private final int index; - private final List elements; - - // TODO This is an ideal candidate for a sealed record hierarchy. - private LocalChange(int index, List elements) { - this.index = index; - this.elements = Collections.unmodifiableList(elements); - } - - @Deprecated - abstract LocalChange copy(Function transform); - - /** - * Returns the index of the first element affected by this change. - * - * @return the index of the first element affected by this change - * - * @since 0.1.0 - */ - public final int getIndex() { - return this.index; - } - - /** - * A list of elements related to this change. How this list should be interpreted is defined by an implementing - * class. - * - * @return a list of elements related to this change - * - * @since 0.1.0 - */ - public final List getElements() { - return this.elements; - } - - /** - * Represents insertion of one or more subsequent {@link #getElements() elements} starting from a given - * {@link #getIndex() index}. - * - *

Example:

- *
-         * Initial
-         * Indices:   0 1 2 3 4 5
-         * Elements:  A B C D E F
-         *              ^
-         * Insertion    |
-         * Index:       1
-         * Elements:    X Y Z
-         *
-         * Result
-         * Indices:   0 1 2 3 4 5 6 7 8
-         * Elements:  A X Y Z B C D E F
-         *
-         * 
- * - * @since 0.1.0 - */ - public static final class Insertion extends LocalChange { - - Insertion(int index, List elements) { - super(index, elements); - } - - @Override - LocalChange copy(Function transform) { - return new Insertion<>(this.getIndex(), this.getElements().stream().map(transform).toList()); - } - - } - - /** - * Represents removal of one or more subsequent {@link #getElements() elements} starting from a given - * {@link #getIndex() index}. - * - *

Example:

- *
-         * Initial
-         * Indices:   0 1 2 3 4 5
-         * Elements:  A B C D E F
-         *              ^
-         * Removal      |
-         * Index:       1
-         * Elements:    B C D
-         *
-         * Result
-         * Indices:   0 1 2
-         * Elements:  A E F
-         *
-         * 
- * - * @since 0.1.0 - */ - public static final class Removal extends LocalChange { - - Removal(int index, List elements) { - super(index, elements); - } - - @Override - LocalChange copy(Function transform) { - return new Removal<>(this.getIndex(), this.getElements().stream().map(transform).toList()); - } - - } - - /** - * Represents an update of one or more subsequent {@link #getElements() elements} starting from a given - * {@link #getIndex() index}. - * - *

Example:

- *
-         * Initial
-         * Indices:   0 1 2 3 4 5
-         * Elements:  A B C D E F
-         *              ^
-         * Update       |
-         * Index:       1
-         * Elements:    X Y Z
-         *
-         * Result
-         * Indices:   0 1 2 3 4 5
-         * Elements:  A X Y Z E F
-         *
-         * 
- * - * @since 0.1.0 - */ - public static final class Update extends LocalChange { - - Update(int index, List elements) { - super(index, elements); - } - - @Override - LocalChange copy(Function transform) { - return new Update<>(this.getIndex(), this.getElements().stream().map(transform).toList()); - } - - } - - } - } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableMap.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableMap.java index 7b57eb7..b4db32c 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableMap.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableMap.java @@ -30,10 +30,9 @@ */ package com.osmerion.quitte.collections; -import java.util.Collections; import java.util.Map; -import javax.annotation.Nullable; +import com.osmerion.quitte.Observable; import com.osmerion.quitte.internal.collections.UnmodifiableObservableMap; import com.osmerion.quitte.internal.collections.WrappingObservableMap; @@ -47,7 +46,7 @@ * * @author Leon Linhart */ -public interface ObservableMap extends Map, ObservableCollection> { +public interface ObservableMap extends Map, Observable { /** * Returns an observable view of the specified map. Query operations on the returned map "read and write through" @@ -90,64 +89,67 @@ static ObservableMap unmodifiableViewOf(ObservableMap map) { } /** - * A change done to an {@link ObservableMap}. + * Attaches the given {@link MapChangeListener change listener} to this map. * - * @param the type of the map's elements - * @param the type of the map's elements + *

If the given listener is already attached to this map, this method does nothing and returns {@code false}.

* - * @since 0.1.0 + *

While an {@code MapChangeListener} is attached to a map, it will be {@link MapChangeListener#onChanged(ObservableMap, MapChangeListener.Change)} + * notified} whenever the map is updated.

+ * + *

This map stores a strong reference to the given listener until the listener is either removed explicitly by + * calling {@link #removeChangeListener(MapChangeListener)} or implicitly when this map discovers that the + * listener has become {@link MapChangeListener#isInvalid() invalid}. Generally, it is recommended to use an + * instance of {@link WeakMapChangeListener} when possible to avoid leaking instances.

+ * + * @param listener the listener to be attached to this map + * + * @return {@code true} if the listener was not previously attached to this map and has been successfully attached, + * or {@code false} otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #removeChangeListener(MapChangeListener) + * + * @since 0.8.0 */ - record Change( - Map addedElements, - Map removedElements, - Map> updatedElements - ) { + boolean addChangeListener(MapChangeListener listener); - public Change(@Nullable Map addedElements, @Nullable Map removedElements, @Nullable Map> updatedElements) { - this.addedElements = (addedElements != null) ? Collections.unmodifiableMap(addedElements) : Collections.emptyMap(); - this.removedElements = (removedElements != null) ? Collections.unmodifiableMap(removedElements) : Collections.emptyMap(); - this.updatedElements = (updatedElements != null) ? Collections.unmodifiableMap(updatedElements) : Collections.emptyMap(); - } - - /** - * Describes an update to a map entry's value. - * - * @since 0.1.0 - */ - public record Update(@Nullable V oldValue, @Nullable V newValue) { - - /** - * Returns the old value. - * - * @return the old value - * - * @deprecated Deprecated in favor of canonical record accessor {@link #oldValue()}. - * - * @since 0.1.0 - */ - @Deprecated(since = "0.8.0", forRemoval = true) - @Nullable - public V getOldValue() { - return this.oldValue; - } - - /** - * Returns the new value. - * - * @return the new value - * - * @deprecated Deprecated in favor of canonical record accessor {@link #newValue()}. - * - * @since 0.1.0 - */ - @Deprecated(since = "0.8.0", forRemoval = true) - @Nullable - public V getNewValue() { - return this.newValue; - } + /** + * Detaches the given {@link MapChangeListener change listener} from this map. + * + *

If the given listener is not attached to this map, this method does nothing and returns {@code false}.

+ * + * @param listener the listener to be detached from this map + * + * @return {@code true} if the listener was attached to and has been detached from this map, or {@code false} + * otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #addChangeListener(MapChangeListener) + * + * @since 0.8.0 + */ + boolean removeChangeListener(MapChangeListener listener); - } + /** + * {@inheritDoc} + * + * @return an observable set view of the mappings contained in this map + * + * @since 0.8.0 + */ + @Override + ObservableSet> entrySet(); - } + /** + * {@inheritDoc} + * + * @return an observable set view of the keys contained in this map + * + * @since 0.8.0 + */ + @Override + ObservableSet keySet(); } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableSet.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableSet.java index 173d881..f3c0f41 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableSet.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/ObservableSet.java @@ -30,9 +30,7 @@ */ package com.osmerion.quitte.collections; -import java.util.Collections; import java.util.Set; -import javax.annotation.Nullable; import com.osmerion.quitte.internal.collections.UnmodifiableObservableSet; import com.osmerion.quitte.internal.collections.WrappingObservableSet; @@ -46,7 +44,7 @@ * * @author Leon Linhart */ -public interface ObservableSet extends Set, ObservableCollection> { +public interface ObservableSet extends Set, ObservableCollection { /** * Returns an observable view of the specified set. Query operations on the returned set "read and write through" @@ -87,22 +85,47 @@ static ObservableSet unmodifiableViewOf(ObservableSet set) { } /** - * A change done to an {@link ObservableSet}. + * Attaches the given {@link SetChangeListener change listener} to this set. * - *

Note that adding an element that is already in a set does not modify the set and therefore no change will be - * generated.

+ *

If the given listener is already attached to this set, this method does nothing and returns {@code false}.

* - * @param the type of the set's elements + *

While an {@code SetChangeListener} is attached to a set, it will be {@link SetChangeListener#onChanged(ObservableSet, SetChangeListener.Change)} + * notified} whenever the set is updated.

* - * @since 0.1.0 + *

This set stores a strong reference to the given listener until the listener is either removed explicitly by + * calling {@link #removeChangeListener(SetChangeListener)} or implicitly when this set discovers that the listener + * has become {@link SetChangeListener#isInvalid() invalid}. Generally, it is recommended to use an instance of + * {@link WeakSetChangeListener} when possible to avoid leaking instances.

+ * + * @param listener the listener to be attached to this set + * + * @return {@code true} if the listener was not previously attached to this set and has been successfully attached, + * or {@code false} otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #removeChangeListener(SetChangeListener) + * + * @since 0.8.0 */ - record Change(Set addedElements, Set removedElements) { + boolean addChangeListener(SetChangeListener listener); - public Change(@Nullable Set addedElements, @Nullable Set removedElements) { - this.addedElements = addedElements != null ? Collections.unmodifiableSet(addedElements) : Collections.emptySet(); - this.removedElements = removedElements != null ? Collections.unmodifiableSet(removedElements) : Collections.emptySet(); - } - - } + /** + * Detaches the given {@link SetChangeListener change listener} from this set. + * + *

If the given listener is not attached to this set, this method does nothing and returns {@code false}.

+ * + * @param listener the listener to be detached from this set + * + * @return {@code true} if the listener was attached to and has been detached from this set, or {@code false} + * otherwise + * + * @throws NullPointerException if the given listener is {@code null} + * + * @see #addChangeListener(SetChangeListener) + * + * @since 0.8.0 + */ + boolean removeChangeListener(SetChangeListener listener); } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/CollectionChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/SetChangeListener.java similarity index 54% rename from modules/quitte/src/main/java/com/osmerion/quitte/collections/CollectionChangeListener.java rename to modules/quitte/src/main/java/com/osmerion/quitte/collections/SetChangeListener.java index e97a291..73bdf03 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/CollectionChangeListener.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/SetChangeListener.java @@ -30,28 +30,32 @@ */ package com.osmerion.quitte.collections; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + /** - * A listener that may be used to subscribe to changes to one or more observable collections. - * - * @param the type of change this listener can process + * A listener that may be used to subscribe to changes to one or more {@link ObservableSet observable set}. * - * @since 0.1.0 + * @param the type of the set elements * - * @author Leon Linhart + * @since 0.8.0 */ -public interface CollectionChangeListener { +public interface SetChangeListener { /** - * Processes changes to an observable collection this listener is attached to. + * Processes changes to an {@link ObservableSet observable set} this listener is attached to. * - * @param change the change to process + * @param observable the observable set + * @param change the change to process * - * @since 0.1.0 + * @since 0.8.0 */ - void onChanged(C change); + void onChanged(ObservableSet observable, Change change); /** - * Returns whether this listener is invalid. + * {@return whether this listener is invalid} * *

Once an observable collection discovers that a listener is invalid, it will stop notifying the listener of * updates and release all strong references to the listener.

@@ -59,12 +63,40 @@ public interface CollectionChangeListener { *

Once this method returned {@code true}, it must never return {@code false} again for the same instance. * Breaking this contract may result in unexpected behavior.

* - * @return whether this listener is invalid - * * @since 0.1.0 */ default boolean isInvalid() { return false; } + /** + * A change done to an {@link ObservableSet}. + * + *

Note that adding an element that is already in a set does not modify the set and therefore no change will be + * generated.

+ * + * @param the type of the set's elements + * @param addedElements the added elements + * @param removedElements the removed elements + * + * @since 0.8.0 + */ + record Change(Set addedElements, Set removedElements) { + + /** + * Creates a new {@code Change}. + * + * @param addedElements the added elements + * @param removedElements the removed elements + * + * @since 0.8.0 + */ + @SuppressWarnings("Java9CollectionFactory") // Cannot replace the code with Set::copyOf because the maps might contain null keys and values + public Change(@Nullable Set addedElements, @Nullable Set removedElements) { + this.addedElements = addedElements != null ? Collections.unmodifiableSet(new HashSet<>(addedElements)) : Set.of(); + this.removedElements = removedElements != null ? Collections.unmodifiableSet(new HashSet<>(removedElements)) : Set.of(); + } + + } + } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakDequeChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakDequeChangeListener.java new file mode 100644 index 0000000..297c102 --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakDequeChangeListener.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +/** + * A {@code WeakDequeChangeListener} may be used to wrap a listener that should only be referenced weakly from an + * {@link ObservableDeque}. + * + *

This listener does not keep a strong reference to the wrapped listener.

+ * + * @param the type of the deque elements + * + * @see WeakReference + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public final class WeakDequeChangeListener implements DequeChangeListener { + + private final WeakReference> ref; + + private boolean wasGarbageCollected; + + /** + * Wraps the given {@link DequeChangeListener listener}. + * + * @param listener the listener to wrap + * + * @throws NullPointerException if the given listener is {@code null} + * + * @since 0.8.0 + */ + public WeakDequeChangeListener(DequeChangeListener listener) { + this.ref = new WeakReference<>(Objects.requireNonNull(listener)); + this.wasGarbageCollected = false; + } + + /** + * {@inheritDoc} + * + * @since 0.8.0 + */ + @Override + public void onChanged(ObservableDeque observable, DequeChangeListener.Change change) { + var listener = this.ref.get(); + + if (listener != null) { + listener.onChanged(observable, change); + } else { + this.wasGarbageCollected = true; + } + } + + /** + * {@return whether the underlying listener was garbage collected or has become invalid} + * + * @since 0.8.0 + */ + @Override + public boolean isInvalid() { + if (this.wasGarbageCollected) return true; + + var listener = this.ref.get(); + return (listener != null && listener.isInvalid()); + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakListChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakListChangeListener.java new file mode 100644 index 0000000..e12424b --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakListChangeListener.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +/** + * A {@code WeakListChangeListener} may be used to wrap a listener that should only be referenced weakly from an + * {@link ObservableList}. + * + *

This listener does not keep a strong reference to the wrapped listener.

+ * + * @param the type of the list elements + * + * @see WeakReference + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public final class WeakListChangeListener implements ListChangeListener { + + private final WeakReference> ref; + + private boolean wasGarbageCollected; + + /** + * Wraps the given {@link ListChangeListener listener}. + * + * @param listener the listener to wrap + * + * @throws NullPointerException if the given listener is {@code null} + * + * @since 0.8.0 + */ + public WeakListChangeListener(ListChangeListener listener) { + this.ref = new WeakReference<>(Objects.requireNonNull(listener)); + this.wasGarbageCollected = false; + } + + /** + * {@inheritDoc} + * + * @since 0.8.0 + */ + @Override + public void onChanged(ObservableList observable, ListChangeListener.Change change) { + var listener = this.ref.get(); + + if (listener != null) { + listener.onChanged(observable, change); + } else { + this.wasGarbageCollected = true; + } + } + + /** + * {@return whether the underlying listener was garbage collected or has become invalid} + * + * @since 0.8.0 + */ + @Override + public boolean isInvalid() { + if (this.wasGarbageCollected) return true; + + var listener = this.ref.get(); + return (listener != null && listener.isInvalid()); + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakMapChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakMapChangeListener.java new file mode 100644 index 0000000..78a8ca7 --- /dev/null +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakMapChangeListener.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2018-2023 Leon Linhart, + * 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. 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 com.osmerion.quitte.collections; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +/** + * A {@code WeakMapChangeListener} may be used to wrap a listener that should only be referenced weakly from an + * {@link ObservableMap}. + * + *

This listener does not keep a strong reference to the wrapped listener.

+ * + * @param the type of the map keys + * @param the type of the map values + * + * @see WeakReference + * + * @since 0.8.0 + * + * @author Leon Linhart + */ +public final class WeakMapChangeListener implements MapChangeListener { + + private final WeakReference> ref; + + private boolean wasGarbageCollected; + + /** + * Wraps the given {@link MapChangeListener listener}. + * + * @param listener the listener to wrap + * + * @throws NullPointerException if the given listener is {@code null} + * + * @since 0.8.0 + */ + public WeakMapChangeListener(MapChangeListener listener) { + this.ref = new WeakReference<>(Objects.requireNonNull(listener)); + this.wasGarbageCollected = false; + } + + /** + * {@inheritDoc} + * + * @since 0.8.0 + */ + @Override + public void onChanged(ObservableMap observable, MapChangeListener.Change change) { + var listener = this.ref.get(); + + if (listener != null) { + listener.onChanged(observable, change); + } else { + this.wasGarbageCollected = true; + } + } + + /** + * {@return whether the underlying listener was garbage collected or has become invalid} + * + * @since 0.8.0 + */ + @Override + public boolean isInvalid() { + if (this.wasGarbageCollected) return true; + + var listener = this.ref.get(); + return (listener != null && listener.isInvalid()); + } + +} \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakCollectionChangeListener.java b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakSetChangeListener.java similarity index 74% rename from modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakCollectionChangeListener.java rename to modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakSetChangeListener.java index 47d2bac..269ad3b 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakCollectionChangeListener.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/collections/WeakSetChangeListener.java @@ -34,45 +34,50 @@ import java.util.Objects; /** - * A {@code WeakCollectionChangeListener} may be used to wrap a listener that should only be referenced weakly from an - * {@link ObservableCollection}. + * A {@code WeakSetChangeListener} may be used to wrap a listener that should only be referenced weakly from an + * {@link ObservableSet}. + * + *

This listener does not keep a strong reference to the wrapped listener.

+ * + * @param the type of the set elements * * @see WeakReference * - * @since 0.1.0 + * @since 0.8.0 * * @author Leon Linhart */ -public final class WeakCollectionChangeListener implements CollectionChangeListener { +public final class WeakSetChangeListener implements SetChangeListener { - private final WeakReference> ref; + private final WeakReference> ref; private boolean wasGarbageCollected; /** - * Wraps the given {@link CollectionChangeListener listener}. + * Wraps the given {@link SetChangeListener listener}. * * @param listener the listener to wrap * * @throws NullPointerException if the given listener is {@code null} * - * @since 0.1.0 + * @since 0.8.0 */ - public WeakCollectionChangeListener(CollectionChangeListener listener) { + public WeakSetChangeListener(SetChangeListener listener) { this.ref = new WeakReference<>(Objects.requireNonNull(listener)); + this.wasGarbageCollected = false; } /** * {@inheritDoc} * - * @since 0.1.0 + * @since 0.8.0 */ @Override - public void onChanged(C change) { - CollectionChangeListener listener = this.ref.get(); + public void onChanged(ObservableSet observable, SetChangeListener.Change change) { + var listener = this.ref.get(); if (listener != null) { - listener.onChanged(change); + listener.onChanged(observable, change); } else { this.wasGarbageCollected = true; } @@ -81,7 +86,7 @@ public void onChanged(C change) { /** * {@return whether the underlying listener was garbage collected or has become invalid} * - * @since 0.1.0 + * @since 0.8.0 */ @Override public boolean isInvalid() { diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/DequeBinding.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/DequeBinding.java index 74fb8ee..b18f498 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/DequeBinding.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/DequeBinding.java @@ -30,18 +30,15 @@ */ package com.osmerion.quitte.internal.binding; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Deque; -import java.util.Iterator; -import java.util.List; +import java.util.*; import java.util.function.Function; +import java.util.stream.Collectors; import com.osmerion.quitte.InvalidationListener; import com.osmerion.quitte.WeakInvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.DequeChangeListener; import com.osmerion.quitte.collections.ObservableDeque; -import com.osmerion.quitte.collections.WeakCollectionChangeListener; +import com.osmerion.quitte.collections.WeakDequeChangeListener; /** * A specialized {@link Deque} binding. @@ -50,12 +47,12 @@ */ public final class DequeBinding implements Binding { - private final Deque> changes = new ArrayDeque<>(); + private final Deque> changes = new ArrayDeque<>(); private final ObservableDeque source; private final InvalidationListener invalidationListener; - private final CollectionChangeListener> changeListener; + private final DequeChangeListener changeListener; private final Function transform; @@ -64,18 +61,39 @@ public DequeBinding(Runnable invalidator, ObservableDeque source, Function invalidator.run())); - this.source.addChangeListener(new WeakCollectionChangeListener<>(this.changeListener = this.changes::addLast)); + this.source.addChangeListener(new WeakDequeChangeListener<>(this.changeListener = ((observable, change) -> this.changes.addLast(change)))); } - @SuppressWarnings("deprecation") - public List> getChanges() { - List> changes = new ArrayList<>(this.changes.size()); - Iterator> changeItr = this.changes.iterator(); + public List> getChanges() { + List> changes = new ArrayList<>(this.changes.size()); + Iterator> changeItr = this.changes.iterator(); while (changeItr.hasNext()) { - ObservableDeque.Change change = changeItr.next(); - changes.add(change.copy(this.transform)); + DequeChangeListener.Change change = changeItr.next(); changeItr.remove(); + + DequeChangeListener.Change transformedChange = new DequeChangeListener.Change<>( + change.localChanges().stream() + .map(it -> { + if (it instanceof DequeChangeListener.LocalChange.Insertion localInsertion) { + //noinspection SimplifyStreamApiCallChains + return new DequeChangeListener.LocalChange.Insertion<>( + localInsertion.site(), + localInsertion.elements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()) + ); + } else if (it instanceof DequeChangeListener.LocalChange.Removal localRemoval) { + //noinspection SimplifyStreamApiCallChains + return new DequeChangeListener.LocalChange.Removal<>( + localRemoval.site(), + localRemoval.elements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()) + ); + } else { + throw new IllegalStateException(); + } + } + ).toList()); + + changes.add(transformedChange); } return changes; diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/ListBinding.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/ListBinding.java index 883e925..6e6c3bb 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/ListBinding.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/ListBinding.java @@ -36,12 +36,13 @@ import java.util.Iterator; import java.util.List; import java.util.function.Function; +import java.util.stream.Collectors; import com.osmerion.quitte.InvalidationListener; import com.osmerion.quitte.WeakInvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.ListChangeListener; import com.osmerion.quitte.collections.ObservableList; -import com.osmerion.quitte.collections.WeakCollectionChangeListener; +import com.osmerion.quitte.collections.WeakListChangeListener; /** * A specialized {@link List} binding. @@ -50,12 +51,12 @@ */ public final class ListBinding implements Binding { - private final Deque> changes = new ArrayDeque<>(); + private final Deque> changes = new ArrayDeque<>(); private final ObservableList source; private final InvalidationListener invalidationListener; - private final CollectionChangeListener> changeListener; + private final ListChangeListener changeListener; private final Function transform; @@ -64,18 +65,55 @@ public ListBinding(Runnable invalidator, ObservableList source, Function invalidator.run())); - this.source.addChangeListener(new WeakCollectionChangeListener<>(this.changeListener = this.changes::addLast)); + this.source.addChangeListener(new WeakListChangeListener<>(this.changeListener = (observable, change) -> this.changes.addLast(change))); } - @SuppressWarnings("deprecation") - public List> getChanges() { - List> changes = new ArrayList<>(this.changes.size()); - Iterator> changeItr = this.changes.iterator(); + public List> getChanges() { + List> changes = new ArrayList<>(this.changes.size()); + Iterator> changeItr = this.changes.iterator(); while (changeItr.hasNext()) { - ObservableList.Change change = changeItr.next(); - changes.add(change.copy(this.transform)); + ListChangeListener.Change change = changeItr.next(); changeItr.remove(); + + ListChangeListener.Change transformedChange; + + if (change instanceof ListChangeListener.Change.Permutation permutation) { + transformedChange = new ListChangeListener.Change.Permutation<>(permutation.indices()); + } else if (change instanceof ListChangeListener.Change.Update update) { + List> transformedLocalChanges = update.localChanges().stream() + .map(it -> { + if (it instanceof ListChangeListener.LocalChange.Insertion localInsertion) { + //noinspection SimplifyStreamApiCallChains + return new ListChangeListener.LocalChange.Insertion<>( + localInsertion.index(), + localInsertion.elements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()) + ); + } else if (it instanceof ListChangeListener.LocalChange.Removal localRemoval) { + //noinspection SimplifyStreamApiCallChains + return new ListChangeListener.LocalChange.Removal<>( + localRemoval.index(), + localRemoval.elements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()) + ); + } else if (it instanceof ListChangeListener.LocalChange.Update localUpdate) { + //noinspection SimplifyStreamApiCallChains + return new ListChangeListener.LocalChange.Update<>( + localUpdate.index(), + localUpdate.oldElements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()), + localUpdate.newElements().stream().map(this.transform).collect(Collectors.toUnmodifiableList()) + ); + } else { + throw new IllegalStateException(); + } + }) + .toList(); + + transformedChange = new ListChangeListener.Change.Update<>(transformedLocalChanges); + } else { + throw new IllegalStateException(); + } + + changes.add(transformedChange); } return changes; diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/MapBinding.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/MapBinding.java index f00c2ac..48dc434 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/MapBinding.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/MapBinding.java @@ -41,9 +41,9 @@ import com.osmerion.quitte.InvalidationListener; import com.osmerion.quitte.WeakInvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.MapChangeListener; import com.osmerion.quitte.collections.ObservableMap; -import com.osmerion.quitte.collections.WeakCollectionChangeListener; +import com.osmerion.quitte.collections.WeakMapChangeListener; /** * A specialized {@link Map} binding. @@ -52,12 +52,12 @@ */ public final class MapBinding implements Binding { - private final Deque> changes = new ArrayDeque<>(); + private final Deque> changes = new ArrayDeque<>(); private final ObservableMap source; private final InvalidationListener invalidationListener; - private final CollectionChangeListener> changeListener; + private final MapChangeListener changeListener; private final BiFunction> transform; @@ -66,25 +66,25 @@ public MapBinding(Runnable invalidator, ObservableMap source, BiFunction invalidator.run())); - this.source.addChangeListener(new WeakCollectionChangeListener<>(this.changeListener = this.changes::addLast)); + this.source.addChangeListener(new WeakMapChangeListener<>(this.changeListener = (observable, change) -> this.changes.addLast(change))); } - public List> getChanges() { - List> changes = new ArrayList<>(this.changes.size()); - Iterator> changeItr = this.changes.iterator(); + public List> getChanges() { + List> changes = new ArrayList<>(this.changes.size()); + Iterator> changeItr = this.changes.iterator(); while (changeItr.hasNext()) { - ObservableMap.Change change = changeItr.next(); + MapChangeListener.Change change = changeItr.next(); changeItr.remove(); - ObservableMap.Change transformedChange = new ObservableMap.Change<>( + MapChangeListener.Change transformedChange = new MapChangeListener.Change<>( change.addedElements().entrySet().stream().map(e -> this.transform.apply(e.getKey(), e.getValue())).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)), change.removedElements().entrySet().stream().map(e -> this.transform.apply(e.getKey(), e.getValue())).collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)), change.updatedElements().entrySet().stream().map(e -> { Map.Entry oldValue = this.transform.apply(e.getKey(), e.getValue().oldValue()); Map.Entry newValue = this.transform.apply(e.getKey(), e.getValue().newValue()); - return Map.entry(newValue.getKey(), new ObservableMap.Change.Update<>(oldValue.getValue(), newValue.getValue())); + return Map.entry(newValue.getKey(), new MapChangeListener.Change.Update<>(oldValue.getValue(), newValue.getValue())); }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) ); diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/SetBinding.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/SetBinding.java index fc54116..9947cc2 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/SetBinding.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/binding/SetBinding.java @@ -41,9 +41,9 @@ import com.osmerion.quitte.InvalidationListener; import com.osmerion.quitte.WeakInvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; import com.osmerion.quitte.collections.ObservableSet; -import com.osmerion.quitte.collections.WeakCollectionChangeListener; +import com.osmerion.quitte.collections.SetChangeListener; +import com.osmerion.quitte.collections.WeakSetChangeListener; /** * A specialized {@link Set} binding. @@ -52,12 +52,12 @@ */ public final class SetBinding implements Binding { - private final Deque> changes = new ArrayDeque<>(); + private final Deque> changes = new ArrayDeque<>(); private final ObservableSet source; private final InvalidationListener invalidationListener; - private final CollectionChangeListener> changeListener; + private final SetChangeListener changeListener; private final Function transform; @@ -66,18 +66,18 @@ public SetBinding(Runnable invalidator, ObservableSet source, Function this.transform = transform; this.source.addInvalidationListener(new WeakInvalidationListener(this.invalidationListener = (observable) -> invalidator.run())); - this.source.addChangeListener(new WeakCollectionChangeListener<>(this.changeListener = this.changes::addLast)); + this.source.addChangeListener(new WeakSetChangeListener<>(this.changeListener = ((observable, change) -> this.changes.addLast(change)))); } - public List> getChanges() { - List> changes = new ArrayList<>(this.changes.size()); - Iterator> changeItr = this.changes.iterator(); + public List> getChanges() { + List> changes = new ArrayList<>(this.changes.size()); + Iterator> changeItr = this.changes.iterator(); while (changeItr.hasNext()) { - ObservableSet.Change change = changeItr.next(); + SetChangeListener.Change change = changeItr.next(); changeItr.remove(); - ObservableSet.Change transformedChange = new ObservableSet.Change<>( + SetChangeListener.Change transformedChange = new SetChangeListener.Change<>( change.addedElements().stream().map(this.transform).collect(Collectors.toUnmodifiableSet()), change.removedElements().stream().map(this.transform).collect(Collectors.toUnmodifiableSet()) ); diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableDeque.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableDeque.java index 03b24ed..14ba51e 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableDeque.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableDeque.java @@ -36,7 +36,7 @@ import javax.annotation.Nullable; import com.osmerion.quitte.InvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.DequeChangeListener; import com.osmerion.quitte.collections.ObservableDeque; /** @@ -63,8 +63,8 @@ public UnmodifiableObservableDeque(ObservableDeque impl) { @Override public boolean addInvalidationListener(InvalidationListener listener) { return this.impl.addInvalidationListener(listener); } @Override public boolean removeInvalidationListener(InvalidationListener listener) { return this.impl.removeInvalidationListener(listener); } - @Override public boolean addChangeListener(CollectionChangeListener> listener) { return this.impl.addChangeListener(listener); } - @Override public boolean removeChangeListener(CollectionChangeListener> listener) { return this.impl.removeChangeListener(listener); } + @Override public boolean addChangeListener(DequeChangeListener listener) { return this.impl.addChangeListener(listener); } + @Override public boolean removeChangeListener(DequeChangeListener listener) { return this.impl.removeChangeListener(listener); } @Override public boolean contains(Object o) { return this.impl.contains(o); } @Override public boolean containsAll(Collection c) { return this.impl.containsAll(c); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableList.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableList.java index a7b3f73..ff6a95e 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableList.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableList.java @@ -34,7 +34,7 @@ import java.util.Collection; import com.osmerion.quitte.InvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.ListChangeListener; import com.osmerion.quitte.collections.ObservableList; /** @@ -61,8 +61,8 @@ public UnmodifiableObservableList(ObservableList impl) { @Override public boolean addInvalidationListener(InvalidationListener listener) { return this.impl.addInvalidationListener(listener); } @Override public boolean removeInvalidationListener(InvalidationListener listener) { return this.impl.removeInvalidationListener(listener); } - @Override public boolean addChangeListener(CollectionChangeListener> listener) { return this.impl.addChangeListener(listener); } - @Override public boolean removeChangeListener(CollectionChangeListener> listener) { return this.impl.removeChangeListener(listener); } + @Override public boolean addChangeListener(ListChangeListener listener) { return this.impl.addChangeListener(listener); } + @Override public boolean removeChangeListener(ListChangeListener listener) { return this.impl.removeChangeListener(listener); } @Override public boolean contains(Object o) { return this.impl.contains(o); } @Override public boolean containsAll(Collection c) { return this.impl.containsAll(c); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableMap.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableMap.java index 753d11e..e5d2d19 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableMap.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableMap.java @@ -34,12 +34,12 @@ import java.util.Collections; import java.util.Map; import java.util.Objects; -import java.util.Set; import javax.annotation.Nullable; import com.osmerion.quitte.InvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; +import com.osmerion.quitte.collections.MapChangeListener; import com.osmerion.quitte.collections.ObservableMap; +import com.osmerion.quitte.collections.ObservableSet; /** * A wrapper for an {@link ObservableMap} that blocks mutation. @@ -66,18 +66,18 @@ public UnmodifiableObservableMap(ObservableMap impl) { @Override public boolean addInvalidationListener(InvalidationListener listener) { return this.impl.addInvalidationListener(listener); } @Override public boolean removeInvalidationListener(InvalidationListener listener) { return this.impl.removeInvalidationListener(listener); } - @Override public boolean addChangeListener(CollectionChangeListener> listener) { return this.impl.addChangeListener(listener); } - @Override public boolean removeChangeListener(CollectionChangeListener> listener) { return this.impl.removeChangeListener(listener); } + @Override public boolean addChangeListener(MapChangeListener listener) { return this.impl.addChangeListener(listener); } + @Override public boolean removeChangeListener(MapChangeListener listener) { return this.impl.removeChangeListener(listener); } @Override public boolean containsKey(Object key) { return this.impl.containsKey(key); } @Override public boolean containsValue(Object value) { return this.impl.containsValue(value); } @Nullable - private transient Set> entrySet; + private transient ObservableSet> entrySet; @Override - public Set> entrySet() { - if (this.entrySet == null) this.entrySet = Collections.unmodifiableSet(this.impl.entrySet()); + public ObservableSet> entrySet() { + if (this.entrySet == null) this.entrySet = ObservableSet.unmodifiableViewOf(this.impl.entrySet()); return this.entrySet; } @@ -86,8 +86,8 @@ public Set> entrySet() { @Override public boolean isEmpty() { return this.impl.isEmpty(); } @Override - public Set keySet() { - return Collections.unmodifiableSet(this.impl.keySet()); + public ObservableSet keySet() { + return ObservableSet.unmodifiableViewOf(this.impl.keySet()); } @Override public int size() { return this.impl.size(); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableSet.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableSet.java index 637f423..96cb815 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableSet.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/UnmodifiableObservableSet.java @@ -35,8 +35,8 @@ import java.util.Objects; import com.osmerion.quitte.InvalidationListener; -import com.osmerion.quitte.collections.CollectionChangeListener; import com.osmerion.quitte.collections.ObservableSet; +import com.osmerion.quitte.collections.SetChangeListener; /** * A wrapper for an {@link ObservableSet} that blocks mutation. @@ -62,8 +62,8 @@ public UnmodifiableObservableSet(ObservableSet impl) { @Override public boolean addInvalidationListener(InvalidationListener listener) { return this.impl.addInvalidationListener(listener); } @Override public boolean removeInvalidationListener(InvalidationListener listener) { return this.impl.removeInvalidationListener(listener); } - @Override public boolean addChangeListener(CollectionChangeListener> listener) { return this.impl.addChangeListener(listener); } - @Override public boolean removeChangeListener(CollectionChangeListener> listener) { return this.impl.removeChangeListener(listener); } + @Override public boolean addChangeListener(SetChangeListener listener) { return this.impl.addChangeListener(listener); } + @Override public boolean removeChangeListener(SetChangeListener listener) { return this.impl.removeChangeListener(listener); } @Override public boolean contains(Object o) { return this.impl.contains(o); } @Override public boolean containsAll(Collection c) { return this.impl.containsAll(c); } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableDeque.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableDeque.java index 1e572bc..03dbf19 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableDeque.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableDeque.java @@ -35,6 +35,7 @@ import javax.annotation.Nullable; import com.osmerion.quitte.collections.AbstractObservableDeque; +import com.osmerion.quitte.collections.DequeChangeListener; import com.osmerion.quitte.collections.ObservableDeque; /** @@ -91,7 +92,7 @@ public E next() { public void remove() { try (ChangeBuilder changeBuilder = WrappingObservableDeque.this.beginChange()) { this.impl.remove(); - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, this.cursor); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, this.cursor); } } @@ -122,7 +123,7 @@ public E next() { public void remove() { try (ChangeBuilder changeBuilder = WrappingObservableDeque.this.beginChange()) { this.impl.remove(); - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, this.cursor); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, this.cursor); } } diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableMap.java b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableMap.java index 90b2ccf..de4704d 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableMap.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/internal/collections/WrappingObservableMap.java @@ -56,19 +56,11 @@ public final class WrappingObservableMap extends AbstractObservableMap impl; - @Nullable - private transient Set> entrySet; - public WrappingObservableMap(Map impl) { this.impl = Objects.requireNonNull(impl); } + @Override protected Set> entrySetImpl() { return this.impl.entrySet(); } @Override public V putImpl(@Nullable K key, @Nullable V value) { return this.impl.put(key, value); } - @Override - public Set> entrySet() { - if (this.entrySet == null) this.entrySet = new WrappingObservableEntrySet(this.impl.entrySet()); - return this.entrySet; - } - } \ No newline at end of file diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/property/DequeProperty.java b/modules/quitte/src/main/java/com/osmerion/quitte/property/DequeProperty.java index 11e0c75..32c9671 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/property/DequeProperty.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/property/DequeProperty.java @@ -38,6 +38,7 @@ import javax.annotation.Nullable; import com.osmerion.quitte.collections.AbstractObservableDeque; +import com.osmerion.quitte.collections.DequeChangeListener; import com.osmerion.quitte.collections.ObservableDeque; import com.osmerion.quitte.internal.binding.DequeBinding; @@ -272,7 +273,7 @@ public void remove() { try (ChangeBuilder changeBuilder = DequeProperty.this.beginChange()) { this.impl.remove(); - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, this.cursor); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, this.cursor); } } @@ -328,7 +329,7 @@ public void remove() { try (ChangeBuilder changeBuilder = DequeProperty.this.beginChange()) { this.impl.remove(); - changeBuilder.logRemove(ObservableDeque.Site.OPAQUE, this.cursor); + changeBuilder.logRemove(DequeChangeListener.Site.OPAQUE, this.cursor); } } @@ -378,7 +379,7 @@ public final int size() { void onBindingInvalidated() { assert (this.binding != null); - List> changes = this.binding.getChanges(); + List> changes = this.binding.getChanges(); try { this.inBoundUpdate = true; @@ -386,18 +387,18 @@ void onBindingInvalidated() { try (ChangeBuilder ignored = this.beginChange()) { for (var change : changes) { for (var localChange : change.localChanges()) { - if (localChange instanceof LocalChange.Insertion insertion) { - switch (insertion.getSite()) { - case HEAD -> insertion.getElements().forEach(this::addFirst); - case TAIL -> insertion.getElements().forEach(this::addLast); + if (localChange instanceof DequeChangeListener.LocalChange.Insertion insertion) { + switch (insertion.site()) { + case HEAD -> insertion.elements().forEach(this::addFirst); + case TAIL -> insertion.elements().forEach(this::addLast); default -> throw new IllegalStateException(); } - } else if (localChange instanceof LocalChange.Removal removal) { - switch (removal.getSite()) { - case HEAD -> removal.getElements().forEach(it -> this.removeFirst()); - case TAIL -> removal.getElements().forEach(it -> this.removeLast()); + } else if (localChange instanceof DequeChangeListener.LocalChange.Removal removal) { + switch (removal.site()) { + case HEAD -> removal.elements().forEach(it -> this.removeFirst()); + case TAIL -> removal.elements().forEach(it -> this.removeLast()); // TODO approximation might produce incorrect results, maybe we should disallow operations that produce opaque changes for bound observable deques? - case OPAQUE -> removal.getElements().forEach(this::remove); + case OPAQUE -> removal.elements().forEach(this::remove); default -> throw new IllegalStateException(); } } else { diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/property/ListProperty.java b/modules/quitte/src/main/java/com/osmerion/quitte/property/ListProperty.java index af42fd5..e6c8877 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/property/ListProperty.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/property/ListProperty.java @@ -38,6 +38,7 @@ import javax.annotation.Nullable; import com.osmerion.quitte.collections.AbstractObservableList; +import com.osmerion.quitte.collections.ListChangeListener; import com.osmerion.quitte.collections.ObservableList; import com.osmerion.quitte.internal.binding.ListBinding; @@ -218,15 +219,15 @@ public final int size() { void onBindingInvalidated() { assert (this.binding != null); - List> changes = this.binding.getChanges(); + List> changes = this.binding.getChanges(); try { this.inBoundUpdate = true; try (ChangeBuilder ignored = this.beginChange()) { for (var change : changes) { - if (change instanceof Change.Permutation perm) { - List indices = perm.getIndices(); + if (change instanceof ListChangeListener.Change.Permutation perm) { + List indices = perm.indices(); if (this.size() != indices.size()) throw new IndexOutOfBoundsException(); @@ -235,31 +236,31 @@ void onBindingInvalidated() { for (int i = 0; i < this.size(); i++) { this.set(indices.get(i), copy.get(i)); } - } else if (change instanceof Change.Update update) { - for (var localChange : update.getLocalChanges()) { - if (localChange instanceof LocalChange.Insertion insertion) { - if (this.size() < insertion.getIndex()) throw new IndexOutOfBoundsException(); + } else if (change instanceof ListChangeListener.Change.Update update) { + for (var localChange : update.localChanges()) { + if (localChange instanceof ListChangeListener.LocalChange.Insertion insertion) { + if (this.size() < insertion.index()) throw new IndexOutOfBoundsException(); - List elements = insertion.getElements(); - int offset = insertion.getIndex(); + List elements = insertion.elements(); + int offset = insertion.index(); for (int i = 0; i < elements.size(); i++) { this.addAll(offset + i, elements); } - } else if (localChange instanceof LocalChange.Removal removal) { - if (this.size() < removal.getIndex() + removal.getElements().size()) throw new IndexOutOfBoundsException(); + } else if (localChange instanceof ListChangeListener.LocalChange.Removal removal) { + if (this.size() < removal.index() + removal.elements().size()) throw new IndexOutOfBoundsException(); - List elements = removal.getElements(); - int offset = removal.getIndex(); + List elements = removal.elements(); + int offset = removal.index(); for (int i = 0; i < elements.size(); i++) { this.remove(offset); } - } else if (localChange instanceof LocalChange.Update localUpdate) { - if (this.size() < localUpdate.getIndex() + localUpdate.getElements().size()) throw new IndexOutOfBoundsException(); + } else if (localChange instanceof ListChangeListener.LocalChange.Update localUpdate) { + if (this.size() < localUpdate.index() + localUpdate.newElements().size()) throw new IndexOutOfBoundsException(); - List elements = localUpdate.getElements(); - int offset = localUpdate.getIndex(); + List elements = localUpdate.newElements(); + int offset = localUpdate.index(); for (int i = 0; i < elements.size(); i++) { this.set(offset + i, elements.get(i)); diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/property/MapProperty.java b/modules/quitte/src/main/java/com/osmerion/quitte/property/MapProperty.java index f08ceb9..cb72948 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/property/MapProperty.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/property/MapProperty.java @@ -30,18 +30,11 @@ */ package com.osmerion.quitte.property; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.BiFunction; import javax.annotation.Nullable; -import com.osmerion.quitte.collections.AbstractObservableMap; -import com.osmerion.quitte.collections.ObservableMap; +import com.osmerion.quitte.collections.*; import com.osmerion.quitte.internal.binding.MapBinding; /** @@ -62,9 +55,6 @@ public class MapProperty extends AbstractObservableMap implements Wr private transient MapBinding binding; private transient boolean inBoundUpdate; - @Nullable - private transient Set> entrySet; - /** * Creates a new {@code MapProperty}. * @@ -162,8 +152,11 @@ protected V putImpl(@Nullable K key, @Nullable V value) { return this.impl.put(key, value); } + @Nullable + private transient Set> entrySet; + @Override - public Set> entrySet() { + protected Set> entrySetImpl() { if (this.entrySet == null) this.entrySet = new WrappingObservableEntrySet(this.impl.entrySet()); return this.entrySet; } @@ -171,7 +164,7 @@ public Set> entrySet() { void onBindingInvalidated() { assert (this.binding != null); - List> changes = this.binding.getChanges(); + List> changes = this.binding.getChanges(); try { this.inBoundUpdate = true; @@ -180,7 +173,7 @@ void onBindingInvalidated() { for (var change : changes) { this.putAll(change.addedElements()); change.removedElements().forEach(this::remove); - change.updatedElements().forEach((k, u) -> this.put(k, u.getNewValue())); + change.updatedElements().forEach((k, u) -> this.put(k, u.newValue())); } } } finally { @@ -193,7 +186,7 @@ void onBindingInvalidated() { * * @since 0.1.0 */ - protected final class WrappingObservableEntrySet extends AbstractSet> { + protected final class WrappingObservableEntrySet extends AbstractObservableSet> { private final Set> impl; @@ -232,14 +225,21 @@ public void remove() { }; } + @Override + protected boolean addImpl(@Nullable Entry element) { + throw new UnsupportedOperationException(); + } + @SuppressWarnings("unchecked") @Override - public boolean remove(Object element) { + protected boolean removeImpl(@Nullable Object element) { if (MapProperty.this.binding != null && !MapProperty.this.inBoundUpdate) throw new IllegalStateException("A bound property's value may not be set explicitly"); - if (this.impl.remove(element)) { - try (ChangeBuilder changeBuilder = MapProperty.this.beginChange()) { + Objects.requireNonNull(element); + + if (this.impl.remove((Map.Entry) element)) { + try (AbstractObservableMap.ChangeBuilder changeBuilder = MapProperty.this.beginChange()) { /* * Technically, this cast is wrong and there is a tiny chance that it could fail if the EntrySet of * the backing map implementation does not perform identity-checks in it's Set#remove(Object) diff --git a/modules/quitte/src/main/java/com/osmerion/quitte/property/SetProperty.java b/modules/quitte/src/main/java/com/osmerion/quitte/property/SetProperty.java index 5d63750..183199e 100644 --- a/modules/quitte/src/main/java/com/osmerion/quitte/property/SetProperty.java +++ b/modules/quitte/src/main/java/com/osmerion/quitte/property/SetProperty.java @@ -41,6 +41,7 @@ import com.osmerion.quitte.collections.AbstractObservableSet; import com.osmerion.quitte.collections.ObservableSet; +import com.osmerion.quitte.collections.SetChangeListener; import com.osmerion.quitte.internal.binding.SetBinding; /** @@ -236,7 +237,7 @@ public final int size() { void onBindingInvalidated() { assert (this.binding != null); - List> changes = this.binding.getChanges(); + List> changes = this.binding.getChanges(); try { this.inBoundUpdate = true; diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockDequeChangeListener.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockDequeChangeListener.java index 55ed6ca..d92b37e1 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockDequeChangeListener.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockDequeChangeListener.java @@ -36,24 +36,24 @@ import static org.junit.jupiter.api.Assertions.*; -final class MockDequeChangeListener implements CollectionChangeListener> { +final class MockDequeChangeListener implements DequeChangeListener { @Nullable private Context context; @SuppressWarnings("unchecked") @Override - public void onChanged(ObservableDeque.Change change) { + public void onChanged(ObservableDeque observable, DequeChangeListener.Change change) { if (this.context == null) return; change.localChanges().stream() .map(it -> { - if (it instanceof ObservableDeque.LocalChange.Insertion) { - ObservableDeque.LocalChange.Insertion localChange = ((ObservableDeque.LocalChange.Insertion) it); - return new Operation(OpType.INSERTION, localChange.getSite(), localChange.getElements()); - } else if (it instanceof ObservableDeque.LocalChange.Removal) { - ObservableDeque.LocalChange.Removal localChange = ((ObservableDeque.LocalChange.Removal) it); - return new Operation(OpType.REMOVAL, localChange.getSite(), localChange.getElements()); + if (it instanceof DequeChangeListener.LocalChange.Insertion) { + DequeChangeListener.LocalChange.Insertion localChange = ((DequeChangeListener.LocalChange.Insertion) it); + return new Operation(OpType.INSERTION, localChange.site(), localChange.elements()); + } else if (it instanceof DequeChangeListener.LocalChange.Removal) { + DequeChangeListener.LocalChange.Removal localChange = ((DequeChangeListener.LocalChange.Removal) it); + return new Operation(OpType.REMOVAL, localChange.site(), localChange.elements()); } else { throw new IllegalStateException(); } @@ -79,11 +79,11 @@ public void close() { this.assertEmpty(); } - public void assertInsertion(ObservableDeque.Site site, E element) { + public void assertInsertion(DequeChangeListener.Site site, E element) { this.assertInsertion(site, List.of(element)); } - public void assertInsertion(ObservableDeque.Site site, List elements) { + public void assertInsertion(DequeChangeListener.Site site, List elements) { Operation operation = this.operations.pollFirst(); assertNotNull(operation); @@ -92,11 +92,11 @@ public void assertInsertion(ObservableDeque.Site site, List element assertEquals(elements, operation.elements); } - public void assertRemoval(ObservableDeque.Site site, E element) { + public void assertRemoval(DequeChangeListener.Site site, E element) { this.assertRemoval(site, List.of(element)); } - public void assertRemoval(ObservableDeque.Site site, List elements) { + public void assertRemoval(DequeChangeListener.Site site, List elements) { Operation operation = this.operations.pollFirst(); assertNotNull(operation); @@ -115,10 +115,10 @@ public void assertEmpty() { private final class Operation { private final OpType type; - @Nullable private final ObservableDeque.Site site; + @Nullable private final DequeChangeListener.Site site; private final List elements; - Operation(OpType type, @Nullable ObservableDeque.Site site, List elements) { + Operation(OpType type, @Nullable DequeChangeListener.Site site, List elements) { this.type = type; this.site = site; this.elements = elements; diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockListChangeListener.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockListChangeListener.java index 77e3bda..fdf076e 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockListChangeListener.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockListChangeListener.java @@ -31,40 +31,48 @@ package com.osmerion.quitte.collections; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.List; import javax.annotation.Nullable; import static org.junit.jupiter.api.Assertions.*; -final class MockListChangeListener implements CollectionChangeListener> { +final class MockListChangeListener implements ListChangeListener { @Nullable private Context context; @SuppressWarnings("unchecked") @Override - public void onChanged(ObservableList.Change change) { + public void onChanged(ObservableList observable, ListChangeListener.Change change) { if (this.context == null) return; - if (change instanceof ObservableList.Change.Permutation) { - ObservableList.Change.Permutation permutation = (ObservableList.Change.Permutation) change; - this.context.operations.add(new Permutation<>(permutation.getIndices())); - } else if (change instanceof ObservableList.Change.Update) { - ObservableList.Change.Update update = (ObservableList.Change.Update) change; - update.getLocalChanges().forEach(localChange -> { + if (change instanceof ListChangeListener.Change.Permutation) { + ListChangeListener.Change.Permutation permutation = (ListChangeListener.Change.Permutation) change; + this.context.operations.add(new Permutation<>(permutation.indices())); + } else if (change instanceof ListChangeListener.Change.Update) { + ListChangeListener.Change.Update update = (ListChangeListener.Change.Update) change; + update.localChanges().forEach(localChange -> { OpType type; + List oldElements, newElements; - if (localChange instanceof ObservableList.LocalChange.Insertion) { + if (localChange instanceof ListChangeListener.LocalChange.Insertion localInsertion) { type = OpType.INSERTION; - } else if (localChange instanceof ObservableList.LocalChange.Removal) { + oldElements = List.of(); + newElements = localInsertion.elements(); + } else if (localChange instanceof ListChangeListener.LocalChange.Removal localRemoval) { type = OpType.REMOVAL; - } else if (localChange instanceof ObservableList.LocalChange.Update) { + oldElements = localRemoval.elements(); + newElements = List.of(); + } else if (localChange instanceof ListChangeListener.LocalChange.Update localUpdate) { type = OpType.UPDATE; + oldElements = localUpdate.oldElements(); + newElements = localUpdate.newElements(); } else { throw new IllegalStateException(); } - this.context.operations.add(new Update<>(type, localChange.getIndex(), localChange.getElements())); + this.context.operations.add(new Update<>(type, localChange.index(), oldElements, newElements)); }); } else { throw new UnsupportedOperationException(); @@ -103,7 +111,7 @@ public void assertInsertion(int offset, List elements) { } assertEquals(offset, update.offset); - assertEquals(elements, update.elements); + assertEquals(elements, update.newElements()); } @SafeVarargs @@ -120,7 +128,7 @@ public void assertRemoval(int offset, List elements) { } assertEquals(offset, update.offset); - assertEquals(elements, update.elements); + assertEquals(elements, update.oldElements()); } public void assertPermutation(List indices) { @@ -134,18 +142,45 @@ public void assertPermutation(List indices) { assertEquals(indices, permutation.indices); } + @SafeVarargs + public final void assertUpdate(int offset, E... elements) { + List oldElements = new ArrayList<>(); + List newElements = new ArrayList<>(); + + for (int i = 0; i < elements.length / 2; i += 2) { + oldElements.add(elements[i]); + newElements.add(elements[i + 1]); + } + + assertUpdate(offset, List.copyOf(oldElements), List.copyOf(newElements)); + } + + public void assertUpdate(int offset, List oldElements, List newElements) { + Operation operation = this.operations.pollFirst(); + + if (!(operation instanceof Update update) || update.type != OpType.UPDATE) { + fail(); + return; + } + + assertEquals(offset, update.offset()); + assertEquals(oldElements, update.oldElements()); + assertEquals(newElements, update.newElements()); + } + /** Asserts that there are no unconsumed operations left in the current context. */ public void assertEmpty() { assertTrue(this.operations.isEmpty()); } - + } - private interface Operation {} + @SuppressWarnings("unused") + private sealed interface Operation {} - private static record Permutation(List indices) implements Operation {} + private record Permutation(List indices) implements Operation {} - private record Update(OpType type, int offset, List elements) implements Operation {} + private record Update(OpType type, int offset, List oldElements, List newElements) implements Operation {} enum OpType { INSERTION, diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockMapChangeListener.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockMapChangeListener.java index 2d4f30e..47241e4 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockMapChangeListener.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockMapChangeListener.java @@ -30,59 +30,83 @@ */ package com.osmerion.quitte.collections; +import java.util.AbstractMap; import java.util.ArrayDeque; +import java.util.Map; import java.util.Objects; import javax.annotation.Nullable; import static org.junit.jupiter.api.Assertions.*; -final class MockMapChangeListener implements CollectionChangeListener> { +final class MockMapChangeListener implements MapChangeListener { + + final MockSetChangeListener> entrySetListener = new MockSetChangeListener<>(); + final MockSetChangeListener keySetListener = new MockSetChangeListener<>(); @Nullable private Context context; @Override - public void onChanged(ObservableMap.Change change) { + public void onChanged(ObservableMap observable, MapChangeListener.Change change) { if (this.context == null) return; change.addedElements().forEach((k, v) -> this.context.operations.add(new Addition(k, v))); change.removedElements().forEach((k, v) -> this.context.operations.add(new Removal(k, v))); - change.updatedElements().forEach((k, update) -> this.context.operations.add(new Update(k, update.getOldValue(), update.getNewValue()))); + change.updatedElements().forEach((k, update) -> this.context.operations.add(new Update(k, update.oldValue(), update.newValue()))); } public Context push() { if (this.context != null) throw new IllegalStateException(); - return (this.context = new Context()); + return (this.context = new Context(this.entrySetListener.push(), this.keySetListener.push())); } public final class Context implements AutoCloseable { private final ArrayDeque operations = new ArrayDeque<>(); - private Context() {} + private final MockSetChangeListener>.Context entrySetContext; + private final MockSetChangeListener.Context keySetContext; + + private Context( + MockSetChangeListener>.Context entrySetContext, + MockSetChangeListener.Context keySetContext + ) { + this.entrySetContext = entrySetContext; + this.keySetContext = keySetContext; + } /** Closes this context and asserts that all operations have been consumed. */ @Override public void close() { + this.entrySetContext.close(); + MockMapChangeListener.this.context = null; this.assertEmpty(); } public void assertAddition(K key, V value) { assertTrue(this.operations.removeFirstOccurrence(new Addition(key, value)), "No unconsumed addition of element recorded"); + this.entrySetContext.assertAddition(new AbstractMap.SimpleEntry<>(key, value)); + this.keySetContext.assertAddition(key); } public void assertRemoval(K key, V value) { assertTrue(this.operations.removeFirstOccurrence(new Removal(key, value)), "No unconsumed removal of element recorded"); + this.entrySetContext.assertRemoval(new AbstractMap.SimpleEntry<>(key, value)); + this.keySetContext.assertRemoval(key); } public void assertUpdate(K key, V oldValue, V newValue) { assertTrue(this.operations.removeFirstOccurrence(new Update(key, oldValue, newValue)), "No unconsumed update of element recorded"); + this.entrySetContext.assertRemoval(new AbstractMap.SimpleEntry<>(key, oldValue)); + this.entrySetContext.assertAddition(new AbstractMap.SimpleEntry<>(key, newValue)); } /** Asserts that there are no unconsumed operations left in the current context. */ public void assertEmpty() { assertTrue(this.operations.isEmpty()); + this.entrySetContext.assertEmpty(); + this.keySetContext.assertEmpty(); } } diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockSetChangeListener.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockSetChangeListener.java index c0d80a3..128aa08 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockSetChangeListener.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/MockSetChangeListener.java @@ -31,23 +31,22 @@ package com.osmerion.quitte.collections; import java.util.ArrayDeque; -import java.util.Objects; import javax.annotation.Nullable; import static org.junit.jupiter.api.Assertions.*; -final class MockSetChangeListener implements CollectionChangeListener> { +final class MockSetChangeListener implements SetChangeListener { @Nullable private Context context; @Override - public void onChanged(ObservableSet.Change change) { + public void onChanged(ObservableSet observable, SetChangeListener.Change change) { if (this.context == null) return; - change.addedElements().forEach(e -> this.context.operations.add(new Addition(e))); - change.removedElements().forEach(e -> this.context.operations.add(new Removal(e))); + change.addedElements().forEach(e -> this.context.operations.add(new Addition<>(e))); + change.removedElements().forEach(e -> this.context.operations.add(new Removal<>(e))); } public Context push() { @@ -57,7 +56,7 @@ public Context push() { public final class Context implements AutoCloseable { - private final ArrayDeque operations = new ArrayDeque<>(); + private final ArrayDeque> operations = new ArrayDeque<>(); private Context() {} @@ -69,11 +68,11 @@ public void close() { } public void assertAddition(E element) { - assertTrue(this.operations.removeFirstOccurrence(new Addition(element)), "No unconsumed addition of element recorded"); + assertTrue(this.operations.removeFirstOccurrence(new Addition<>(element)), "No unconsumed addition of element recorded"); } public void assertRemoval(E element) { - assertTrue(this.operations.removeFirstOccurrence(new Removal(element)), "No unconsumed removal of element recorded"); + assertTrue(this.operations.removeFirstOccurrence(new Removal<>(element)), "No unconsumed removal of element recorded"); } /** Asserts that there are no unconsumed operations left in the current context. */ @@ -83,35 +82,9 @@ public void assertEmpty() { } - private abstract class Operation { + private sealed interface Operation {} - private final E element; - - Operation(E element) { - this.element = element; - } - - @SuppressWarnings("unchecked") - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - - if (this.getClass() == obj.getClass()) { - Operation other = (Operation) obj; - return Objects.equals(this.element, other.element); - } - - return false; - } - - } - - private class Addition extends Operation { - Addition(E element) { super(element); } - } - - private class Removal extends Operation { - Removal(E element) { super(element); } - } + private record Addition(E element) implements Operation {} + private record Removal(E element) implements Operation {} } \ No newline at end of file diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableDequeTest.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableDequeTest.java index 3ecc523..c7c0e64 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableDequeTest.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableDequeTest.java @@ -63,14 +63,14 @@ public void reset() { public void testClear() { try (var changeCtx = this.changeListener.push()) { this.observableDeque.addAll(List.of("foo", "bar")); - changeCtx.assertInsertion(ObservableDeque.Site.TAIL, List.of("foo", "bar")); + changeCtx.assertInsertion(DequeChangeListener.Site.TAIL, List.of("foo", "bar")); changeCtx.assertEmpty(); this.observableDeque.add("foo"); - changeCtx.assertInsertion(ObservableDeque.Site.TAIL, "foo"); + changeCtx.assertInsertion(DequeChangeListener.Site.TAIL, "foo"); this.observableDeque.clear(); - changeCtx.assertRemoval(ObservableDeque.Site.OPAQUE, List.of("foo", "bar", "foo")); + changeCtx.assertRemoval(DequeChangeListener.Site.OPAQUE, List.of("foo", "bar", "foo")); changeCtx.assertEmpty(); } } diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableListTest.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableListTest.java index 24e70ee..d5fec45 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableListTest.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableListTest.java @@ -167,6 +167,17 @@ public void testSize() { assertEquals(1, this.observableList.size()); } + @Test + public void testUpdate() { + this.observableList.add(0, "foo"); + try (var changeCtx = this.changeListener.push()) { + this.observableList.set(0, "bar"); + changeCtx.assertUpdate(0, "foo", "bar"); + + this.observableList.set(0, "bar"); // Ensure that no change is created + } + } + @Test @DisplayName("ObservableList#size() after modification of underlying List") public void testSizeWithUnderlyingModification() { diff --git a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableMapTest.java b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableMapTest.java index 74ca063..5fb058e 100644 --- a/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableMapTest.java +++ b/modules/quitte/src/test/java/com/osmerion/quitte/collections/ObservableMapTest.java @@ -55,6 +55,8 @@ public final class ObservableMapTest { public void reset() { this.observableMap = ObservableMap.of(this.underlyingMap = new HashMap<>()); this.observableMap.addChangeListener(this.changeListener = new MockMapChangeListener<>()); + this.observableMap.entrySet().addChangeListener(this.changeListener.entrySetListener); + this.observableMap.keySet().addChangeListener(this.changeListener.keySetListener); } @Test @@ -77,6 +79,17 @@ public void testClear() { } } + @Test + @DisplayName("ObservableMap#entrySet() => remove(Object)") + public void testEntrySetRemove() { + this.observableMap.put("foo", "bar"); + + try (var changeCtx = this.changeListener.push()) { + this.observableMap.entrySet().remove(Map.entry("foo", "bar")); + changeCtx.assertRemoval("foo", "bar"); + } + } + @Test @DisplayName("ObservableMap#isEmpty() after modification") public void testIsEmpty() { @@ -117,6 +130,29 @@ public void testNull() { } } + @Test + @DisplayName("ObservableMap#put(Object, Object)") + public void testPut() { + try (var changeCtx = this.changeListener.push()) { + this.observableMap.put("foo", "bar"); + changeCtx.assertAddition("foo", "bar"); + + this.observableMap.put("foo", "boo"); + changeCtx.assertUpdate("foo", "bar", "boo"); + } + } + + @Test + @DisplayName("ObservableMap#remove(Object)") + public void testRemove() { + this.observableMap.put("foo", "bar"); + + try (var changeCtx = this.changeListener.push()) { + this.observableMap.remove("foo"); + changeCtx.assertRemoval("foo", "bar"); + } + } + @Test @DisplayName("ObservableMap#size()") public void testSize() {