17 |
ilm |
1 |
/*
|
|
|
2 |
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
|
|
|
3 |
*
|
|
|
4 |
* Copyright 2011 OpenConcerto, by ILM Informatique. All rights reserved.
|
|
|
5 |
*
|
|
|
6 |
* The contents of this file are subject to the terms of the GNU General Public License Version 3
|
|
|
7 |
* only ("GPL"). You may not use this file except in compliance with the License. You can obtain a
|
|
|
8 |
* copy of the License at http://www.gnu.org/licenses/gpl-3.0.html See the License for the specific
|
|
|
9 |
* language governing permissions and limitations under the License.
|
|
|
10 |
*
|
|
|
11 |
* When distributing the software, include this License Header Notice in each file.
|
|
|
12 |
*/
|
|
|
13 |
|
|
|
14 |
package org.openconcerto.utils.cache;
|
|
|
15 |
|
|
|
16 |
import org.openconcerto.utils.Log;
|
132 |
ilm |
17 |
import org.openconcerto.utils.cache.CacheItem.RemovalType;
|
17 |
ilm |
18 |
import org.openconcerto.utils.cache.CacheResult.State;
|
144 |
ilm |
19 |
import org.openconcerto.utils.cc.IClosure;
|
17 |
ilm |
20 |
|
144 |
ilm |
21 |
import java.beans.PropertyChangeEvent;
|
|
|
22 |
import java.beans.PropertyChangeListener;
|
|
|
23 |
import java.beans.PropertyChangeSupport;
|
132 |
ilm |
24 |
import java.util.ArrayList;
|
17 |
ilm |
25 |
import java.util.Collections;
|
|
|
26 |
import java.util.HashMap;
|
|
|
27 |
import java.util.HashSet;
|
|
|
28 |
import java.util.LinkedHashMap;
|
132 |
ilm |
29 |
import java.util.List;
|
17 |
ilm |
30 |
import java.util.Map;
|
|
|
31 |
import java.util.Set;
|
132 |
ilm |
32 |
import java.util.concurrent.TimeUnit;
|
17 |
ilm |
33 |
import java.util.logging.Level;
|
|
|
34 |
|
182 |
ilm |
35 |
import net.jcip.annotations.GuardedBy;
|
|
|
36 |
|
17 |
ilm |
37 |
/**
|
|
|
38 |
* To keep results computed from some data. The results will be automatically invalidated after some
|
|
|
39 |
* period of time or when the data is modified.
|
|
|
40 |
*
|
|
|
41 |
* @author Sylvain CUAZ
|
|
|
42 |
* @param <K> key type, eg String.
|
|
|
43 |
* @param <V> value type, eg List of SQLRow.
|
|
|
44 |
* @param <D> source data type, eg SQLTable.
|
|
|
45 |
*/
|
|
|
46 |
public class ICache<K, V, D> {
|
|
|
47 |
|
|
|
48 |
private static final Level LEVEL = Level.FINEST;
|
|
|
49 |
|
144 |
ilm |
50 |
public static final String ITEMS_CHANGED = "itemsChanged";
|
|
|
51 |
public static final String ITEM_ADDED = "itemAdded";
|
|
|
52 |
public static final String ITEM_REMOVED = "itemRemoved";
|
|
|
53 |
|
132 |
ilm |
54 |
private final ICacheSupport<D> supp;
|
|
|
55 |
// linked to fifo, ATTN the values in this map can be invalid since clear() is called without
|
|
|
56 |
// the lock on CacheValue
|
182 |
ilm |
57 |
@GuardedBy("this")
|
132 |
ilm |
58 |
private final LinkedHashMap<K, CacheItem<K, V, D>> cache;
|
182 |
ilm |
59 |
@GuardedBy("this")
|
132 |
ilm |
60 |
private final Map<K, CacheItem<K, V, D>> running;
|
17 |
ilm |
61 |
private final int delay;
|
|
|
62 |
private final int size;
|
|
|
63 |
private final String name;
|
|
|
64 |
|
73 |
ilm |
65 |
private ICache<K, V, D> parent;
|
|
|
66 |
|
144 |
ilm |
67 |
private final PropertyChangeSupport propSupp = new PropertyChangeSupport(this);
|
|
|
68 |
|
17 |
ilm |
69 |
public ICache() {
|
|
|
70 |
this(60);
|
|
|
71 |
}
|
|
|
72 |
|
|
|
73 |
public ICache(int delay) {
|
|
|
74 |
this(delay, -1);
|
|
|
75 |
}
|
|
|
76 |
|
|
|
77 |
public ICache(int delay, int size) {
|
|
|
78 |
this(delay, size, null);
|
|
|
79 |
}
|
|
|
80 |
|
|
|
81 |
/**
|
|
|
82 |
* Creates a cache with the given parameters.
|
|
|
83 |
*
|
|
|
84 |
* @param delay the delay in seconds before a key is cleared.
|
|
|
85 |
* @param size the maximum size of the cache, negative means no limit.
|
|
|
86 |
* @param name name of this cache and associated thread.
|
|
|
87 |
* @throws IllegalArgumentException if size is 0.
|
|
|
88 |
*/
|
|
|
89 |
public ICache(int delay, int size, String name) {
|
132 |
ilm |
90 |
this(null, delay, size, name);
|
|
|
91 |
}
|
|
|
92 |
|
|
|
93 |
public ICache(final ICacheSupport<D> supp, int delay, int size, String name) {
|
|
|
94 |
this.supp = supp == null ? createSupp(getCacheSuppName(name)) : supp;
|
|
|
95 |
this.running = new HashMap<K, CacheItem<K, V, D>>();
|
17 |
ilm |
96 |
this.delay = delay;
|
|
|
97 |
if (size == 0)
|
|
|
98 |
throw new IllegalArgumentException("0 size");
|
|
|
99 |
this.size = size;
|
132 |
ilm |
100 |
this.cache = new LinkedHashMap<K, CacheItem<K, V, D>>(size < 0 ? 64 : size);
|
17 |
ilm |
101 |
this.name = name;
|
|
|
102 |
|
73 |
ilm |
103 |
this.parent = null;
|
17 |
ilm |
104 |
}
|
|
|
105 |
|
132 |
ilm |
106 |
protected ICacheSupport<D> createSupp(final String name) {
|
|
|
107 |
return new ICacheSupport<D>(name);
|
17 |
ilm |
108 |
}
|
|
|
109 |
|
132 |
ilm |
110 |
protected String getCacheSuppName(final String cacheName) {
|
|
|
111 |
return cacheName;
|
|
|
112 |
}
|
17 |
ilm |
113 |
|
132 |
ilm |
114 |
public final ICacheSupport<D> getSupp() {
|
|
|
115 |
return this.supp;
|
|
|
116 |
}
|
17 |
ilm |
117 |
|
132 |
ilm |
118 |
public final int getMaximumSize() {
|
|
|
119 |
return this.size;
|
17 |
ilm |
120 |
}
|
|
|
121 |
|
132 |
ilm |
122 |
public final String getName() {
|
|
|
123 |
return this.name;
|
|
|
124 |
}
|
|
|
125 |
|
17 |
ilm |
126 |
/**
|
73 |
ilm |
127 |
* Allow to continue the search for a key in another instance.
|
|
|
128 |
*
|
|
|
129 |
* @param parent the cache to search when a key isn't found in this.
|
|
|
130 |
*/
|
|
|
131 |
public final synchronized void setParent(final ICache<K, V, D> parent) {
|
|
|
132 |
ICache<K, V, D> current = parent;
|
|
|
133 |
while (current != null) {
|
|
|
134 |
if (current == this)
|
|
|
135 |
throw new IllegalArgumentException("Cycle detected, cannot set parent to " + parent);
|
|
|
136 |
current = current.getParent();
|
|
|
137 |
}
|
|
|
138 |
this.parent = parent;
|
|
|
139 |
}
|
|
|
140 |
|
|
|
141 |
public final synchronized ICache<K, V, D> getParent() {
|
|
|
142 |
return this.parent;
|
|
|
143 |
}
|
|
|
144 |
|
|
|
145 |
/**
|
17 |
ilm |
146 |
* If <code>sel</code> is in cache returns its value, else if key is running block until the key
|
73 |
ilm |
147 |
* is put (or the current thread is interrupted). Then if a {@link #setParent(ICache) parent}
|
|
|
148 |
* has been set, use it. Otherwise the key is not in cache so return a CacheResult of state
|
|
|
149 |
* {@link State#NOT_IN_CACHE}.
|
17 |
ilm |
150 |
*
|
|
|
151 |
* @param sel the key we're getting the value for.
|
|
|
152 |
* @return a CacheResult with the appropriate state.
|
|
|
153 |
*/
|
|
|
154 |
public final CacheResult<V> get(K sel) {
|
73 |
ilm |
155 |
return this.get(sel, true);
|
|
|
156 |
}
|
|
|
157 |
|
|
|
158 |
private final CacheResult<V> get(K sel, final boolean checkRunning) {
|
132 |
ilm |
159 |
ICache<K, V, D> parent = null;
|
17 |
ilm |
160 |
synchronized (this) {
|
132 |
ilm |
161 |
final CacheResult<V> localRes = this.cache.containsKey(sel) ? this.cache.get(sel).getResult() : CacheResult.<V> getNotInCache();
|
|
|
162 |
if (localRes.getState() == State.VALID) {
|
17 |
ilm |
163 |
log("IN cache", sel);
|
132 |
ilm |
164 |
return localRes;
|
73 |
ilm |
165 |
} else if (checkRunning && isRunning(sel)) {
|
17 |
ilm |
166 |
log("RUNNING", sel);
|
|
|
167 |
try {
|
|
|
168 |
this.wait();
|
|
|
169 |
} catch (InterruptedException e) {
|
|
|
170 |
// return sinon thread ne peut sortir que lorsque sel sera fini
|
|
|
171 |
return CacheResult.getInterrupted();
|
|
|
172 |
}
|
|
|
173 |
return this.get(sel);
|
73 |
ilm |
174 |
} else if (this.parent != null) {
|
|
|
175 |
log("CALLING parent", sel);
|
132 |
ilm |
176 |
parent = this.parent;
|
17 |
ilm |
177 |
} else {
|
|
|
178 |
log("NOT in cache", sel);
|
|
|
179 |
return CacheResult.getNotInCache();
|
|
|
180 |
}
|
|
|
181 |
}
|
132 |
ilm |
182 |
// don't call our parent with our lock
|
|
|
183 |
return parent.get(sel, false);
|
17 |
ilm |
184 |
}
|
|
|
185 |
|
|
|
186 |
/**
|
|
|
187 |
* Tell this cache that we're in process of getting the value for key, so if someone else ask
|
132 |
ilm |
188 |
* have them wait. ATTN after calling this method you MUST call put() or removeRunning(),
|
|
|
189 |
* otherwise get() will always block for <code>key</code>.
|
17 |
ilm |
190 |
*
|
132 |
ilm |
191 |
* @param val the value that will receive the result.
|
|
|
192 |
* @return <code>true</code> if the value was added, <code>false</code> if the key was already
|
|
|
193 |
* running.
|
17 |
ilm |
194 |
* @see #put(Object, Object, Set)
|
132 |
ilm |
195 |
* @see #removeRunning(Object)
|
17 |
ilm |
196 |
*/
|
132 |
ilm |
197 |
private final synchronized boolean addRunning(final CacheItem<K, V, D> val) {
|
|
|
198 |
if (!this.isRunning(val.getKey())) {
|
|
|
199 |
// ATTN this can invalidate val
|
|
|
200 |
val.addToWatchers();
|
|
|
201 |
if (val.getRemovalType() == null) {
|
|
|
202 |
this.running.put(val.getKey(), val);
|
|
|
203 |
return true;
|
|
|
204 |
}
|
|
|
205 |
}
|
|
|
206 |
return false;
|
17 |
ilm |
207 |
}
|
|
|
208 |
|
132 |
ilm |
209 |
// return null if the item wasn't added to this
|
|
|
210 |
final CacheItem<K, V, D> getRunningValFromRes(final CacheResult<V> cacheRes) {
|
|
|
211 |
if (cacheRes.getState() != CacheResult.State.NOT_IN_CACHE)
|
|
|
212 |
throw new IllegalArgumentException("Wrong state : " + cacheRes.getState());
|
|
|
213 |
if (cacheRes.getVal() == null) {
|
|
|
214 |
// happens when check() is called and ICacheSupport is dead, i.e. this.running was not
|
|
|
215 |
// modified and CacheResult.getNotInCache() was returned
|
|
|
216 |
assert cacheRes == CacheResult.getNotInCache();
|
|
|
217 |
} else {
|
|
|
218 |
if (cacheRes.getVal().getCache() != this)
|
|
|
219 |
throw new IllegalArgumentException("Not running in this cache");
|
|
|
220 |
assert cacheRes.getVal().getState() == CacheItem.State.RUNNING || cacheRes.getVal().getState() == CacheItem.State.INVALID;
|
|
|
221 |
}
|
|
|
222 |
@SuppressWarnings("unchecked")
|
|
|
223 |
final CacheItem<K, V, D> res = (CacheItem<K, V, D>) cacheRes.getVal();
|
|
|
224 |
return res;
|
17 |
ilm |
225 |
}
|
|
|
226 |
|
132 |
ilm |
227 |
public final synchronized void removeRunning(final CacheResult<V> res) {
|
|
|
228 |
removeRunning(getRunningValFromRes(res));
|
|
|
229 |
}
|
|
|
230 |
|
182 |
ilm |
231 |
private final synchronized boolean removeRunning(final CacheItem<K, V, D> val) {
|
132 |
ilm |
232 |
if (val == null)
|
182 |
ilm |
233 |
return false;
|
132 |
ilm |
234 |
final K key = val.getKey();
|
182 |
ilm |
235 |
final boolean removed;
|
|
|
236 |
if (this.running.get(key) == val) {
|
132 |
ilm |
237 |
this.removeRunning(key);
|
182 |
ilm |
238 |
removed = true;
|
|
|
239 |
} else {
|
132 |
ilm |
240 |
// either val wasn't created in this cache or another value was already put in this
|
|
|
241 |
// cache
|
182 |
ilm |
242 |
removed = val.setRemovalType(RemovalType.EXPLICIT);
|
|
|
243 |
}
|
132 |
ilm |
244 |
assert val.getRemovalType() != null;
|
182 |
ilm |
245 |
return removed;
|
132 |
ilm |
246 |
}
|
|
|
247 |
|
|
|
248 |
private final synchronized void removeRunning(K key) {
|
|
|
249 |
final CacheItem<K, V, D> removed = this.running.remove(key);
|
|
|
250 |
if (removed != null) {
|
|
|
251 |
// if the removed value isn't in us (this happens if put() is called without passing the
|
|
|
252 |
// value returned by check()), kill it so that it stops listening to its data
|
|
|
253 |
if (this.cache.get(key) != removed)
|
|
|
254 |
removed.setRemovalType(RemovalType.EXPLICIT);
|
|
|
255 |
this.notifyAll();
|
|
|
256 |
}
|
|
|
257 |
}
|
|
|
258 |
|
17 |
ilm |
259 |
public final synchronized boolean isRunning(K sel) {
|
132 |
ilm |
260 |
return this.running.containsKey(sel);
|
17 |
ilm |
261 |
}
|
|
|
262 |
|
132 |
ilm |
263 |
public final synchronized Set<K> getRunning() {
|
|
|
264 |
return Collections.unmodifiableSet(new HashSet<K>(this.running.keySet()));
|
|
|
265 |
}
|
|
|
266 |
|
17 |
ilm |
267 |
/**
|
|
|
268 |
* Check if key is in cache, in that case returns the value otherwise adds key to running and
|
|
|
269 |
* returns <code>NOT_IN_CACHE</code>.
|
|
|
270 |
*
|
|
|
271 |
* @param key the key to be checked.
|
132 |
ilm |
272 |
* @return the associated value, never <code>null</code>.
|
17 |
ilm |
273 |
* @see #addRunning(Object)
|
132 |
ilm |
274 |
* @see #removeRunning(CacheResult)
|
|
|
275 |
* @see #put(CacheResult, Object, long)
|
17 |
ilm |
276 |
*/
|
132 |
ilm |
277 |
public final CacheResult<V> check(K key) {
|
|
|
278 |
return this.check(key, Collections.<D> emptySet());
|
|
|
279 |
}
|
|
|
280 |
|
|
|
281 |
public final CacheResult<V> check(K key, final Set<? extends D> data) {
|
|
|
282 |
return this.check(key, true, true, data);
|
|
|
283 |
}
|
|
|
284 |
|
|
|
285 |
public final CacheResult<V> check(K key, final boolean readCache, final boolean willWriteToCache, final Set<? extends D> data) {
|
|
|
286 |
return this.check(key, readCache, willWriteToCache, data, this.delay * 1000);
|
|
|
287 |
}
|
|
|
288 |
|
|
|
289 |
public final synchronized CacheResult<V> check(K key, final boolean readCache, final boolean willWriteToCache, final Set<? extends D> data, final long timeout) {
|
|
|
290 |
final CacheResult<V> l = readCache ? this.get(key) : CacheResult.<V> getNotInCache();
|
|
|
291 |
if (willWriteToCache && l.getState() == State.NOT_IN_CACHE) {
|
|
|
292 |
final CacheItem<K, V, D> val = new CacheItem<K, V, D>(this, key, data);
|
|
|
293 |
if (this.addRunning(val)) {
|
|
|
294 |
val.addTimeout(timeout, TimeUnit.MILLISECONDS);
|
|
|
295 |
return new CacheResult<V>(val);
|
|
|
296 |
} else {
|
|
|
297 |
// val was never referenced so it will be garbage collected
|
|
|
298 |
assert !val.getState().isActive() : "active value : " + val;
|
|
|
299 |
}
|
|
|
300 |
}
|
17 |
ilm |
301 |
return l;
|
|
|
302 |
}
|
|
|
303 |
|
|
|
304 |
/**
|
|
|
305 |
* Put a result which doesn't depend on variable data in this cache.
|
|
|
306 |
*
|
|
|
307 |
* @param sel the key.
|
|
|
308 |
* @param res the result associated with <code>sel</code>.
|
132 |
ilm |
309 |
* @return the item that was created.
|
17 |
ilm |
310 |
*/
|
132 |
ilm |
311 |
public final CacheItem<K, V, D> put(K sel, V res) {
|
|
|
312 |
return this.put(sel, res, Collections.<D> emptySet());
|
17 |
ilm |
313 |
}
|
|
|
314 |
|
|
|
315 |
/**
|
|
|
316 |
* Put a result in this cache.
|
|
|
317 |
*
|
|
|
318 |
* @param sel the key.
|
|
|
319 |
* @param res the result associated with <code>sel</code>.
|
|
|
320 |
* @param data the data from which <code>res</code> is computed.
|
132 |
ilm |
321 |
* @return the item that was created.
|
17 |
ilm |
322 |
*/
|
132 |
ilm |
323 |
public final CacheItem<K, V, D> put(K sel, V res, Set<? extends D> data) {
|
|
|
324 |
return this.put(sel, res, data, this.delay * 1000);
|
|
|
325 |
}
|
17 |
ilm |
326 |
|
132 |
ilm |
327 |
public final CacheItem<K, V, D> put(K sel, V res, Set<? extends D> data, final long timeoutDelay) {
|
|
|
328 |
return this.put(sel, true, res, data, timeoutDelay);
|
|
|
329 |
}
|
|
|
330 |
|
|
|
331 |
private final CacheItem<K, V, D> put(K key, final boolean allowReplace, V res, Set<? extends D> data, final long timeoutDelay) {
|
|
|
332 |
final CacheItem<K, V, D> item = new CacheItem<K, V, D>(this, key, res, data);
|
|
|
333 |
item.addTimeout(timeoutDelay, TimeUnit.MILLISECONDS);
|
|
|
334 |
item.addToWatchers();
|
|
|
335 |
return put(item, allowReplace);
|
|
|
336 |
}
|
|
|
337 |
|
|
|
338 |
/**
|
|
|
339 |
* Assign a value to a {@link CacheItem.State#RUNNING} item.
|
|
|
340 |
*
|
|
|
341 |
* @param cacheRes an instance obtained from <code>check()</code>.
|
|
|
342 |
* @param val the value to store.
|
|
|
343 |
* @return the item that was added, <code>null</code> if none was added.
|
|
|
344 |
* @see #check(Object, boolean, boolean, Set)
|
|
|
345 |
*/
|
|
|
346 |
public final CacheItem<K, V, D> put(CacheResult<V> cacheRes, V val) {
|
|
|
347 |
final CacheItem<K, V, D> item = getRunningValFromRes(cacheRes);
|
|
|
348 |
if (item == null)
|
|
|
349 |
return null;
|
|
|
350 |
item.setValue(val);
|
|
|
351 |
return put(item, true);
|
|
|
352 |
}
|
|
|
353 |
|
|
|
354 |
private final CacheItem<K, V, D> put(final CacheItem<K, V, D> val, final boolean allowReplace) {
|
|
|
355 |
final K sel = val.getKey();
|
|
|
356 |
synchronized (this) {
|
|
|
357 |
final CacheItem.State valState = val.getState();
|
|
|
358 |
if (!valState.isActive())
|
|
|
359 |
return null;
|
|
|
360 |
else if (valState != CacheItem.State.VALID)
|
|
|
361 |
throw new IllegalStateException("Non valid : " + val);
|
|
|
362 |
final boolean replacing = this.cache.containsKey(sel) && this.cache.get(sel).getRemovalType() == null;
|
|
|
363 |
if (!allowReplace && replacing)
|
|
|
364 |
return null;
|
|
|
365 |
|
|
|
366 |
if (!replacing && this.size > 0 && this.cache.size() == this.size)
|
|
|
367 |
this.cache.values().iterator().next().setRemovalType(RemovalType.SIZE_LIMIT);
|
|
|
368 |
final CacheItem<K, V, D> prev = this.cache.put(sel, val);
|
|
|
369 |
if (replacing)
|
|
|
370 |
prev.setRemovalType(RemovalType.DATA_CHANGE);
|
|
|
371 |
assert this.size <= 0 || this.cache.size() <= this.size;
|
|
|
372 |
this.removeRunning(sel);
|
17 |
ilm |
373 |
}
|
144 |
ilm |
374 |
this.propSupp.firePropertyChange(new Event<K, V, D>(this, ITEMS_CHANGED, null, null));
|
|
|
375 |
this.propSupp.firePropertyChange(this.createItemEvent(ITEM_ADDED, null, val));
|
132 |
ilm |
376 |
return val;
|
|
|
377 |
}
|
17 |
ilm |
378 |
|
132 |
ilm |
379 |
/**
|
|
|
380 |
* Get the remaining time before the passed key will be removed.
|
|
|
381 |
*
|
|
|
382 |
* @param key the key.
|
|
|
383 |
* @return the remaining milliseconds before the removal, negative if the passed key isn't in
|
|
|
384 |
* this.
|
|
|
385 |
* @see #getRemovalTime(Object)
|
|
|
386 |
*/
|
|
|
387 |
public final long getRemainingTime(K key) {
|
|
|
388 |
final CacheItem<K, V, D> val;
|
|
|
389 |
synchronized (this) {
|
|
|
390 |
val = this.cache.get(key);
|
|
|
391 |
}
|
|
|
392 |
if (val == null)
|
|
|
393 |
return -1;
|
|
|
394 |
return val.getRemainingTimeoutDelay();
|
|
|
395 |
}
|
17 |
ilm |
396 |
|
132 |
ilm |
397 |
public final void putAll(final ICache<K, V, D> otherCache, final boolean allowReplace) {
|
|
|
398 |
if (otherCache == this)
|
|
|
399 |
return;
|
|
|
400 |
if (otherCache.getSupp() != this.getSupp())
|
|
|
401 |
Log.get().warning("Since both caches don't share watchers, some early events might not be notified to this cache");
|
|
|
402 |
final List<CacheItem<K, V, D>> oItems = new ArrayList<CacheItem<K, V, D>>();
|
|
|
403 |
synchronized (otherCache) {
|
|
|
404 |
oItems.addAll(otherCache.cache.values());
|
|
|
405 |
}
|
|
|
406 |
for (final CacheItem<K, V, D> oItem : oItems) {
|
|
|
407 |
final CacheItem<K, V, D> newItem = this.put(oItem.getKey(), allowReplace, oItem.getValue(), oItem.getData(), oItem.getRemainingTimeoutDelay());
|
|
|
408 |
// if oItem was changed before newItem was created or see CacheWatcher.dataChanged() :
|
|
|
409 |
// 1. if newItem was added to a watcher before the first synchronized block, it will be
|
|
|
410 |
// notified
|
|
|
411 |
// 2. if newItem was added between the synchronized blocks (during the first iteration)
|
|
|
412 |
// it will be notified by the second iteration
|
|
|
413 |
// 3. if newItem was added after the second synchronized block, oItem will already be
|
|
|
414 |
// notified
|
|
|
415 |
if (newItem != null && oItem.getRemovalType() == RemovalType.DATA_CHANGE) {
|
|
|
416 |
newItem.setRemovalType(oItem.getRemovalType());
|
|
|
417 |
}
|
|
|
418 |
}
|
17 |
ilm |
419 |
}
|
|
|
420 |
|
132 |
ilm |
421 |
public final ICache<K, V, D> copy(final String name, final boolean copyItems) {
|
|
|
422 |
final ICache<K, V, D> res = new ICache<K, V, D>(this.getSupp(), this.delay, this.getMaximumSize(), name);
|
|
|
423 |
if (copyItems)
|
|
|
424 |
res.putAll(this, false);
|
|
|
425 |
return res;
|
|
|
426 |
}
|
|
|
427 |
|
17 |
ilm |
428 |
public final synchronized void clear(K select) {
|
|
|
429 |
log("clear", select);
|
|
|
430 |
if (this.cache.containsKey(select)) {
|
132 |
ilm |
431 |
this.cache.get(select).setRemovalType(RemovalType.EXPLICIT);
|
17 |
ilm |
432 |
}
|
|
|
433 |
}
|
|
|
434 |
|
132 |
ilm |
435 |
final boolean clear(final CacheItem<K, V, D> val) {
|
|
|
436 |
if (val.getRemovalType() == null)
|
|
|
437 |
throw new IllegalStateException("Not yet removed : " + val);
|
182 |
ilm |
438 |
final boolean removedFromRunning, toBeRemoved;
|
132 |
ilm |
439 |
synchronized (this) {
|
|
|
440 |
log("clear", val);
|
182 |
ilm |
441 |
removedFromRunning = this.removeRunning(val);
|
132 |
ilm |
442 |
toBeRemoved = this.cache.get(val.getKey()) == val;
|
|
|
443 |
if (toBeRemoved) {
|
|
|
444 |
this.cache.remove(val.getKey());
|
17 |
ilm |
445 |
}
|
|
|
446 |
}
|
144 |
ilm |
447 |
// NOTE these events are often fired with our monitor since this method is called with it
|
182 |
ilm |
448 |
if (removedFromRunning || toBeRemoved) {
|
|
|
449 |
this.propSupp.firePropertyChange(new Event<K, V, D>(this, ITEMS_CHANGED, null, null));
|
|
|
450 |
this.propSupp.firePropertyChange(this.createItemEvent(ITEM_REMOVED, val, null));
|
|
|
451 |
}
|
132 |
ilm |
452 |
return toBeRemoved;
|
17 |
ilm |
453 |
}
|
|
|
454 |
|
132 |
ilm |
455 |
public final synchronized void clear() {
|
182 |
ilm |
456 |
for (final CacheItem<K, V, D> val : new ArrayList<CacheItem<K, V, D>>(this.cache.values())) {
|
|
|
457 |
// We have our monitor so if val is still in us but setRemovalType() was already called,
|
|
|
458 |
// then it means another thread is waiting on our monitor in clear(CacheItem). In that
|
|
|
459 |
// case, just call it now so that this is empty at the end of this method.
|
|
|
460 |
if (!val.setRemovalType(RemovalType.EXPLICIT)) {
|
|
|
461 |
final boolean removed = this.clear(val);
|
|
|
462 |
assert removed;
|
|
|
463 |
}
|
|
|
464 |
}
|
|
|
465 |
assert this.size() == 0 : this + " expected to be empty but contains : " + this.cache.keySet();
|
|
|
466 |
|
132 |
ilm |
467 |
for (final CacheItem<K, V, D> val : new ArrayList<CacheItem<K, V, D>>(this.running.values()))
|
|
|
468 |
val.setRemovalType(RemovalType.EXPLICIT);
|
182 |
ilm |
469 |
assert this.running.size() == 0 : this + " expected to have no running but contains : " + this.running.keySet();
|
17 |
ilm |
470 |
}
|
|
|
471 |
|
|
|
472 |
private final void log(String msg, Object subject) {
|
|
|
473 |
// do the toString() on subject only if necessary
|
|
|
474 |
if (Log.get().isLoggable(LEVEL))
|
|
|
475 |
Log.get().log(LEVEL, msg + ": " + subject);
|
|
|
476 |
}
|
|
|
477 |
|
|
|
478 |
public final synchronized int size() {
|
|
|
479 |
return this.cache.size();
|
|
|
480 |
}
|
|
|
481 |
|
144 |
ilm |
482 |
static public class Event<K, V, D> extends PropertyChangeEvent {
|
|
|
483 |
|
|
|
484 |
public Event(ICache<K, V, D> source, String propertyName, Object oldValue, Object newValue) {
|
|
|
485 |
super(source, propertyName, oldValue, newValue);
|
|
|
486 |
}
|
|
|
487 |
|
|
|
488 |
@SuppressWarnings("unchecked")
|
|
|
489 |
@Override
|
|
|
490 |
public ICache<K, V, D> getSource() {
|
|
|
491 |
return (ICache<K, V, D>) super.getSource();
|
|
|
492 |
}
|
|
|
493 |
}
|
|
|
494 |
|
|
|
495 |
private final Event<K, V, D> castEvent(final PropertyChangeEvent evt) {
|
|
|
496 |
final Event<?, ?, ?> casted = (Event<?, ?, ?>) evt;
|
|
|
497 |
// Needed sine this method can be called from outside this class
|
|
|
498 |
// Only other option is to not use PropertyChangeSupport, and create our own generics-aware
|
|
|
499 |
// version
|
|
|
500 |
if (casted.getSource() != ICache.this)
|
|
|
501 |
throw new IllegalArgumentException("Cannot uphold type safety");
|
|
|
502 |
@SuppressWarnings("unchecked")
|
|
|
503 |
final Event<K, V, D> res = (Event<K, V, D>) casted;
|
|
|
504 |
return res;
|
|
|
505 |
}
|
|
|
506 |
|
|
|
507 |
private ItemEvent<K, V, D> createItemEvent(String propertyName, CacheItem<K, V, D> oldValue, CacheItem<K, V, D> newValue) {
|
|
|
508 |
if (oldValue == null && newValue == null)
|
|
|
509 |
throw new IllegalArgumentException("No values");
|
|
|
510 |
assert (oldValue == null || oldValue.getCache() == this);
|
|
|
511 |
assert (newValue == null || newValue.getCache() == this);
|
|
|
512 |
return new ItemEvent<K, V, D>(this, propertyName, oldValue, newValue);
|
|
|
513 |
}
|
|
|
514 |
|
|
|
515 |
static public class ItemEvent<K, V, D> extends Event<K, V, D> {
|
|
|
516 |
|
|
|
517 |
// ATTN doesn't check parameters
|
|
|
518 |
private ItemEvent(ICache<K, V, D> source, String propertyName, CacheItem<K, V, D> oldValue, CacheItem<K, V, D> newValue) {
|
|
|
519 |
super(source, propertyName, oldValue, newValue);
|
|
|
520 |
}
|
|
|
521 |
|
|
|
522 |
@SuppressWarnings("unchecked")
|
|
|
523 |
@Override
|
|
|
524 |
public CacheItem<K, V, D> getOldValue() {
|
|
|
525 |
return (CacheItem<K, V, D>) super.getOldValue();
|
|
|
526 |
}
|
|
|
527 |
|
|
|
528 |
@SuppressWarnings("unchecked")
|
|
|
529 |
@Override
|
|
|
530 |
public CacheItem<K, V, D> getNewValue() {
|
|
|
531 |
return (CacheItem<K, V, D>) super.getNewValue();
|
|
|
532 |
}
|
|
|
533 |
}
|
|
|
534 |
|
|
|
535 |
public final PropertyChangeListener addItemListener(final IClosure<? super ItemEvent<K, V, D>> listener) {
|
|
|
536 |
return addListener(new PropertyChangeListener() {
|
|
|
537 |
@Override
|
|
|
538 |
public void propertyChange(PropertyChangeEvent evt) {
|
|
|
539 |
if (evt instanceof ItemEvent)
|
|
|
540 |
listener.executeChecked((ItemEvent<K, V, D>) castEvent(evt));
|
|
|
541 |
}
|
|
|
542 |
});
|
|
|
543 |
}
|
|
|
544 |
|
|
|
545 |
public final PropertyChangeListener addListener(final IClosure<? super Event<K, V, D>> listener) {
|
|
|
546 |
return addListener(new PropertyChangeListener() {
|
|
|
547 |
@Override
|
|
|
548 |
public void propertyChange(PropertyChangeEvent evt) {
|
|
|
549 |
listener.executeChecked(castEvent(evt));
|
|
|
550 |
}
|
|
|
551 |
});
|
|
|
552 |
}
|
|
|
553 |
|
|
|
554 |
public final PropertyChangeListener addListener(final PropertyChangeListener listener) {
|
|
|
555 |
this.propSupp.addPropertyChangeListener(listener);
|
|
|
556 |
return listener;
|
|
|
557 |
}
|
|
|
558 |
|
|
|
559 |
public final void removeListener(final PropertyChangeListener listener) {
|
|
|
560 |
this.propSupp.removePropertyChangeListener(listener);
|
|
|
561 |
}
|
|
|
562 |
|
132 |
ilm |
563 |
@Override
|
17 |
ilm |
564 |
public final String toString() {
|
132 |
ilm |
565 |
return this.toString(false);
|
17 |
ilm |
566 |
}
|
132 |
ilm |
567 |
|
|
|
568 |
public final String toString(final boolean withKeys) {
|
|
|
569 |
final String keys;
|
|
|
570 |
if (withKeys) {
|
|
|
571 |
synchronized (this) {
|
|
|
572 |
keys = ", keys cached: " + this.cache.keySet().toString();
|
|
|
573 |
}
|
|
|
574 |
} else {
|
|
|
575 |
keys = "";
|
|
|
576 |
}
|
|
|
577 |
return this.getClass().getName() + " '" + this.getName() + "'" + keys;
|
|
|
578 |
}
|
17 |
ilm |
579 |
}
|