001: /* ***** BEGIN LICENSE BLOCK *****
002: * Version: MPL 1.1
003: * The contents of this file are subject to the Mozilla Public License Version
004: * 1.1 (the "License"); you may not use this file except in compliance with
005: * the License. You may obtain a copy of the License at
006: * http://www.mozilla.org/MPL/
007: *
008: * Software distributed under the License is distributed on an "AS IS" basis,
009: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
010: * for the specific language governing rights and limitations under the
011: * License.
012: *
013: * The Original Code is Riot.
014: *
015: * The Initial Developer of the Original Code is
016: * Neteye GmbH.
017: * Portions created by the Initial Developer are Copyright (C) 2006
018: * the Initial Developer. All Rights Reserved.
019: *
020: * Contributor(s):
021: * Felix Gnass [fgnass at neteye dot de]
022: *
023: * ***** END LICENSE BLOCK ***** */
024: package org.riotfamily.cachius;
025:
026: import java.io.File;
027: import java.io.IOException;
028: import java.io.ObjectInputStream;
029: import java.io.Serializable;
030: import java.util.ArrayList;
031: import java.util.Collections;
032: import java.util.Comparator;
033: import java.util.HashSet;
034: import java.util.Iterator;
035: import java.util.List;
036: import java.util.Map;
037: import java.util.Set;
038:
039: import org.apache.commons.logging.Log;
040: import org.apache.commons.logging.LogFactory;
041: import org.riotfamily.common.io.IOUtils;
042: import org.springframework.core.CollectionFactory;
043:
044: /**
045: * The Cachius cache.
046: *
047: * @author Felix Gnass
048: */
049: public final class Cache implements Serializable {
050:
051: private static Log log = LogFactory.getLog(Cache.class);
052:
053: private static Comparator itemUsageComparator = new ItemUsageComparator();
054:
055: private int size;
056:
057: private Map map;
058:
059: private Map taggedItems;
060:
061: private File cacheDir = null;
062:
063: private int currentDir = 0;
064:
065: private double evictionFactor = 0.2;
066:
067: private boolean enabled = true;
068:
069: private transient int capacity;
070:
071: private transient File[] dirs = null;
072:
073: private transient int numberOfDirs;
074:
075: private transient Object addItemlock;
076:
077: private transient CleanUpThread cleanUpThread;
078:
079: /**
080: * Create the cache.
081: */
082: public Cache(File cacheDir, int capacity, boolean enabled) {
083: this .enabled = enabled;
084:
085: int mapCapacity = (int) (capacity / 0.75f) + 1;
086: if (mapCapacity % 2 == 0) {
087: mapCapacity++;
088: }
089:
090: this .map = CollectionFactory
091: .createConcurrentMapIfPossible(mapCapacity);
092: this .taggedItems = CollectionFactory
093: .createConcurrentMapIfPossible(mapCapacity);
094:
095: init();
096: setCapacity(capacity);
097: setCacheDir(cacheDir);
098: }
099:
100: private void readObject(ObjectInputStream in)
101: throws ClassNotFoundException, IOException {
102:
103: in.defaultReadObject();
104: init();
105: }
106:
107: /**
108: * Initializes the instance upon creation or deserialization.
109: */
110: private void init() {
111: addItemlock = new Object();
112: cleanUpThread = new CleanUpThread();
113: cleanUpThread.start();
114: }
115:
116: /**
117: * Sets the directory where the cache items are stored. If a different
118: * directory is already set, the old directory is emptied.
119: *
120: * The method is not thread-safe and therefore protected.
121: */
122: protected final void setCacheDir(File cacheDir) {
123: if (this .cacheDir != null) {
124: clear();
125: }
126: this .cacheDir = cacheDir;
127: }
128:
129: /**
130: * Sets the cache capacity.
131: *
132: * The method is not thread-safe and therefore protected.
133: */
134: protected final void setCapacity(int capacity) {
135: this .capacity = capacity;
136: numberOfDirs = capacity / 1000;
137: dirs = new File[numberOfDirs];
138: }
139:
140: private void clear() {
141: log.info("Removing all cache entries ...");
142: map.clear();
143: taggedItems.clear();
144: size = 0;
145: IOUtils.clearDirectory(cacheDir);
146: }
147:
148: public void setEnabled(boolean enabled) {
149: this .enabled = enabled;
150: }
151:
152: /**
153: * Returns whether an item with the given key exists.
154: */
155: public boolean containsKey(String key) {
156: return map.containsKey(key);
157: }
158:
159: /**
160: * Returns the CacheItem with the given key or creates a new one, if no
161: * entry with that key exists.
162: *
163: * @param key The cache key
164: * @return The CacheItem for the given key
165: */
166: public CacheItem getItem(String key) {
167: if (!enabled) {
168: return null;
169: }
170: CacheItem item = (CacheItem) map.get(key);
171: if (item == null) {
172: synchronized (addItemlock) {
173: item = (CacheItem) map.get(key);
174: if (item == null) {
175: try {
176: item = new CacheItem(key, getNextDir());
177: map.put(key, item);
178: size++;
179: } catch (Exception e) {
180: log.error("Error creating CacheItem", e);
181: return null;
182: }
183: }
184: }
185: checkCapacity();
186: }
187: item.touch();
188: return item;
189: }
190:
191: /**
192: * Returns the directory where the next cache item should be created.
193: * Note: Access to this method must be synchronized externally.
194: */
195: private File getNextDir() {
196: cacheDir.mkdirs();
197: if (numberOfDirs <= 1) {
198: return cacheDir;
199: }
200: if (currentDir >= numberOfDirs) {
201: currentDir = 0;
202: }
203: File dir = dirs[currentDir];
204: if (dir == null) {
205: dir = new File(cacheDir, String.valueOf(currentDir));
206: dir.mkdir();
207: dirs[currentDir] = dir;
208: }
209: currentDir++;
210: return dir;
211: }
212:
213: /**
214: * Notifies the clean-up thread when the capacity is exceeded.
215: */
216: private void checkCapacity() {
217: if (size >= capacity) {
218: synchronized (cleanUpThread) {
219: cleanUpThread.notify();
220: }
221: }
222: }
223:
224: /**
225: * Removes items from the cache that havn't been used for a long time.
226: * The number of items removed is <code>capacity * evictionFactor</code>.
227: */
228: private void cleanup() {
229: log.info("Cache capacity exceeded. Performing cleanup ...");
230: ArrayList items = new ArrayList(map.values());
231: Collections.sort(items, itemUsageComparator);
232: int i = (int) Math.ceil(capacity * evictionFactor);
233: Iterator it = items.iterator();
234: while (it.hasNext() && i > 0) {
235: CacheItem item = (CacheItem) it.next();
236: removeItem(item);
237: i--;
238: }
239: }
240:
241: /**
242: * Removes the given item from the cache.
243: */
244: private void removeItem(CacheItem item) {
245: synchronized (addItemlock) {
246: map.remove(item.getKey());
247: size--;
248: removeTags(item, item.getTags());
249: item.delete();
250: }
251: }
252:
253: /**
254: * Returns a list of items tagged with the given String.
255: */
256: private List getTaggedItems(String tag) {
257: return (List) taggedItems.get(tag);
258: }
259:
260: /**
261: * Removes the given item from all specified tag lists. If item is the last
262: * entry in a tag-list, the whole list is removed from the taggedItems map.
263: */
264: private void removeTags(CacheItem item, Set tags) {
265: if (tags != null) {
266: Iterator it = tags.iterator();
267: while (it.hasNext()) {
268: String tag = (String) it.next();
269: List items = getTaggedItems(tag);
270: if (items != null) {
271: items.remove(item);
272: if (items.isEmpty()) {
273: taggedItems.remove(tag);
274: }
275: }
276: }
277: }
278: }
279:
280: /**
281: * Adds the given item to the specified tag lists.
282: */
283: private void addTags(CacheItem item, Set tags) {
284: if (tags != null) {
285: Iterator it = tags.iterator();
286: while (it.hasNext()) {
287: String tag = (String) it.next();
288: List items = getTaggedItems(tag);
289: if (items == null) {
290: items = new ArrayList();
291: taggedItems.put(tag, items);
292: }
293: items.add(item);
294: }
295: }
296: }
297:
298: /**
299: * Updates the tags of the given CacheItem.
300: */
301: public void tagItem(CacheItem item, Set newTags) {
302: Set tagsToRemove = new HashSet();
303: Set existingTags = item.getTags();
304: if (existingTags != null) {
305: tagsToRemove.addAll(existingTags);
306: }
307: if (newTags != null) {
308: Set tagsToAdd = new HashSet();
309: Iterator it = newTags.iterator();
310: while (it.hasNext()) {
311: String tag = (String) it.next();
312: if (!tagsToRemove.remove(tag)) {
313: tagsToAdd.add(tag);
314: }
315: }
316: addTags(item, tagsToAdd);
317: }
318: removeTags(item, tagsToRemove);
319: item.setTags(newTags);
320: }
321:
322: /**
323: * Invalidates all items tagged with the given String.
324: */
325: public void invalidateTaggedItems(String tag) {
326: if (tag != null) {
327: log.debug("Invalidating items tagged as " + tag);
328: List items = getTaggedItems(tag);
329: if (items != null) {
330: Iterator it = items.iterator();
331: while (it.hasNext()) {
332: CacheItem item = (CacheItem) it.next();
333: item.invalidate();
334: }
335: }
336: }
337: }
338:
339: /**
340: * Comparator that compares items by their {@link CacheItem#getLastUsed()
341: * last-used} date.
342: */
343: private static class ItemUsageComparator implements Comparator {
344: public int compare(Object obj1, Object obj2) {
345: CacheItem item1 = (CacheItem) obj1;
346: CacheItem item2 = (CacheItem) obj2;
347: long l1 = item1.getLastUsed();
348: long l2 = item2.getLastUsed();
349: return (l1 < l2 ? -1 : (l1 == l2 ? 0 : 1));
350: }
351: }
352:
353: public void shutdown() {
354: cleanUpThread.shutdown();
355: }
356:
357: /**
358: * Thread that performs a clean-up upon notification.
359: */
360: private class CleanUpThread extends Thread {
361:
362: private Log log = LogFactory.getLog(CleanUpThread.class);
363:
364: private boolean running = true;
365:
366: public void run() {
367: while (running) {
368: synchronized (this ) {
369: try {
370: wait();
371: } catch (InterruptedException e) {
372: log.info("CleanUpThread interrupted.");
373: break;
374: }
375: }
376: if (running) {
377: cleanup();
378: }
379: }
380: log.info("CleanUpThread finished.");
381: }
382:
383: public synchronized void shutdown() {
384: log.info("Stopping CleanUpThread ...");
385: running = false;
386: notify();
387: }
388: }
389:
390: }
|