Skip to content

Commit 148069a

Browse files
committed
Fix ObservableMap replacements being recorded as two separate change events
1 parent d9df9ef commit 148069a

File tree

4 files changed

+329
-7
lines changed

4 files changed

+329
-7
lines changed

src/main/java/software/coley/collections/box/IntBox.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,30 @@ public void set(int value) {
4141
this.value = value;
4242
}
4343

44+
/**
45+
* Increment by 1
46+
*/
47+
public void increment() {
48+
increment(1);
49+
}
50+
51+
/**
52+
* Decrement by 1
53+
*/
54+
public void decrement() {
55+
increment(-1);
56+
}
57+
58+
/**
59+
* Increment by the given value.
60+
*
61+
* @param value
62+
* Value to increment by.
63+
*/
64+
public void increment(int value) {
65+
set(get() + value);
66+
}
67+
4468
/**
4569
* @param function
4670
* Mapping function.

src/main/java/software/coley/collections/observable/MapChange.java

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package software.coley.collections.observable;
22

3+
import software.coley.collections.Sets;
4+
35
import javax.annotation.Nonnull;
6+
import javax.annotation.Nullable;
47
import java.util.Collections;
8+
import java.util.Iterator;
59
import java.util.Map;
10+
import java.util.Objects;
611
import java.util.Set;
712
import java.util.stream.Collectors;
813

@@ -75,6 +80,33 @@ public static <K, V> MapChange<K, V> removal(@Nonnull Map<K, V> removed) {
7580
return new MapChange<>(Collections.emptySet(), mappedRemoved);
7681
}
7782

83+
/**
84+
* @param added
85+
* Added items.
86+
* @param removed
87+
* Removed items.
88+
* @param <K>
89+
* Map key type.
90+
* @param <V>
91+
* Map value type.
92+
*
93+
* @return Change of the added and removed items.
94+
*/
95+
@Nonnull
96+
public static <K, V> MapChange<K, V> of(@Nullable Map<K, V> added, @Nullable Map<K, V> removed) {
97+
Set<Entry<K, V>> mappedAdded = added == null ?
98+
Collections.emptySet() :
99+
added.entrySet().stream()
100+
.map(Entry::new)
101+
.collect(Collectors.toSet());
102+
Set<Entry<K, V>> mappedRemoved = removed == null ?
103+
Collections.emptySet() :
104+
removed.entrySet().stream()
105+
.map(Entry::new)
106+
.collect(Collectors.toSet());
107+
return new MapChange<>(mappedAdded, mappedRemoved);
108+
}
109+
78110
/**
79111
* @return Added entries.
80112
*/
@@ -91,6 +123,95 @@ public Set<Entry<K, V>> getRemovedEntries() {
91123
return removedEntries;
92124
}
93125

126+
/**
127+
* @return Iterator for all entries <i>(added and removed)</i>.
128+
*/
129+
@Nonnull
130+
public Iterator<Entry<K, V>> entryIterator() {
131+
return Sets.iterator(addedEntries, removedEntries);
132+
}
133+
134+
/**
135+
* @param entry
136+
* Entry to check.
137+
*
138+
* @return {@code true} when the entry holds an added key in this change.
139+
*/
140+
public boolean wasAdded(@Nonnull Entry<K, ?> entry) {
141+
return wasAdded(entry.key);
142+
}
143+
144+
/**
145+
* @param key
146+
* Key to check.
147+
*
148+
* @return {@code true} when the key was added in this change.
149+
*/
150+
public boolean wasAdded(@Nullable K key) {
151+
return addedEntries.stream().anyMatch(e -> Objects.equals(key, e.getKey())) &&
152+
removedEntries.stream().noneMatch(e -> Objects.equals(key, e.getKey()));
153+
}
154+
155+
/**
156+
* @param entry
157+
* Entry to check.
158+
*
159+
* @return {@code true} when the entry holds a removed key in this change.
160+
*/
161+
public boolean wasRemoved(@Nonnull Entry<K, ?> entry) {
162+
return wasRemoved(entry.key);
163+
}
164+
165+
/**
166+
* @param key
167+
* Key to check.
168+
*
169+
* @return {@code true} when the key was removed in this change.
170+
*/
171+
public boolean wasRemoved(@Nullable K key) {
172+
return removedEntries.stream().anyMatch(e -> Objects.equals(key, e.getKey())) &&
173+
addedEntries.stream().noneMatch(e -> Objects.equals(key, e.getKey()));
174+
}
175+
176+
/**
177+
* @param entry
178+
* Entry to check.
179+
*
180+
* @return {@code true} when the entry holds a replaced key in this change.
181+
*/
182+
public boolean wasReplaced(@Nonnull Entry<K, ?> entry) {
183+
return wasReplaced(entry.key);
184+
}
185+
186+
/**
187+
* @param key
188+
* Key to check.
189+
*
190+
* @return {@code true} when the key was replaced in this change.
191+
*/
192+
public boolean wasReplaced(@Nullable K key) {
193+
return removedEntries.stream().anyMatch(e -> Objects.equals(key, e.getKey())) &&
194+
addedEntries.stream().anyMatch(e -> Objects.equals(key, e.getKey()));
195+
}
196+
197+
@Override
198+
public boolean equals(Object o) {
199+
if (this == o) return true;
200+
if (!(o instanceof MapChange)) return false;
201+
202+
MapChange<?, ?> mapChange = (MapChange<?, ?>) o;
203+
204+
if (!addedEntries.equals(mapChange.addedEntries)) return false;
205+
return removedEntries.equals(mapChange.removedEntries);
206+
}
207+
208+
@Override
209+
public int hashCode() {
210+
int result = addedEntries.hashCode();
211+
result = 31 * result + removedEntries.hashCode();
212+
return result;
213+
}
214+
94215
/**
95216
* Pair type mirroring an {@link Map.Entry}.
96217
*
@@ -135,5 +256,28 @@ public K getKey() {
135256
public V getValue() {
136257
return value;
137258
}
259+
260+
@Override
261+
public boolean equals(Object o) {
262+
if (this == o) return true;
263+
if (!(o instanceof Entry)) return false;
264+
265+
Entry<?, ?> entry = (Entry<?, ?>) o;
266+
267+
if (!Objects.equals(key, entry.key)) return false;
268+
return Objects.equals(value, entry.value);
269+
}
270+
271+
@Override
272+
public int hashCode() {
273+
int result = key != null ? key.hashCode() : 0;
274+
result = 31 * result + (value != null ? value.hashCode() : 0);
275+
return result;
276+
}
277+
278+
@Override
279+
public String toString() {
280+
return key + "=" + value;
281+
}
138282
}
139283
}

src/main/java/software/coley/collections/observable/ObservableMap.java

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,12 +93,13 @@ private void post(@Nonnull MapChange<K, V> change) {
9393

9494
@Override
9595
public V put(K key, V value) {
96-
V result = super.put(key, value);
97-
if (result != null) {
98-
post(MapChange.removal(Maps.of(key, result)));
96+
V replaced = super.put(key, value);
97+
if (replaced != null) {
98+
post(MapChange.of(Maps.of(key, value), Maps.of(key, replaced)));
99+
} else {
100+
post(MapChange.addition(Maps.of(key, value)));
99101
}
100-
post(MapChange.addition(Maps.of(key, value)));
101-
return result;
102+
return replaced;
102103
}
103104

104105
@Override
@@ -110,9 +111,10 @@ public void putAll(@Nonnull Map<? extends K, ? extends V> m) {
110111
.forEach(e -> replaced.put(e.getKey(), e.getValue()));
111112
super.putAll(m);
112113
if (!replaced.isEmpty()) {
113-
post(MapChange.addition(replaced));
114+
post(MapChange.of((Map<K, V>) m, replaced));
115+
} else {
116+
post(MapChange.addition((Map<K, V>) m));
114117
}
115-
post(MapChange.addition((Map<K, V>) m));
116118
}
117119

118120
@Override
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package software.coley.collections.observable;
2+
3+
import org.junit.jupiter.api.Test;
4+
import software.coley.collections.Lists;
5+
import software.coley.collections.box.IntBox;
6+
7+
import java.util.Comparator;
8+
import java.util.HashMap;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
import static org.junit.jupiter.api.Assertions.*;
13+
14+
public class MapTest {
15+
private static final Comparator<MapChange.Entry<String, String>> ENTRY_CMP = Comparator.comparing(MapChange.Entry::getKey);
16+
17+
@Test
18+
void put() {
19+
IntBox box = new IntBox();
20+
21+
ObservableMap<String, String> map = new ObservableMap<>();
22+
map.addChangeListener((source, change) -> {
23+
assertEquals(1, change.getAddedEntries().size());
24+
25+
MapChange.Entry<String, String> entry = change.getAddedEntries().iterator().next();
26+
assertEquals("k", entry.getKey());
27+
assertEquals("v", entry.getValue());
28+
assertTrue(change.wasAdded(entry));
29+
assertFalse(change.wasRemoved(entry));
30+
assertFalse(change.wasReplaced(entry));
31+
box.increment();
32+
});
33+
34+
map.put("k", "v");
35+
assertEquals(1, box.get());
36+
}
37+
38+
@Test
39+
void putAll() {
40+
IntBox box = new IntBox();
41+
Map<String, String> stuff = new HashMap<>();
42+
stuff.put("k1", "v1");
43+
stuff.put("k2", "v2");
44+
45+
ObservableMap<String, String> map = new ObservableMap<>();
46+
map.addChangeListener((source, change) -> {
47+
assertEquals(2, change.getAddedEntries().size());
48+
49+
List<MapChange.Entry<String, String>> added = Lists.sorted(ENTRY_CMP, change.getAddedEntries());
50+
MapChange.Entry<String, String> entry = added.get(0);
51+
assertEquals("k1", entry.getKey());
52+
assertEquals("v1", entry.getValue());
53+
assertTrue(change.wasAdded(entry));
54+
assertFalse(change.wasRemoved(entry));
55+
assertFalse(change.wasReplaced(entry));
56+
57+
entry = added.get(1);
58+
assertEquals("k2", entry.getKey());
59+
assertEquals("v2", entry.getValue());
60+
assertTrue(change.wasAdded(entry));
61+
assertFalse(change.wasRemoved(entry));
62+
assertFalse(change.wasReplaced(entry));
63+
64+
box.increment();
65+
});
66+
67+
map.putAll(stuff);
68+
assertEquals(1, box.get());
69+
}
70+
71+
@Test
72+
void putReplace() {
73+
IntBox box = new IntBox();
74+
75+
ObservableMap<String, String> map = new ObservableMap<>();
76+
map.put("k", "v");
77+
78+
map.addChangeListener((source, change) -> {
79+
assertEquals(1, change.getRemovedEntries().size());
80+
assertEquals(1, change.getAddedEntries().size());
81+
82+
MapChange.Entry<String, String> entry = change.getRemovedEntries().iterator().next();
83+
assertEquals("k", entry.getKey());
84+
assertEquals("v", entry.getValue());
85+
assertFalse(change.wasAdded(entry));
86+
assertFalse(change.wasRemoved(entry));
87+
assertTrue(change.wasReplaced(entry));
88+
89+
entry = change.getAddedEntries().iterator().next();
90+
assertEquals("k", entry.getKey());
91+
assertEquals("other", entry.getValue());
92+
assertFalse(change.wasAdded(entry));
93+
assertFalse(change.wasRemoved(entry));
94+
assertTrue(change.wasReplaced(entry));
95+
96+
box.increment();
97+
});
98+
99+
map.put("k", "other");
100+
assertEquals(1, box.get());
101+
}
102+
103+
@Test
104+
void putAllReplace() {
105+
IntBox box = new IntBox();
106+
Map<String, String> stuff = new HashMap<>();
107+
stuff.put("k1", "other1");
108+
stuff.put("k2", "other2");
109+
110+
ObservableMap<String, String> map = new ObservableMap<>();
111+
map.put("k1", "v1");
112+
map.put("k2", "v2");
113+
114+
map.addChangeListener((source, change) -> {
115+
assertEquals(2, change.getRemovedEntries().size());
116+
assertEquals(2, change.getAddedEntries().size());
117+
118+
List<MapChange.Entry<String, String>> removed = Lists.sorted(ENTRY_CMP, change.getRemovedEntries());
119+
MapChange.Entry<String, String> entry = removed.get(0);
120+
assertEquals("k1", entry.getKey());
121+
assertEquals("v1", entry.getValue());
122+
assertFalse(change.wasAdded(entry));
123+
assertFalse(change.wasRemoved(entry));
124+
assertTrue(change.wasReplaced(entry));
125+
entry = removed.get(1);
126+
assertEquals("k2", entry.getKey());
127+
assertEquals("v2", entry.getValue());
128+
assertFalse(change.wasAdded(entry));
129+
assertFalse(change.wasRemoved(entry));
130+
assertTrue(change.wasReplaced(entry));
131+
132+
List<MapChange.Entry<String, String>> added = Lists.sorted(ENTRY_CMP, change.getAddedEntries());
133+
entry = added.get(0);
134+
assertEquals("k1", entry.getKey());
135+
assertEquals("other1", entry.getValue());
136+
assertFalse(change.wasAdded(entry));
137+
assertFalse(change.wasRemoved(entry));
138+
assertTrue(change.wasReplaced(entry));
139+
entry = added.get(1);
140+
assertEquals("k2", entry.getKey());
141+
assertEquals("other2", entry.getValue());
142+
assertFalse(change.wasAdded(entry));
143+
assertFalse(change.wasRemoved(entry));
144+
assertTrue(change.wasReplaced(entry));
145+
146+
box.increment();
147+
});
148+
149+
map.putAll(stuff);
150+
assertEquals(1, box.get());
151+
}
152+
}

0 commit comments

Comments
 (0)