/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008 jOpenDocument, by ILM Informatique. All rights reserved. * * The contents of this file are subject to the terms of the GNU * General Public License Version 3 only ("GPL"). * You may not use this file except in compliance with the License. * You can obtain a copy of the License at http://www.gnu.org/licenses/gpl-3.0.html * See the License for the specific language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each file. * */ package org.jopendocument.util.cache; import org.jopendocument.util.CollectionMap; import org.jopendocument.util.ExceptionUtils; import org.jopendocument.util.Log; import org.jopendocument.util.cache.CacheResult.State; import org.jopendocument.util.cc.Transformer; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.logging.Level; import org.apache.commons.collections.map.LazyMap; /** * To keep results computed from some data. The results will be automatically invalidated after some * period of time or when the data is modified. * * @author Sylvain CUAZ * @param <K> key type, eg String. * @param <V> value type, eg List of SQLRow. * @param <D> source data type, eg SQLTable. */ public class ICache<K, V, D> { private static final Level LEVEL = Level.FINEST; // linked to fifo private final LinkedHashMap<K, V> cache; private final Set<K> running; private final int delay; private final int size; // lazy initialization to avoid creating unnecessary threads private Timer timer; private final String name; private final Map<K, CacheTimeOut<K>> timeoutTasks; private Map<D, CacheWatcher<K, D>> watchers; private final CollectionMap<K, CacheWatcher<K, D>> watchersByKey; public ICache() { this(60); } public ICache(int delay) { this(delay, -1); } public ICache(int delay, int size) { this(delay, size, null); } /** * Creates a cache with the given parameters. * * @param delay the delay in seconds before a key is cleared. * @param size the maximum size of the cache, negative means no limit. * @param name name of this cache and associated thread. * @throws IllegalArgumentException if size is 0. */ public ICache(int delay, int size, String name) { this.running = new HashSet<K>(); this.delay = delay; if (size == 0) throw new IllegalArgumentException("0 size"); this.size = size; this.cache = new LinkedHashMap<K, V>(size < 0 ? 64 : size); this.timer = null; this.name = name; this.timeoutTasks = new HashMap<K, CacheTimeOut<K>>(); this.watchers = null; this.watchersByKey = new CollectionMap<K, CacheWatcher<K, D>>(HashSet.class); } private final Timer getTimer() { if (this.timer == null) this.timer = this.name == null ? new Timer(true) : new Timer("cache for " + this.name, true); return this.timer; } @SuppressWarnings("unchecked") public final void setWatcherFactory(final CacheWatcherFactory<K, D> f) { this.watchers = LazyMap.decorate(new HashMap(), new Transformer<D, CacheWatcher<K, D>>() { @Override public CacheWatcher<K, D> transformChecked(D input) { try { return f.createWatcher(ICache.this, input); } catch (Exception e) { throw ExceptionUtils.createExn(IllegalStateException.class, "could not create watcher for " + input, e); } } }); } /** * If <code>sel</code> is in cache returns its value, else if key is running block until the * key is put (or the current thread is interrupted). Otherwise the key is not in cache so * return a CacheResult of state {@link State#NOT_IN_CACHE}. * * @param sel the key we're getting the value for. * @return a CacheResult with the appropriate state. */ public final CacheResult<V> get(K sel) { synchronized (this) { if (this.cache.containsKey(sel)) { log("IN cache", sel); return new CacheResult<V>(this.cache.get(sel)); } else if (isRunning(sel)) { log("RUNNING", sel); try { this.wait(); } catch (InterruptedException e) { // return sinon thread ne peut sortir que lorsque sel sera fini return CacheResult.getInterrupted(); } return this.get(sel); } else { log("NOT in cache", sel); return CacheResult.getNotInCache(); } } } /** * Tell this cache that we're in process of getting the value for key, so if someone else ask * have them wait. ATTN after calling this method you MUST call put(), otherwise get() will * always block for key. * * @param key the key we're getting the value for. * @see #put(Object, Object, Set) */ public final synchronized void addRunning(K key) { this.running.add(key); } public final synchronized void removeRunning(K key) { this.running.remove(key); this.notifyAll(); } public final synchronized boolean isRunning(K sel) { return this.running.contains(sel); } /** * Check if key is in cache, in that case returns the value otherwise adds key to running and * returns <code>NOT_IN_CACHE</code>. * * @param key the key to be checked. * @return the associated value, or <code>null</code>. * @see #addRunning(Object) */ public final synchronized CacheResult<V> check(K key) { final CacheResult<V> l = this.get(key); if (l.getState() == State.NOT_IN_CACHE) this.addRunning(key); return l; } /** * Put a result which doesn't depend on variable data in this cache. * * @param sel the key. * @param res the result associated with <code>sel</code>. */ public final synchronized void put(K sel, V res) { this.put(sel, res, Collections.<D> emptySet()); } /** * Put a result in this cache. * * @param sel the key. * @param res the result associated with <code>sel</code>. * @param data the data from which <code>res</code> is computed. * @return the watchers monitoring the passed key. */ public final synchronized Set<? extends CacheWatcher<K, D>> put(K sel, V res, Set<? extends D> data) { if (this.size > 0 && this.cache.size() == this.size) this.clear(this.cache.keySet().iterator().next()); this.cache.put(sel, res); this.removeRunning(sel); for (final D datum : data) { if (this.watchers != null) { final CacheWatcher<K, D> watcher = this.watchers.get(datum); watcher.add(sel); this.watchersByKey.put(sel, watcher); } } final CacheTimeOut<K> timeout = new CacheTimeOut<K>(this, sel); this.timeoutTasks.put(sel, timeout); this.getTimer().schedule(timeout, this.delay * 1000); return (Set<CacheWatcher<K, D>>) this.watchersByKey.getNonNull(sel); } public final synchronized void clear(K select) { log("clear", select); if (this.cache.containsKey(select)) { this.cache.remove(select); this.timeoutTasks.remove(select).cancel(); final Set<CacheWatcher<K, D>> keyWatchers = (Set<CacheWatcher<K, D>>) this.watchersByKey.getNonNull(select); this.watchersByKey.remove(select); // a key can specify no watchers at all if (keyWatchers != null) { for (final CacheWatcher<K, D> w : keyWatchers) { w.remove(select); if (w.isEmpty()) { w.die(); this.watchers.remove(w.getData()); } } } } } final synchronized boolean dependsOn(D data) { return this.watchers.containsKey(data); } private final void log(String msg, Object subject) { // do the toString() on subject only if necessary if (Log.get().isLoggable(LEVEL)) Log.get().log(LEVEL, msg + ": " + subject); } public final synchronized int size() { return this.cache.size(); } public final String toString() { return this.getClass().getName() + ", keys cached: " + this.timeoutTasks.keySet(); } }