OpenConcerto

Dépôt officiel du code source de l'ERP OpenConcerto
sonarqube

svn://code.openconcerto.org/openconcerto

Rev

Rev 144 | Go to most recent revision | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
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
}