|
|||||||||||||||||||
Source file | Conditionals | Statements | Methods | TOTAL | |||||||||||||||
Cache.java | 74,2% | 81,7% | 80,5% | 79,1% |
|
1 | /* | |
2 | * Copyright (c) 2002-2003 by OpenSymphony | |
3 | * All rights reserved. | |
4 | */ | |
5 | package com.opensymphony.oscache.base; | |
6 | ||
7 | import com.opensymphony.oscache.base.algorithm.AbstractConcurrentReadCache; | |
8 | import com.opensymphony.oscache.base.algorithm.LRUCache; | |
9 | import com.opensymphony.oscache.base.algorithm.UnlimitedCache; | |
10 | import com.opensymphony.oscache.base.events.*; | |
11 | import com.opensymphony.oscache.base.persistence.PersistenceListener; | |
12 | import com.opensymphony.oscache.util.FastCronParser; | |
13 | ||
14 | import org.apache.commons.logging.Log; | |
15 | import org.apache.commons.logging.LogFactory; | |
16 | ||
17 | import java.io.Serializable; | |
18 | ||
19 | import java.text.ParseException; | |
20 | ||
21 | import java.util.*; | |
22 | ||
23 | import javax.swing.event.EventListenerList; | |
24 | ||
25 | /** | |
26 | * Provides an interface to the cache itself. Creating an instance of this class | |
27 | * will create a cache that behaves according to its construction parameters. | |
28 | * The public API provides methods to manage objects in the cache and configure | |
29 | * any cache event listeners. | |
30 | * | |
31 | * @version $Revision: 1.1 $ | |
32 | * @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a> | |
33 | * @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a> | |
34 | * @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a> | |
35 | * @author <a href="mailto:chris@swebtec.com">Chris Miller</a> | |
36 | */ | |
37 | public class Cache implements Serializable { | |
38 | /** | |
39 | * An event that origininated from within another event. | |
40 | */ | |
41 | public static final String NESTED_EVENT = "NESTED"; | |
42 | private static transient final Log log = LogFactory.getLog(Cache.class); | |
43 | ||
44 | /** | |
45 | * A list of all registered event listeners for this cache. | |
46 | */ | |
47 | protected EventListenerList listenerList = new EventListenerList(); | |
48 | ||
49 | /** | |
50 | * The actual cache map. This is where the cached objects are held. | |
51 | */ | |
52 | private AbstractConcurrentReadCache cacheMap = null; | |
53 | ||
54 | /** | |
55 | * Date of last complete cache flush. | |
56 | */ | |
57 | private Date flushDateTime = null; | |
58 | ||
59 | /** | |
60 | * A map that holds keys of cache entries that are currently being built, and EntryUpdateState instance as values. This is used to coordinate threads | |
61 | * that modify/access a same key in concurrence. | |
62 | * | |
63 | * The cache checks against this map when a stale entry is requested, or a cache miss is observed. | |
64 | * | |
65 | * If the requested key is in here, we know the entry is currently being | |
66 | * built by another thread and hence we can either block and wait or serve | |
67 | * the stale entry (depending on whether cache blocking is enabled or not). | |
68 | * <p> | |
69 | * To avoid data races, values in this map should remain present during the whole time distinct threads deal with the | |
70 | * same key. We implement this using explicit reference counting in the EntryUpdateState instance, to be able to clean up | |
71 | * the map once all threads have declared they are done accessing/updating a given key. | |
72 | * | |
73 | * It is not possible to locate this into the CacheEntry because this would require to have a CacheEntry instance for all cache misses, and | |
74 | * may therefore generate a memory leak. More over, the CacheEntry instance may not be hold in memory in the case no | |
75 | * memory cache is configured. | |
76 | */ | |
77 | private Map updateStates = new HashMap(); | |
78 | ||
79 | /** | |
80 | * Indicates whether the cache blocks requests until new content has | |
81 | * been generated or just serves stale content instead. | |
82 | */ | |
83 | private boolean blocking = false; | |
84 | ||
85 | /** | |
86 | * Create a new Cache | |
87 | * | |
88 | * @param useMemoryCaching Specify if the memory caching is going to be used | |
89 | * @param unlimitedDiskCache Specify if the disk caching is unlimited | |
90 | * @param overflowPersistence Specify if the persistent cache is used in overflow only mode | |
91 | */ | |
92 | 12 | public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence) { |
93 | 12 | this(useMemoryCaching, unlimitedDiskCache, overflowPersistence, false, null, 0); |
94 | } | |
95 | ||
96 | /** | |
97 | * Create a new Cache. | |
98 | * | |
99 | * If a valid algorithm class is specified, it will be used for this cache. | |
100 | * Otherwise if a capacity is specified, it will use LRUCache. | |
101 | * If no algorithm or capacity is specified UnlimitedCache is used. | |
102 | * | |
103 | * @see com.opensymphony.oscache.base.algorithm.LRUCache | |
104 | * @see com.opensymphony.oscache.base.algorithm.UnlimitedCache | |
105 | * @param useMemoryCaching Specify if the memory caching is going to be used | |
106 | * @param unlimitedDiskCache Specify if the disk caching is unlimited | |
107 | * @param overflowPersistence Specify if the persistent cache is used in overflow only mode | |
108 | * @param blocking This parameter takes effect when a cache entry has | |
109 | * just expired and several simultaneous requests try to retrieve it. While | |
110 | * one request is rebuilding the content, the other requests will either | |
111 | * block and wait for the new content (<code>blocking == true</code>) or | |
112 | * instead receive a copy of the stale content so they don't have to wait | |
113 | * (<code>blocking == false</code>). the default is <code>false</code>, | |
114 | * which provides better performance but at the expense of slightly stale | |
115 | * data being served. | |
116 | * @param algorithmClass The class implementing the desired algorithm | |
117 | * @param capacity The capacity | |
118 | */ | |
119 | 108 | public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence, boolean blocking, String algorithmClass, int capacity) { |
120 | // Instantiate the algo class if valid | |
121 | 108 | if (((algorithmClass != null) && (algorithmClass.length() > 0)) && (capacity > 0)) { |
122 | 16 | try { |
123 | 16 | cacheMap = (AbstractConcurrentReadCache) Class.forName(algorithmClass).newInstance(); |
124 | 16 | cacheMap.setMaxEntries(capacity); |
125 | } catch (Exception e) { | |
126 | 0 | log.error("Invalid class name for cache algorithm class. " + e.toString()); |
127 | } | |
128 | } | |
129 | ||
130 | 108 | if (cacheMap == null) { |
131 | // If we have a capacity, use LRU cache otherwise use unlimited Cache | |
132 | 92 | if (capacity > 0) { |
133 | 36 | cacheMap = new LRUCache(capacity); |
134 | } else { | |
135 | 56 | cacheMap = new UnlimitedCache(); |
136 | } | |
137 | } | |
138 | ||
139 | 108 | cacheMap.setUnlimitedDiskCache(unlimitedDiskCache); |
140 | 108 | cacheMap.setOverflowPersistence(overflowPersistence); |
141 | 108 | cacheMap.setMemoryCaching(useMemoryCaching); |
142 | ||
143 | 108 | this.blocking = blocking; |
144 | } | |
145 | ||
146 | /** | |
147 | * Allows the capacity of the cache to be altered dynamically. Note that | |
148 | * some cache implementations may choose to ignore this setting (eg the | |
149 | * {@link UnlimitedCache} ignores this call). | |
150 | * | |
151 | * @param capacity the maximum number of items to hold in the cache. | |
152 | */ | |
153 | 16 | public void setCapacity(int capacity) { |
154 | 16 | cacheMap.setMaxEntries(capacity); |
155 | } | |
156 | ||
157 | /** | |
158 | * Checks if the cache was flushed more recently than the CacheEntry provided. | |
159 | * Used to determine whether to refresh the particular CacheEntry. | |
160 | * | |
161 | * @param cacheEntry The cache entry which we're seeing whether to refresh | |
162 | * @return Whether or not the cache has been flushed more recently than this cache entry was updated. | |
163 | */ | |
164 | 218 | public boolean isFlushed(CacheEntry cacheEntry) { |
165 | 218 | if (flushDateTime != null) { |
166 | 0 | long lastUpdate = cacheEntry.getLastUpdate(); |
167 | ||
168 | 0 | return (flushDateTime.getTime() >= lastUpdate); |
169 | } else { | |
170 | 218 | return false; |
171 | } | |
172 | } | |
173 | ||
174 | /** | |
175 | * Retrieve an object from the cache specifying its key. | |
176 | * | |
177 | * @param key Key of the object in the cache. | |
178 | * | |
179 | * @return The object from cache | |
180 | * | |
181 | * @throws NeedsRefreshException Thrown when the object either | |
182 | * doesn't exist, or exists but is stale. When this exception occurs, | |
183 | * the CacheEntry corresponding to the supplied key will be locked | |
184 | * and other threads requesting this entry will potentially be blocked | |
185 | * until the caller repopulates the cache. If the caller choses not | |
186 | * to repopulate the cache, they <em>must</em> instead call | |
187 | * {@link #cancelUpdate(String)}. | |
188 | */ | |
189 | 8000 | public Object getFromCache(String key) throws NeedsRefreshException { |
190 | 8000 | return getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, null); |
191 | } | |
192 | ||
193 | /** | |
194 | * Retrieve an object from the cache specifying its key. | |
195 | * | |
196 | * @param key Key of the object in the cache. | |
197 | * @param refreshPeriod How long before the object needs refresh. To | |
198 | * allow the object to stay in the cache indefinitely, supply a value | |
199 | * of {@link CacheEntry#INDEFINITE_EXPIRY}. | |
200 | * | |
201 | * @return The object from cache | |
202 | * | |
203 | * @throws NeedsRefreshException Thrown when the object either | |
204 | * doesn't exist, or exists but is stale. When this exception occurs, | |
205 | * the CacheEntry corresponding to the supplied key will be locked | |
206 | * and other threads requesting this entry will potentially be blocked | |
207 | * until the caller repopulates the cache. If the caller choses not | |
208 | * to repopulate the cache, they <em>must</em> instead call | |
209 | * {@link #cancelUpdate(String)}. | |
210 | */ | |
211 | 2004396 | public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException { |
212 | 2003238 | return getFromCache(key, refreshPeriod, null); |
213 | } | |
214 | ||
215 | /** | |
216 | * Retrieve an object from the cache specifying its key. | |
217 | * | |
218 | * @param key Key of the object in the cache. | |
219 | * @param refreshPeriod How long before the object needs refresh. To | |
220 | * allow the object to stay in the cache indefinitely, supply a value | |
221 | * of {@link CacheEntry#INDEFINITE_EXPIRY}. | |
222 | * @param cronExpiry A cron expression that specifies fixed date(s) | |
223 | * and/or time(s) that this cache entry should | |
224 | * expire on. | |
225 | * | |
226 | * @return The object from cache | |
227 | * | |
228 | * @throws NeedsRefreshException Thrown when the object either | |
229 | * doesn't exist, or exists but is stale. When this exception occurs, | |
230 | * the CacheEntry corresponding to the supplied key will be locked | |
231 | * and other threads requesting this entry will potentially be blocked | |
232 | * until the caller repopulates the cache. If the caller choses not | |
233 | * to repopulate the cache, they <em>must</em> instead call | |
234 | * {@link #cancelUpdate(String)}. | |
235 | */ | |
236 | 2012065 | public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException { |
237 | 2011909 | CacheEntry cacheEntry = this.getCacheEntry(key, null, null); |
238 | ||
239 | 2012384 | Object content = cacheEntry.getContent(); |
240 | 2010975 | CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT; |
241 | ||
242 | 2012384 | boolean reload = false; |
243 | ||
244 | // Check if this entry has expired or has not yet been added to the cache. If | |
245 | // so, we need to decide whether to block, serve stale content or throw a | |
246 | // NeedsRefreshException | |
247 | 2012384 | if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) { |
248 | ||
249 | //Get access to the EntryUpdateState instance and increment the usage count during the potential sleep | |
250 | 2012166 | EntryUpdateState updateState = getUpdateState(key); |
251 | 2012166 | try { |
252 | 2012166 | synchronized (updateState) { |
253 | 2012166 | if (updateState.isAwaitingUpdate() || updateState.isCancelled()) { |
254 | // No one else is currently updating this entry - grab ownership | |
255 | 1979696 | updateState.startUpdate(); |
256 | ||
257 | 1979696 | if (cacheEntry.isNew()) { |
258 | 8026 | accessEventType = CacheMapAccessEventType.MISS; |
259 | } else { | |
260 | 1971670 | accessEventType = CacheMapAccessEventType.STALE_HIT; |
261 | } | |
262 | 32470 | } else if (updateState.isUpdating()) { |
263 | // Another thread is already updating the cache. We block if this | |
264 | // is a new entry, or blocking mode is enabled. Either putInCache() | |
265 | // or cancelUpdate() can cause this thread to resume. | |
266 | 32470 | if (cacheEntry.isNew() || blocking) { |
267 | 32466 | do { |
268 | 126624 | try { |
269 | 126624 | updateState.wait(); |
270 | } catch (InterruptedException e) { | |
271 | } | |
272 | 126624 | } while (updateState.isUpdating()); |
273 | ||
274 | 32466 | if (updateState.isCancelled()) { |
275 | // The updating thread cancelled the update, let this one have a go. | |
276 | // This increments the usage count for this EntryUpdateState instance | |
277 | 32446 | updateState.startUpdate(); |
278 | ||
279 | 32446 | if (cacheEntry.isNew()) { |
280 | 4 | accessEventType = CacheMapAccessEventType.MISS; |
281 | } else { | |
282 | 32442 | accessEventType = CacheMapAccessEventType.STALE_HIT; |
283 | } | |
284 | 20 | } else if (updateState.isComplete()) { |
285 | 20 | reload = true; |
286 | } else { | |
287 | 0 | log.error("Invalid update state for cache entry " + key); |
288 | } | |
289 | } | |
290 | } else { | |
291 | 0 | reload = true; |
292 | } | |
293 | } | |
294 | } finally { | |
295 | //Make sure we release the usage count for this EntryUpdateState since we don't use it anymore. If the current thread started the update, then the counter was | |
296 | //increased by one in startUpdate() | |
297 | 2012166 | releaseUpdateState(updateState, key); |
298 | } | |
299 | } | |
300 | ||
301 | // If reload is true then another thread must have successfully rebuilt the cache entry | |
302 | 2012384 | if (reload) { |
303 | 20 | cacheEntry = (CacheEntry) cacheMap.get(key); |
304 | ||
305 | 20 | if (cacheEntry != null) { |
306 | 20 | content = cacheEntry.getContent(); |
307 | } else { | |
308 | 0 | log.error("Could not reload cache entry after waiting for it to be rebuilt"); |
309 | } | |
310 | } | |
311 | ||
312 | 2012384 | dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null); |
313 | ||
314 | // If we didn't end up getting a hit then we need to throw a NRE | |
315 | 2012384 | if (accessEventType != CacheMapAccessEventType.HIT) { |
316 | 2012142 | throw new NeedsRefreshException(content); |
317 | } | |
318 | ||
319 | 242 | return content; |
320 | } | |
321 | ||
322 | /** | |
323 | * Set the listener to use for data persistence. Only one | |
324 | * <code>PersistenceListener</code> can be configured per cache. | |
325 | * | |
326 | * @param listener The implementation of a persistance listener | |
327 | */ | |
328 | 54 | public void setPersistenceListener(PersistenceListener listener) { |
329 | 54 | cacheMap.setPersistenceListener(listener); |
330 | } | |
331 | ||
332 | /** | |
333 | * Retrieves the currently configured <code>PersistenceListener</code>. | |
334 | * | |
335 | * @return the cache's <code>PersistenceListener</code>, or <code>null</code> | |
336 | * if no listener is configured. | |
337 | */ | |
338 | 0 | public PersistenceListener getPersistenceListener() { |
339 | 0 | return cacheMap.getPersistenceListener(); |
340 | } | |
341 | ||
342 | /** | |
343 | * Register a listener for Cache events. The listener must implement | |
344 | * one of the child interfaces of the {@link CacheEventListener} interface. | |
345 | * | |
346 | * @param listener The object that listens to events. | |
347 | */ | |
348 | 96 | public void addCacheEventListener(CacheEventListener listener, Class clazz) { |
349 | 96 | if (CacheEventListener.class.isAssignableFrom(clazz)) { |
350 | 96 | listenerList.add(clazz, listener); |
351 | } else { | |
352 | 0 | log.error("The class '" + clazz.getName() + "' is not a CacheEventListener. Ignoring this listener."); |
353 | } | |
354 | } | |
355 | ||
356 | /** | |
357 | * Cancels any pending update for this cache entry. This should <em>only</em> | |
358 | * be called by the thread that is responsible for performing the update ie | |
359 | * the thread that received the original {@link NeedsRefreshException}.<p/> | |
360 | * If a cache entry is not updated (via {@link #putInCache} and this method is | |
361 | * not called to let OSCache know the update will not be forthcoming, subsequent | |
362 | * requests for this cache entry will either block indefinitely (if this is a new | |
363 | * cache entry or cache.blocking=true), or forever get served stale content. Note | |
364 | * however that there is no harm in cancelling an update on a key that either | |
365 | * does not exist or is not currently being updated. | |
366 | * | |
367 | * @param key The key for the cache entry in question. | |
368 | */ | |
369 | 2008122 | public void cancelUpdate(String key) { |
370 | 2008122 | EntryUpdateState state; |
371 | ||
372 | 2008122 | if (key != null) { |
373 | 2008122 | synchronized (updateStates) { |
374 | 2008122 | state = (EntryUpdateState) updateStates.get(key); |
375 | ||
376 | 2008122 | if (state != null) { |
377 | 2008122 | synchronized (state) { |
378 | 2008122 | int usageCounter = state.cancelUpdate(); |
379 | 2008122 | state.notify(); |
380 | ||
381 | 2008122 | checkEntryStateUpdateUsage(key, state, usageCounter); |
382 | } | |
383 | } else { | |
384 | 0 | if (log.isErrorEnabled()) { |
385 | 0 | log.error("internal error: expected to get a state from key [" + key + "]"); |
386 | } | |
387 | } | |
388 | } | |
389 | } | |
390 | } | |
391 | ||
392 | /** | |
393 | * Utility method to check if the specified usage count is zero, and if so remove the corresponding EntryUpdateState from the updateStates. This is designed to factor common code. | |
394 | * | |
395 | * Warning: This method should always be called while holding both the updateStates field and the state parameter | |
396 | * @throws Exception | |
397 | */ | |
398 | 4024308 | private void checkEntryStateUpdateUsage(String key, EntryUpdateState state, int usageCounter) { |
399 | //Clean up the updateStates map to avoid a memory leak once no thread is using this EntryUpdateState instance anymore. | |
400 | 4024308 | if (usageCounter ==0) { |
401 | 23993 | EntryUpdateState removedState = (EntryUpdateState) updateStates.remove(key); |
402 | 23993 | if (state != removedState) { |
403 | 0 | if (log.isErrorEnabled()) { |
404 | 0 | log.error("internal error: removed state [" + removedState + "] from key [" + key + "] whereas we expected [" + state + "]"); |
405 | 0 | try { |
406 | 0 | throw new Exception("states not equal"); |
407 | } catch (Exception e) { | |
408 | // TODO Auto-generated catch block | |
409 | 0 | e.printStackTrace(); |
410 | } | |
411 | } | |
412 | } | |
413 | } | |
414 | } | |
415 | ||
416 | /** | |
417 | * Flush all entries in the cache on the given date/time. | |
418 | * | |
419 | * @param date The date at which all cache entries will be flushed. | |
420 | */ | |
421 | 0 | public void flushAll(Date date) { |
422 | 0 | flushAll(date, null); |
423 | } | |
424 | ||
425 | /** | |
426 | * Flush all entries in the cache on the given date/time. | |
427 | * | |
428 | * @param date The date at which all cache entries will be flushed. | |
429 | * @param origin The origin of this flush request (optional) | |
430 | */ | |
431 | 0 | public void flushAll(Date date, String origin) { |
432 | 0 | flushDateTime = date; |
433 | ||
434 | 0 | if (listenerList.getListenerCount() > 0) { |
435 | 0 | dispatchCachewideEvent(CachewideEventType.CACHE_FLUSHED, date, origin); |
436 | } | |
437 | } | |
438 | ||
439 | /** | |
440 | * Flush the cache entry (if any) that corresponds to the cache key supplied. | |
441 | * This call will flush the entry from the cache and remove the references to | |
442 | * it from any cache groups that it is a member of. On completion of the flush, | |
443 | * a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
444 | * | |
445 | * @param key The key of the entry to flush | |
446 | */ | |
447 | 0 | public void flushEntry(String key) { |
448 | 0 | flushEntry(key, null); |
449 | } | |
450 | ||
451 | /** | |
452 | * Flush the cache entry (if any) that corresponds to the cache key supplied. | |
453 | * This call will mark the cache entry as flushed so that the next access | |
454 | * to it will cause a {@link NeedsRefreshException}. On completion of the | |
455 | * flush, a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
456 | * | |
457 | * @param key The key of the entry to flush | |
458 | * @param origin The origin of this flush request (optional) | |
459 | */ | |
460 | 0 | public void flushEntry(String key, String origin) { |
461 | 0 | flushEntry(getCacheEntry(key, null, origin), origin); |
462 | } | |
463 | ||
464 | /** | |
465 | * Flushes all objects that belong to the supplied group. On completion | |
466 | * this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> event. | |
467 | * | |
468 | * @param group The group to flush | |
469 | */ | |
470 | 36 | public void flushGroup(String group) { |
471 | 36 | flushGroup(group, null); |
472 | } | |
473 | ||
474 | /** | |
475 | * Flushes all unexpired objects that belong to the supplied group. On | |
476 | * completion this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> | |
477 | * event. | |
478 | * | |
479 | * @param group The group to flush | |
480 | * @param origin The origin of this flush event (optional) | |
481 | */ | |
482 | 36 | public void flushGroup(String group, String origin) { |
483 | // Flush all objects in the group | |
484 | 36 | Set groupEntries = cacheMap.getGroup(group); |
485 | ||
486 | 36 | if (groupEntries != null) { |
487 | 35 | Iterator itr = groupEntries.iterator(); |
488 | 35 | String key; |
489 | 35 | CacheEntry entry; |
490 | ||
491 | 35 | while (itr.hasNext()) { |
492 | 107 | key = (String) itr.next(); |
493 | 107 | entry = (CacheEntry) cacheMap.get(key); |
494 | ||
495 | 107 | if ((entry != null) && !entry.needsRefresh(CacheEntry.INDEFINITE_EXPIRY)) { |
496 | 50 | flushEntry(entry, NESTED_EVENT); |
497 | } | |
498 | } | |
499 | } | |
500 | ||
501 | 36 | if (listenerList.getListenerCount() > 0) { |
502 | 36 | dispatchCacheGroupEvent(CacheEntryEventType.GROUP_FLUSHED, group, origin); |
503 | } | |
504 | } | |
505 | ||
506 | /** | |
507 | * Flush all entries with keys that match a given pattern | |
508 | * | |
509 | * @param pattern The key must contain this given value | |
510 | * @deprecated For performance and flexibility reasons it is preferable to | |
511 | * store cache entries in groups and use the {@link #flushGroup(String)} method | |
512 | * instead of relying on pattern flushing. | |
513 | */ | |
514 | 40 | public void flushPattern(String pattern) { |
515 | 40 | flushPattern(pattern, null); |
516 | } | |
517 | ||
518 | /** | |
519 | * Flush all entries with keys that match a given pattern | |
520 | * | |
521 | * @param pattern The key must contain this given value | |
522 | * @param origin The origin of this flush request | |
523 | * @deprecated For performance and flexibility reasons it is preferable to | |
524 | * store cache entries in groups and use the {@link #flushGroup(String, String)} | |
525 | * method instead of relying on pattern flushing. | |
526 | */ | |
527 | 40 | public void flushPattern(String pattern, String origin) { |
528 | // Check the pattern | |
529 | 40 | if ((pattern != null) && (pattern.length() > 0)) { |
530 | 24 | String key = null; |
531 | 24 | CacheEntry entry = null; |
532 | 24 | Iterator itr = cacheMap.keySet().iterator(); |
533 | ||
534 | 24 | while (itr.hasNext()) { |
535 | 72 | key = (String) itr.next(); |
536 | ||
537 | 72 | if (key.indexOf(pattern) >= 0) { |
538 | 8 | entry = (CacheEntry) cacheMap.get(key); |
539 | ||
540 | 8 | if (entry != null) { |
541 | 8 | flushEntry(entry, origin); |
542 | } | |
543 | } | |
544 | } | |
545 | ||
546 | 24 | if (listenerList.getListenerCount() > 0) { |
547 | 16 | dispatchCachePatternEvent(CacheEntryEventType.PATTERN_FLUSHED, pattern, origin); |
548 | } | |
549 | } else { | |
550 | // Empty pattern, nothing to do | |
551 | } | |
552 | } | |
553 | ||
554 | /** | |
555 | * Put an object in the cache specifying the key to use. | |
556 | * | |
557 | * @param key Key of the object in the cache. | |
558 | * @param content The object to cache. | |
559 | */ | |
560 | 8 | public void putInCache(String key, Object content) { |
561 | 8 | putInCache(key, content, null, null, null); |
562 | } | |
563 | ||
564 | /** | |
565 | * Put an object in the cache specifying the key and refresh policy to use. | |
566 | * | |
567 | * @param key Key of the object in the cache. | |
568 | * @param content The object to cache. | |
569 | * @param policy Object that implements refresh policy logic | |
570 | */ | |
571 | 12196 | public void putInCache(String key, Object content, EntryRefreshPolicy policy) { |
572 | 12196 | putInCache(key, content, null, policy, null); |
573 | } | |
574 | ||
575 | /** | |
576 | * Put in object into the cache, specifying both the key to use and the | |
577 | * cache groups the object belongs to. | |
578 | * | |
579 | * @param key Key of the object in the cache | |
580 | * @param content The object to cache | |
581 | * @param groups The cache groups to add the object to | |
582 | */ | |
583 | 84 | public void putInCache(String key, Object content, String[] groups) { |
584 | 84 | putInCache(key, content, groups, null, null); |
585 | } | |
586 | ||
587 | /** | |
588 | * Put an object into the cache specifying both the key to use and the | |
589 | * cache groups the object belongs to. | |
590 | * | |
591 | * @param key Key of the object in the cache | |
592 | * @param groups The cache groups to add the object to | |
593 | * @param content The object to cache | |
594 | * @param policy Object that implements the refresh policy logic | |
595 | */ | |
596 | 12288 | public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) { |
597 | 12288 | CacheEntry cacheEntry = this.getCacheEntry(key, policy, origin); |
598 | 12284 | boolean isNewEntry = cacheEntry.isNew(); |
599 | ||
600 | // [CACHE-118] If we have an existing entry, create a new CacheEntry so we can still access the old one later | |
601 | 12284 | if (!isNewEntry) { |
602 | 4048 | cacheEntry = new CacheEntry(key, policy); |
603 | } | |
604 | ||
605 | 12284 | cacheEntry.setContent(content); |
606 | 12284 | cacheEntry.setGroups(groups); |
607 | 12284 | cacheMap.put(key, cacheEntry); |
608 | ||
609 | // Signal to any threads waiting on this update that it's now ready for them | |
610 | // in the cache! | |
611 | 12284 | completeUpdate(key); |
612 | ||
613 | 12284 | if (listenerList.getListenerCount() > 0) { |
614 | 120 | CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin); |
615 | ||
616 | 120 | if (isNewEntry) { |
617 | 80 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_ADDED, event); |
618 | } else { | |
619 | 40 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_UPDATED, event); |
620 | } | |
621 | } | |
622 | } | |
623 | ||
624 | /** | |
625 | * Unregister a listener for Cache events. | |
626 | * | |
627 | * @param listener The object that currently listens to events. | |
628 | */ | |
629 | 96 | public void removeCacheEventListener(CacheEventListener listener, Class clazz) { |
630 | 96 | listenerList.remove(clazz, listener); |
631 | } | |
632 | ||
633 | /** | |
634 | * Get an entry from this cache or create one if it doesn't exist. | |
635 | * | |
636 | * @param key The key of the cache entry | |
637 | * @param policy Object that implements refresh policy logic | |
638 | * @param origin The origin of request (optional) | |
639 | * @return CacheEntry for the specified key. | |
640 | */ | |
641 | 2024684 | protected CacheEntry getCacheEntry(String key, EntryRefreshPolicy policy, String origin) { |
642 | 2023475 | CacheEntry cacheEntry = null; |
643 | ||
644 | // Verify that the key is valid | |
645 | 2024684 | if ((key == null) || (key.length() == 0)) { |
646 | 16 | throw new IllegalArgumentException("getCacheEntry called with an empty or null key"); |
647 | } | |
648 | ||
649 | 2023939 | cacheEntry = (CacheEntry) cacheMap.get(key); |
650 | ||
651 | // if the cache entry does not exist, create a new one | |
652 | 2024668 | if (cacheEntry == null) { |
653 | 16282 | if (log.isDebugEnabled()) { |
654 | 0 | log.debug("No cache entry exists for key='" + key + "', creating"); |
655 | } | |
656 | ||
657 | 16282 | cacheEntry = new CacheEntry(key, policy); |
658 | } | |
659 | ||
660 | 2024668 | return cacheEntry; |
661 | } | |
662 | ||
663 | /** | |
664 | * Indicates whether or not the cache entry is stale. | |
665 | * | |
666 | * @param cacheEntry The cache entry to test the freshness of. | |
667 | * @param refreshPeriod The maximum allowable age of the entry, in seconds. | |
668 | * @param cronExpiry A cron expression specifying absolute date(s) and/or time(s) | |
669 | * that the cache entry should expire at. If the cache entry was refreshed prior to | |
670 | * the most recent match for the cron expression, the entry will be considered stale. | |
671 | * | |
672 | * @return <code>true</code> if the entry is stale, <code>false</code> otherwise. | |
673 | */ | |
674 | 2011500 | protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) { |
675 | 2012384 | boolean result = cacheEntry.needsRefresh(refreshPeriod) || isFlushed(cacheEntry); |
676 | ||
677 | 2012384 | if ((cronExpiry != null) && (cronExpiry.length() > 0)) { |
678 | 0 | try { |
679 | 0 | FastCronParser parser = new FastCronParser(cronExpiry); |
680 | 0 | result = result || parser.hasMoreRecentMatch(cacheEntry.getLastUpdate()); |
681 | } catch (ParseException e) { | |
682 | 0 | log.warn(e); |
683 | } | |
684 | } | |
685 | ||
686 | 2011201 | return result; |
687 | } | |
688 | ||
689 | /** | |
690 | * Get the updating cache entry from the update map. If one is not found, | |
691 | * create a new one (with state {@link EntryUpdateState#NOT_YET_UPDATING}) | |
692 | * and add it to the map. | |
693 | * | |
694 | * @param key The cache key for this entry | |
695 | * | |
696 | * @return the CacheEntry that was found (or added to) the updatingEntries | |
697 | * map. | |
698 | */ | |
699 | 2011453 | protected EntryUpdateState getUpdateState(String key) { |
700 | 2010724 | EntryUpdateState updateState; |
701 | ||
702 | 2012166 | synchronized (updateStates) { |
703 | // Try to find the matching state object in the updating entry map. | |
704 | 2012166 | updateState = (EntryUpdateState) updateStates.get(key); |
705 | ||
706 | 2012166 | if (updateState == null) { |
707 | // It's not there so add it. | |
708 | 23993 | updateState = new EntryUpdateState(); |
709 | 23993 | updateStates.put(key, updateState); |
710 | } else { | |
711 | //Otherwise indicate that we start using it to prevent its removal until all threads are done with it. | |
712 | 1988173 | updateState.incrementUsageCounter(); |
713 | } | |
714 | } | |
715 | ||
716 | 2011934 | return updateState; |
717 | } | |
718 | ||
719 | /** | |
720 | * releases the usage that was made of the specified EntryUpdateState. When this reaches zero, the entry is removed from the map. | |
721 | * @param state the state to release the usage of | |
722 | * @param key the associated key. | |
723 | */ | |
724 | 2012166 | protected void releaseUpdateState(EntryUpdateState state, String key) { |
725 | 2012166 | synchronized (updateStates) { |
726 | 2012166 | int usageCounter = state.decrementUsageCounter(); |
727 | 2012166 | checkEntryStateUpdateUsage(key, state, usageCounter); |
728 | } | |
729 | } | |
730 | ||
731 | /** | |
732 | * Completely clears the cache. | |
733 | */ | |
734 | 28 | protected void clear() { |
735 | 28 | cacheMap.clear(); |
736 | } | |
737 | ||
738 | /** | |
739 | * Removes the update state for the specified key and notifies any other | |
740 | * threads that are waiting on this object. This is called automatically | |
741 | * by the {@link #putInCache} method, so it is possible that no EntryUpdateState was hold | |
742 | * when this method is called. | |
743 | * | |
744 | * @param key The cache key that is no longer being updated. | |
745 | */ | |
746 | 12284 | protected void completeUpdate(String key) { |
747 | 12284 | EntryUpdateState state; |
748 | ||
749 | 12284 | synchronized (updateStates) { |
750 | 12284 | state = (EntryUpdateState) updateStates.get(key); |
751 | ||
752 | 12284 | if (state != null) { |
753 | 4020 | synchronized (state) { |
754 | 4020 | int usageCounter = state.completeUpdate(); |
755 | 4020 | state.notifyAll(); |
756 | ||
757 | 4020 | checkEntryStateUpdateUsage(key, state, usageCounter); |
758 | ||
759 | } | |
760 | } else { | |
761 | //If putInCache() was called directly (i.e. not as a result of a NeedRefreshException) then no EntryUpdateState would be found. | |
762 | } | |
763 | } | |
764 | } | |
765 | ||
766 | /** | |
767 | * Completely removes a cache entry from the cache and its associated cache | |
768 | * groups. | |
769 | * | |
770 | * @param key The key of the entry to remove. | |
771 | */ | |
772 | 0 | public void removeEntry(String key) { |
773 | 0 | removeEntry(key, null); |
774 | } | |
775 | ||
776 | /** | |
777 | * Completely removes a cache entry from the cache and its associated cache | |
778 | * groups. | |
779 | * | |
780 | * @param key The key of the entry to remove. | |
781 | * @param origin The origin of this remove request. | |
782 | */ | |
783 | 0 | protected void removeEntry(String key, String origin) { |
784 | 0 | CacheEntry cacheEntry = (CacheEntry) cacheMap.get(key); |
785 | 0 | cacheMap.remove(key); |
786 | ||
787 | 0 | if (listenerList.getListenerCount() > 0) { |
788 | 0 | CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin); |
789 | 0 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_REMOVED, event); |
790 | } | |
791 | } | |
792 | ||
793 | /** | |
794 | * Dispatch a cache entry event to all registered listeners. | |
795 | * | |
796 | * @param eventType The type of event (used to branch on the proper method) | |
797 | * @param event The event that was fired | |
798 | */ | |
799 | 174 | private void dispatchCacheEntryEvent(CacheEntryEventType eventType, CacheEntryEvent event) { |
800 | // Guaranteed to return a non-null array | |
801 | 174 | Object[] listeners = listenerList.getListenerList(); |
802 | ||
803 | // Process the listeners last to first, notifying | |
804 | // those that are interested in this event | |
805 | 174 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
806 | 348 | if (listeners[i] == CacheEntryEventListener.class) { |
807 | 174 | if (eventType.equals(CacheEntryEventType.ENTRY_ADDED)) { |
808 | 80 | ((CacheEntryEventListener) listeners[i + 1]).cacheEntryAdded(event); |
809 | 94 | } else if (eventType.equals(CacheEntryEventType.ENTRY_UPDATED)) { |
810 | 40 | ((CacheEntryEventListener) listeners[i + 1]).cacheEntryUpdated(event); |
811 | 54 | } else if (eventType.equals(CacheEntryEventType.ENTRY_FLUSHED)) { |
812 | 54 | ((CacheEntryEventListener) listeners[i + 1]).cacheEntryFlushed(event); |
813 | 0 | } else if (eventType.equals(CacheEntryEventType.ENTRY_REMOVED)) { |
814 | 0 | ((CacheEntryEventListener) listeners[i + 1]).cacheEntryRemoved(event); |
815 | } | |
816 | } | |
817 | } | |
818 | } | |
819 | ||
820 | /** | |
821 | * Dispatch a cache group event to all registered listeners. | |
822 | * | |
823 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
824 | * @param group The cache group that the event applies to | |
825 | * @param origin The origin of this event (optional) | |
826 | */ | |
827 | 36 | private void dispatchCacheGroupEvent(CacheEntryEventType eventType, String group, String origin) { |
828 | 36 | CacheGroupEvent event = new CacheGroupEvent(this, group, origin); |
829 | ||
830 | // Guaranteed to return a non-null array | |
831 | 36 | Object[] listeners = listenerList.getListenerList(); |
832 | ||
833 | // Process the listeners last to first, notifying | |
834 | // those that are interested in this event | |
835 | 36 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
836 | 72 | if (listeners[i] == CacheEntryEventListener.class) { |
837 | 36 | if (eventType.equals(CacheEntryEventType.GROUP_FLUSHED)) { |
838 | 36 | ((CacheEntryEventListener) listeners[i + 1]).cacheGroupFlushed(event); |
839 | } | |
840 | } | |
841 | } | |
842 | } | |
843 | ||
844 | /** | |
845 | * Dispatch a cache map access event to all registered listeners. | |
846 | * | |
847 | * @param eventType The type of event | |
848 | * @param entry The entry that was affected. | |
849 | * @param origin The origin of this event (optional) | |
850 | */ | |
851 | 2012384 | private void dispatchCacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) { |
852 | 2012384 | CacheMapAccessEvent event = new CacheMapAccessEvent(eventType, entry, origin); |
853 | ||
854 | // Guaranteed to return a non-null array | |
855 | 2012384 | Object[] listeners = listenerList.getListenerList(); |
856 | ||
857 | // Process the listeners last to first, notifying | |
858 | // those that are interested in this event | |
859 | 2012384 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
860 | 368 | if (listeners[i] == CacheMapAccessEventListener.class) { |
861 | 184 | ((CacheMapAccessEventListener) listeners[i + 1]).accessed(event); |
862 | } | |
863 | } | |
864 | } | |
865 | ||
866 | /** | |
867 | * Dispatch a cache pattern event to all registered listeners. | |
868 | * | |
869 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
870 | * @param pattern The cache pattern that the event applies to | |
871 | * @param origin The origin of this event (optional) | |
872 | */ | |
873 | 16 | private void dispatchCachePatternEvent(CacheEntryEventType eventType, String pattern, String origin) { |
874 | 16 | CachePatternEvent event = new CachePatternEvent(this, pattern, origin); |
875 | ||
876 | // Guaranteed to return a non-null array | |
877 | 16 | Object[] listeners = listenerList.getListenerList(); |
878 | ||
879 | // Process the listeners last to first, notifying | |
880 | // those that are interested in this event | |
881 | 16 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
882 | 32 | if (listeners[i] == CacheEntryEventListener.class) { |
883 | 16 | if (eventType.equals(CacheEntryEventType.PATTERN_FLUSHED)) { |
884 | 16 | ((CacheEntryEventListener) listeners[i + 1]).cachePatternFlushed(event); |
885 | } | |
886 | } | |
887 | } | |
888 | } | |
889 | ||
890 | /** | |
891 | * Dispatches a cache-wide event to all registered listeners. | |
892 | * | |
893 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
894 | * @param origin The origin of this event (optional) | |
895 | */ | |
896 | 0 | private void dispatchCachewideEvent(CachewideEventType eventType, Date date, String origin) { |
897 | 0 | CachewideEvent event = new CachewideEvent(this, date, origin); |
898 | ||
899 | // Guaranteed to return a non-null array | |
900 | 0 | Object[] listeners = listenerList.getListenerList(); |
901 | ||
902 | // Process the listeners last to first, notifying | |
903 | // those that are interested in this event | |
904 | 0 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
905 | 0 | if (listeners[i] == CacheEntryEventListener.class) { |
906 | 0 | if (eventType.equals(CachewideEventType.CACHE_FLUSHED)) { |
907 | 0 | ((CacheEntryEventListener) listeners[i + 1]).cacheFlushed(event); |
908 | } | |
909 | } | |
910 | } | |
911 | } | |
912 | ||
913 | /** | |
914 | * Flush a cache entry. On completion of the flush, a | |
915 | * <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
916 | * | |
917 | * @param entry The entry to flush | |
918 | * @param origin The origin of this flush event (optional) | |
919 | */ | |
920 | 58 | private void flushEntry(CacheEntry entry, String origin) { |
921 | 58 | String key = entry.getKey(); |
922 | ||
923 | // Flush the object itself | |
924 | 58 | entry.flush(); |
925 | ||
926 | 58 | if (!entry.isNew()) { |
927 | // Update the entry's state in the map | |
928 | 58 | cacheMap.put(key, entry); |
929 | } | |
930 | ||
931 | // Trigger an ENTRY_FLUSHED event. [CACHE-107] Do this for all flushes. | |
932 | 58 | if (listenerList.getListenerCount() > 0) { |
933 | 54 | CacheEntryEvent event = new CacheEntryEvent(this, entry, origin); |
934 | 54 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_FLUSHED, event); |
935 | } | |
936 | } | |
937 | ||
938 | /** | |
939 | * Test support only: return the number of EntryUpdateState instances within the updateStates map. | |
940 | */ | |
941 | 32 | protected int getNbUpdateState() { |
942 | 32 | synchronized(updateStates) { |
943 | 32 | return updateStates.size(); |
944 | } | |
945 | } | |
946 | ||
947 | ||
948 | /** | |
949 | * Test support only: return the number of entries currently in the cache map | |
950 | */ | |
951 | 8 | public int getNbEntries() { |
952 | 8 | synchronized(cacheMap) { |
953 | 8 | return cacheMap.size(); |
954 | } | |
955 | } | |
956 | } |
|