-
Notifications
You must be signed in to change notification settings - Fork 1
/
ObservableMap.java
151 lines (136 loc) · 3.8 KB
/
ObservableMap.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
package software.coley.collections.observable;
import software.coley.collections.Maps;
import software.coley.collections.delegate.DelegatingMap;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiFunction;
import java.util.function.Supplier;
/**
* Map type that allows listening to modifications.
*
* @param <K>
* Map key type.
* @param <V>
* Map value type.
*
* @author Matt Coley
* @see MapChangeListener
*/
public class ObservableMap<K, V> extends DelegatingMap<K, V> {
private final List<MapChangeListener<K, V>> listeners = new ArrayList<>();
/**
* New observable map with no pre-existing items.
*/
public ObservableMap() {
// We use a hash-map backing implementation since we want delegate to be modifiable (as opposed
// to using something like Collections.emptyMap()).
this(HashMap::new);
}
/**
* New observable map with some pre-existing items.
*
* @param items
* Existing items to create as an initial state.
*/
public ObservableMap(@Nonnull Map<K, V> items) {
// We wrap the incoming map just to ensure our delegate can modify the map without unintended effects
// if the user passes a map intended to be immutable.
this(() -> new HashMap<>(items));
}
/**
* New observable map with a backing map provided by a factory.
*
* @param factory
* Factory to provide a backing map implementation.
* Intended to be a single call to a map implementation constructor with zero ties to outside state.
*/
public ObservableMap(@Nonnull Supplier<Map<K, V>> factory) {
super(factory.get());
}
/**
* Subscribe a listener to events about map modifications.
*
* @param listener
* Listener to add.
*/
public void addChangeListener(@Nullable MapChangeListener<K, V> listener) {
if (listener != null)
listeners.add(listener);
}
/**
* Unsubscribe a listener from events about map modifications.
*
* @param listener
* Listener to remove.
*
* @return {@code true} on removal success.
*/
public boolean removeChangeListener(@Nullable MapChangeListener<K, V> listener) {
return listeners.remove(listener);
}
/**
* Called AFTER an operation completes.
*
* @param change
* Change to post to subscribed listeners.
*/
private void post(@Nonnull MapChange<K, V> change) {
listeners.forEach(listener -> listener.onMapChanged(this, change));
}
@Override
public V put(K key, V value) {
V result = super.put(key, value);
if (result != null) {
post(MapChange.removal(Maps.of(key, result)));
}
post(MapChange.addition(Maps.of(key, value)));
return result;
}
@Override
@SuppressWarnings("unchecked")
public void putAll(@Nonnull Map<? extends K, ? extends V> m) {
Map<K, V> replaced = new HashMap<>();
entrySet().stream()
.filter(e -> m.containsKey(e.getKey()))
.forEach(e -> replaced.put(e.getKey(), e.getValue()));
super.putAll(m);
if (!replaced.isEmpty()) {
post(MapChange.addition(replaced));
}
post(MapChange.addition((Map<K, V>) m));
}
@Override
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Map<K, V> copy = new HashMap<>(this);
copy.replaceAll(function);
putAll(copy);
}
@Override
@SuppressWarnings("unchecked")
public boolean remove(Object key, Object value) {
boolean result = super.remove(key, value);
if (result) {
post(MapChange.removal(Maps.of((K) key, (V) value)));
}
return result;
}
@Override
@SuppressWarnings("unchecked")
public V remove(Object key) {
V result = super.remove(key);
if (result != null) {
post(MapChange.removal(Maps.of((K) key, result)));
}
return result;
}
@Override
public void clear() {
Map<K, V> copy = new HashMap<>(this);
super.clear();
post(MapChange.removal(copy));
}
}