|
|||||||||||||||||||
Source file | Conditionals | Statements | Methods | TOTAL | |||||||||||||||
Cache.java | 80,6% | 86% | 82,6% | 84% |
|
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: 468 $ | |
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 | 40 | public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence) { |
93 | 40 | 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 | 235 | public Cache(boolean useMemoryCaching, boolean unlimitedDiskCache, boolean overflowPersistence, boolean blocking, String algorithmClass, int capacity) { |
120 | // Instantiate the algo class if valid | |
121 | 235 | if (((algorithmClass != null) && (algorithmClass.length() > 0)) && (capacity > 0)) { |
122 | 20 | try { |
123 | 20 | cacheMap = (AbstractConcurrentReadCache) Class.forName(algorithmClass).newInstance(); |
124 | 20 | cacheMap.setMaxEntries(capacity); |
125 | } catch (Exception e) { | |
126 | 0 | log.error("Invalid class name for cache algorithm class. " + e.toString()); |
127 | } | |
128 | } | |
129 | ||
130 | 235 | if (cacheMap == null) { |
131 | // If we have a capacity, use LRU cache otherwise use unlimited Cache | |
132 | 215 | if (capacity > 0) { |
133 | 68 | cacheMap = new LRUCache(capacity); |
134 | } else { | |
135 | 147 | cacheMap = new UnlimitedCache(); |
136 | } | |
137 | } | |
138 | ||
139 | 235 | cacheMap.setUnlimitedDiskCache(unlimitedDiskCache); |
140 | 235 | cacheMap.setOverflowPersistence(overflowPersistence); |
141 | 235 | cacheMap.setMemoryCaching(useMemoryCaching); |
142 | ||
143 | 235 | this.blocking = blocking; |
144 | } | |
145 | ||
146 | /** | |
147 | * @return the maximum number of items to cache can hold. | |
148 | */ | |
149 | 0 | public int getCapacity() { |
150 | 0 | return cacheMap.getMaxEntries(); |
151 | } | |
152 | ||
153 | /** | |
154 | * Allows the capacity of the cache to be altered dynamically. Note that | |
155 | * some cache implementations may choose to ignore this setting (eg the | |
156 | * {@link UnlimitedCache} ignores this call). | |
157 | * | |
158 | * @param capacity the maximum number of items to hold in the cache. | |
159 | */ | |
160 | 20 | public void setCapacity(int capacity) { |
161 | 20 | cacheMap.setMaxEntries(capacity); |
162 | } | |
163 | ||
164 | /** | |
165 | * Checks if the cache was flushed more recently than the CacheEntry provided. | |
166 | * Used to determine whether to refresh the particular CacheEntry. | |
167 | * | |
168 | * @param cacheEntry The cache entry which we're seeing whether to refresh | |
169 | * @return Whether or not the cache has been flushed more recently than this cache entry was updated. | |
170 | */ | |
171 | 308 | public boolean isFlushed(CacheEntry cacheEntry) { |
172 | 308 | if (flushDateTime != null) { |
173 | 10 | final long lastUpdate = cacheEntry.getLastUpdate(); |
174 | 10 | final long flushTime = flushDateTime.getTime(); |
175 | ||
176 | // CACHE-241: check flushDateTime with current time also | |
177 | 10 | return (flushTime <= System.currentTimeMillis()) && (flushTime >= lastUpdate); |
178 | } else { | |
179 | 298 | return false; |
180 | } | |
181 | } | |
182 | ||
183 | /** | |
184 | * Retrieve an object from the cache specifying its key. | |
185 | * | |
186 | * @param key Key of the object in the cache. | |
187 | * | |
188 | * @return The object from cache | |
189 | * | |
190 | * @throws NeedsRefreshException Thrown when the object either | |
191 | * doesn't exist, or exists but is stale. When this exception occurs, | |
192 | * the CacheEntry corresponding to the supplied key will be locked | |
193 | * and other threads requesting this entry will potentially be blocked | |
194 | * until the caller repopulates the cache. If the caller choses not | |
195 | * to repopulate the cache, they <em>must</em> instead call | |
196 | * {@link #cancelUpdate(String)}. | |
197 | */ | |
198 | 60010 | public Object getFromCache(String key) throws NeedsRefreshException { |
199 | 60010 | return getFromCache(key, CacheEntry.INDEFINITE_EXPIRY, null); |
200 | } | |
201 | ||
202 | /** | |
203 | * Retrieve an object from the cache specifying its key. | |
204 | * | |
205 | * @param key Key of the object in the cache. | |
206 | * @param refreshPeriod How long before the object needs refresh. To | |
207 | * allow the object to stay in the cache indefinitely, supply a value | |
208 | * of {@link CacheEntry#INDEFINITE_EXPIRY}. | |
209 | * | |
210 | * @return The object from cache | |
211 | * | |
212 | * @throws NeedsRefreshException Thrown when the object either | |
213 | * doesn't exist, or exists but is stale. When this exception occurs, | |
214 | * the CacheEntry corresponding to the supplied key will be locked | |
215 | * and other threads requesting this entry will potentially be blocked | |
216 | * until the caller repopulates the cache. If the caller choses not | |
217 | * to repopulate the cache, they <em>must</em> instead call | |
218 | * {@link #cancelUpdate(String)}. | |
219 | */ | |
220 | 4505570 | public Object getFromCache(String key, int refreshPeriod) throws NeedsRefreshException { |
221 | 4505570 | return getFromCache(key, refreshPeriod, null); |
222 | } | |
223 | ||
224 | /** | |
225 | * Retrieve an object from the cache specifying its key. | |
226 | * | |
227 | * @param key Key of the object in the cache. | |
228 | * @param refreshPeriod How long before the object needs refresh. To | |
229 | * allow the object to stay in the cache indefinitely, supply a value | |
230 | * of {@link CacheEntry#INDEFINITE_EXPIRY}. | |
231 | * @param cronExpiry A cron expression that specifies fixed date(s) | |
232 | * and/or time(s) that this cache entry should | |
233 | * expire on. | |
234 | * | |
235 | * @return The object from cache | |
236 | * | |
237 | * @throws NeedsRefreshException Thrown when the object either | |
238 | * doesn't exist, or exists but is stale. When this exception occurs, | |
239 | * the CacheEntry corresponding to the supplied key will be locked | |
240 | * and other threads requesting this entry will potentially be blocked | |
241 | * until the caller repopulates the cache. If the caller choses not | |
242 | * to repopulate the cache, they <em>must</em> instead call | |
243 | * {@link #cancelUpdate(String)}. | |
244 | */ | |
245 | 4565580 | public Object getFromCache(String key, int refreshPeriod, String cronExpiry) throws NeedsRefreshException { |
246 | 4565580 | CacheEntry cacheEntry = this.getCacheEntry(key, null, null); |
247 | ||
248 | 4564811 | Object content = cacheEntry.getContent(); |
249 | 4565565 | CacheMapAccessEventType accessEventType = CacheMapAccessEventType.HIT; |
250 | ||
251 | 4565475 | boolean reload = false; |
252 | ||
253 | // Check if this entry has expired or has not yet been added to the cache. If | |
254 | // so, we need to decide whether to block, serve stale content or throw a | |
255 | // NeedsRefreshException | |
256 | 4565565 | if (this.isStale(cacheEntry, refreshPeriod, cronExpiry)) { |
257 | ||
258 | //Get access to the EntryUpdateState instance and increment the usage count during the potential sleep | |
259 | 4565262 | EntryUpdateState updateState = getUpdateState(key); |
260 | 4564787 | try { |
261 | 4565262 | synchronized (updateState) { |
262 | 4565262 | if (updateState.isAwaitingUpdate() || updateState.isCancelled()) { |
263 | // No one else is currently updating this entry - grab ownership | |
264 | 4527583 | updateState.startUpdate(); |
265 | ||
266 | 4527583 | if (cacheEntry.isNew()) { |
267 | 60044 | accessEventType = CacheMapAccessEventType.MISS; |
268 | } else { | |
269 | 4467539 | accessEventType = CacheMapAccessEventType.STALE_HIT; |
270 | } | |
271 | 37679 | } else if (updateState.isUpdating()) { |
272 | // Another thread is already updating the cache. We block if this | |
273 | // is a new entry, or blocking mode is enabled. Either putInCache() | |
274 | // or cancelUpdate() can cause this thread to resume. | |
275 | 37679 | if (cacheEntry.isNew() || blocking) { |
276 | 37674 | do { |
277 | 129432 | try { |
278 | 129432 | updateState.wait(); |
279 | } catch (InterruptedException e) { | |
280 | } | |
281 | 129432 | } while (updateState.isUpdating()); |
282 | ||
283 | 37674 | if (updateState.isCancelled()) { |
284 | // The updating thread cancelled the update, let this one have a go. | |
285 | // This increments the usage count for this EntryUpdateState instance | |
286 | 37636 | updateState.startUpdate(); |
287 | ||
288 | 37636 | if (cacheEntry.isNew()) { |
289 | 10 | accessEventType = CacheMapAccessEventType.MISS; |
290 | } else { | |
291 | 37626 | accessEventType = CacheMapAccessEventType.STALE_HIT; |
292 | } | |
293 | 38 | } else if (updateState.isComplete()) { |
294 | 38 | reload = true; |
295 | } else { | |
296 | 0 | log.error("Invalid update state for cache entry " + key); |
297 | } | |
298 | } | |
299 | } else { | |
300 | 0 | reload = true; |
301 | } | |
302 | } | |
303 | } finally { | |
304 | //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 | |
305 | //increased by one in startUpdate() | |
306 | 4565262 | releaseUpdateState(updateState, key); |
307 | } | |
308 | } | |
309 | ||
310 | // If reload is true then another thread must have successfully rebuilt the cache entry | |
311 | 4565565 | if (reload) { |
312 | 38 | cacheEntry = (CacheEntry) cacheMap.get(key); |
313 | ||
314 | 38 | if (cacheEntry != null) { |
315 | 38 | content = cacheEntry.getContent(); |
316 | } else { | |
317 | 0 | log.error("Could not reload cache entry after waiting for it to be rebuilt"); |
318 | } | |
319 | } | |
320 | ||
321 | 4565565 | dispatchCacheMapAccessEvent(accessEventType, cacheEntry, null); |
322 | ||
323 | // If we didn't end up getting a hit then we need to throw a NRE | |
324 | 4565565 | if (accessEventType != CacheMapAccessEventType.HIT) { |
325 | 4565219 | throw new NeedsRefreshException(content); |
326 | } | |
327 | ||
328 | 346 | return content; |
329 | } | |
330 | ||
331 | /** | |
332 | * Set the listener to use for data persistence. Only one | |
333 | * <code>PersistenceListener</code> can be configured per cache. | |
334 | * | |
335 | * @param listener The implementation of a persistance listener | |
336 | */ | |
337 | 116 | public void setPersistenceListener(PersistenceListener listener) { |
338 | 116 | cacheMap.setPersistenceListener(listener); |
339 | } | |
340 | ||
341 | /** | |
342 | * Retrieves the currently configured <code>PersistenceListener</code>. | |
343 | * | |
344 | * @return the cache's <code>PersistenceListener</code>, or <code>null</code> | |
345 | * if no listener is configured. | |
346 | */ | |
347 | 0 | public PersistenceListener getPersistenceListener() { |
348 | 0 | return cacheMap.getPersistenceListener(); |
349 | } | |
350 | ||
351 | /** | |
352 | * Register a listener for Cache events. The listener must implement | |
353 | * one of the child interfaces of the {@link CacheEventListener} interface. | |
354 | * | |
355 | * @param listener The object that listens to events. | |
356 | * @since 2.4 | |
357 | */ | |
358 | 179 | public void addCacheEventListener(CacheEventListener listener) { |
359 | // listenerList.add(CacheEventListener.class, listener); | |
360 | 179 | listenerList.add(listener.getClass(), listener); |
361 | } | |
362 | ||
363 | /** | |
364 | * Register a listener for Cache events. The listener must implement | |
365 | * one of the child interfaces of the {@link CacheEventListener} interface. | |
366 | * | |
367 | * @param listener The object that listens to events. | |
368 | * @param clazz the type of the listener to be added | |
369 | * @deprecated use {@link #addCacheEventListener(CacheEventListener)} | |
370 | */ | |
371 | 0 | public void addCacheEventListener(CacheEventListener listener, Class clazz) { |
372 | 0 | if (CacheEventListener.class.isAssignableFrom(clazz)) { |
373 | 0 | listenerList.add(clazz, listener); |
374 | } else { | |
375 | 0 | log.error("The class '" + clazz.getName() + "' is not a CacheEventListener. Ignoring this listener."); |
376 | } | |
377 | } | |
378 | ||
379 | /** | |
380 | * Returns the list of all CacheEventListeners. | |
381 | * @return the CacheEventListener's list of the Cache | |
382 | */ | |
383 | 0 | public EventListenerList getCacheEventListenerList() { |
384 | 0 | return listenerList; |
385 | } | |
386 | ||
387 | /** | |
388 | * Cancels any pending update for this cache entry. This should <em>only</em> | |
389 | * be called by the thread that is responsible for performing the update ie | |
390 | * the thread that received the original {@link NeedsRefreshException}.<p/> | |
391 | * If a cache entry is not updated (via {@link #putInCache} and this method is | |
392 | * not called to let OSCache know the update will not be forthcoming, subsequent | |
393 | * requests for this cache entry will either block indefinitely (if this is a new | |
394 | * cache entry or cache.blocking=true), or forever get served stale content. Note | |
395 | * however that there is no harm in cancelling an update on a key that either | |
396 | * does not exist or is not currently being updated. | |
397 | * | |
398 | * @param key The key for the cache entry in question. | |
399 | * @throws IllegalStateException if the cache entry isn't in the state UPDATE_IN_PROGRESS | |
400 | */ | |
401 | 4510174 | public void cancelUpdate(String key) { |
402 | 4510174 | EntryUpdateState state; |
403 | ||
404 | 4510174 | if (key != null) { |
405 | 4510174 | synchronized (updateStates) { |
406 | 4510174 | state = (EntryUpdateState) updateStates.get(key); |
407 | ||
408 | 4510174 | if (state != null) { |
409 | 4510174 | synchronized (state) { |
410 | 4510174 | int usageCounter = state.cancelUpdate(); |
411 | 4510174 | state.notify(); |
412 | ||
413 | 4510174 | checkEntryStateUpdateUsage(key, state, usageCounter); |
414 | } | |
415 | } else { | |
416 | 0 | if (log.isErrorEnabled()) { |
417 | 0 | log.error("internal error: expected to get a state from key [" + key + "]"); |
418 | } | |
419 | } | |
420 | } | |
421 | } | |
422 | } | |
423 | ||
424 | /** | |
425 | * 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. | |
426 | * | |
427 | * Warning: This method should always be called while holding both the updateStates field and the state parameter | |
428 | * @throws Exception | |
429 | */ | |
430 | 9130481 | private void checkEntryStateUpdateUsage(String key, EntryUpdateState state, int usageCounter) { |
431 | //Clean up the updateStates map to avoid a memory leak once no thread is using this EntryUpdateState instance anymore. | |
432 | 9130481 | if (usageCounter == 0) { |
433 | 205224 | EntryUpdateState removedState = (EntryUpdateState) updateStates.remove(key); |
434 | 205224 | if (state != removedState) { |
435 | 0 | if (log.isErrorEnabled()) { |
436 | 0 | try { |
437 | 0 | throw new Exception("OSCache: internal error: removed state [" + removedState + "] from key [" + key + "] whereas we expected [" + state + "]"); |
438 | } catch (Exception e) { | |
439 | 0 | log.error(e); |
440 | } | |
441 | } | |
442 | } | |
443 | } | |
444 | } | |
445 | ||
446 | /** | |
447 | * Flush all entries in the cache on the given date/time. | |
448 | * | |
449 | * @param date The date at which all cache entries will be flushed. | |
450 | */ | |
451 | 10 | public void flushAll(Date date) { |
452 | 10 | flushAll(date, null); |
453 | } | |
454 | ||
455 | /** | |
456 | * Flush all entries in the cache on the given date/time. | |
457 | * | |
458 | * @param date The date at which all cache entries will be flushed. | |
459 | * @param origin The origin of this flush request (optional) | |
460 | */ | |
461 | 10 | public void flushAll(Date date, String origin) { |
462 | 10 | flushDateTime = date; |
463 | ||
464 | 10 | if (listenerList.getListenerCount() > 0) { |
465 | 6 | dispatchCachewideEvent(CachewideEventType.CACHE_FLUSHED, date, origin); |
466 | } | |
467 | } | |
468 | ||
469 | /** | |
470 | * Flush the cache entry (if any) that corresponds to the cache key supplied. | |
471 | * This call will flush the entry from the cache and remove the references to | |
472 | * it from any cache groups that it is a member of. On completion of the flush, | |
473 | * a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
474 | * | |
475 | * @param key The key of the entry to flush | |
476 | */ | |
477 | 10 | public void flushEntry(String key) { |
478 | 10 | flushEntry(key, null); |
479 | } | |
480 | ||
481 | /** | |
482 | * Flush the cache entry (if any) that corresponds to the cache key supplied. | |
483 | * This call will mark the cache entry as flushed so that the next access | |
484 | * to it will cause a {@link NeedsRefreshException}. On completion of the | |
485 | * flush, a <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
486 | * | |
487 | * @param key The key of the entry to flush | |
488 | * @param origin The origin of this flush request (optional) | |
489 | */ | |
490 | 10 | public void flushEntry(String key, String origin) { |
491 | 10 | flushEntry(getCacheEntry(key, null, origin), origin); |
492 | } | |
493 | ||
494 | /** | |
495 | * Flushes all objects that belong to the supplied group. On completion | |
496 | * this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> event. | |
497 | * | |
498 | * @param group The group to flush | |
499 | */ | |
500 | 45 | public void flushGroup(String group) { |
501 | 45 | flushGroup(group, null); |
502 | } | |
503 | ||
504 | /** | |
505 | * Flushes all unexpired objects that belong to the supplied group. On | |
506 | * completion this method fires a <tt>CacheEntryEventType.GROUP_FLUSHED</tt> | |
507 | * event. | |
508 | * | |
509 | * @param group The group to flush | |
510 | * @param origin The origin of this flush event (optional) | |
511 | */ | |
512 | 45 | public void flushGroup(String group, String origin) { |
513 | // Flush all objects in the group | |
514 | 45 | Set groupEntries = cacheMap.getGroup(group); |
515 | ||
516 | 45 | if (groupEntries != null) { |
517 | 44 | Iterator itr = groupEntries.iterator(); |
518 | 44 | String key; |
519 | 44 | CacheEntry entry; |
520 | ||
521 | 44 | while (itr.hasNext()) { |
522 | 139 | key = (String) itr.next(); |
523 | 139 | entry = (CacheEntry) cacheMap.get(key); |
524 | ||
525 | 139 | if ((entry != null) && !entry.needsRefresh(CacheEntry.INDEFINITE_EXPIRY)) { |
526 | 64 | flushEntry(entry, NESTED_EVENT); |
527 | } | |
528 | } | |
529 | } | |
530 | ||
531 | 45 | if (listenerList.getListenerCount() > 0) { |
532 | 45 | dispatchCacheGroupEvent(CacheEntryEventType.GROUP_FLUSHED, group, origin); |
533 | } | |
534 | } | |
535 | ||
536 | /** | |
537 | * Flush all entries with keys that match a given pattern | |
538 | * | |
539 | * @param pattern The key must contain this given value | |
540 | * @deprecated For performance and flexibility reasons it is preferable to | |
541 | * store cache entries in groups and use the {@link #flushGroup(String)} method | |
542 | * instead of relying on pattern flushing. | |
543 | */ | |
544 | 50 | public void flushPattern(String pattern) { |
545 | 50 | flushPattern(pattern, null); |
546 | } | |
547 | ||
548 | /** | |
549 | * Flush all entries with keys that match a given pattern | |
550 | * | |
551 | * @param pattern The key must contain this given value | |
552 | * @param origin The origin of this flush request | |
553 | * @deprecated For performance and flexibility reasons it is preferable to | |
554 | * store cache entries in groups and use the {@link #flushGroup(String, String)} | |
555 | * method instead of relying on pattern flushing. | |
556 | */ | |
557 | 50 | public void flushPattern(String pattern, String origin) { |
558 | // Check the pattern | |
559 | 50 | if ((pattern != null) && (pattern.length() > 0)) { |
560 | 30 | String key = null; |
561 | 30 | CacheEntry entry = null; |
562 | 30 | Iterator itr = cacheMap.keySet().iterator(); |
563 | ||
564 | 30 | while (itr.hasNext()) { |
565 | 90 | key = (String) itr.next(); |
566 | ||
567 | 90 | if (key.indexOf(pattern) >= 0) { |
568 | 10 | entry = (CacheEntry) cacheMap.get(key); |
569 | ||
570 | 10 | if (entry != null) { |
571 | 10 | flushEntry(entry, origin); |
572 | } | |
573 | } | |
574 | } | |
575 | ||
576 | 30 | if (listenerList.getListenerCount() > 0) { |
577 | 22 | dispatchCachePatternEvent(CacheEntryEventType.PATTERN_FLUSHED, pattern, origin); |
578 | } | |
579 | } else { | |
580 | // Empty pattern, nothing to do | |
581 | } | |
582 | } | |
583 | ||
584 | /** | |
585 | * Put an object in the cache specifying the key to use. | |
586 | * | |
587 | * @param key Key of the object in the cache. | |
588 | * @param content The object to cache. | |
589 | */ | |
590 | 10 | public void putInCache(String key, Object content) { |
591 | 10 | putInCache(key, content, null, null, null); |
592 | } | |
593 | ||
594 | /** | |
595 | * Put an object in the cache specifying the key and refresh policy to use. | |
596 | * | |
597 | * @param key Key of the object in the cache. | |
598 | * @param content The object to cache. | |
599 | * @param policy Object that implements refresh policy logic | |
600 | */ | |
601 | 65295 | public void putInCache(String key, Object content, EntryRefreshPolicy policy) { |
602 | 65295 | putInCache(key, content, null, policy, null); |
603 | } | |
604 | ||
605 | /** | |
606 | * Put in object into the cache, specifying both the key to use and the | |
607 | * cache groups the object belongs to. | |
608 | * | |
609 | * @param key Key of the object in the cache | |
610 | * @param content The object to cache | |
611 | * @param groups The cache groups to add the object to | |
612 | */ | |
613 | 105 | public void putInCache(String key, Object content, String[] groups) { |
614 | 105 | putInCache(key, content, groups, null, null); |
615 | } | |
616 | ||
617 | /** | |
618 | * Put an object into the cache specifying both the key to use and the | |
619 | * cache groups the object belongs to. | |
620 | * | |
621 | * @param key Key of the object in the cache | |
622 | * @param groups The cache groups to add the object to | |
623 | * @param content The object to cache | |
624 | * @param policy Object that implements the refresh policy logic | |
625 | */ | |
626 | 65410 | public void putInCache(String key, Object content, String[] groups, EntryRefreshPolicy policy, String origin) { |
627 | 65410 | CacheEntry cacheEntry = this.getCacheEntry(key, policy, origin); |
628 | 65405 | boolean isNewEntry = cacheEntry.isNew(); |
629 | ||
630 | // [CACHE-118] If we have an existing entry, create a new CacheEntry so we can still access the old one later | |
631 | 65405 | if (!isNewEntry) { |
632 | 5080 | cacheEntry = new CacheEntry(key, policy); |
633 | } | |
634 | ||
635 | 65405 | cacheEntry.setContent(content); |
636 | 65405 | cacheEntry.setGroups(groups); |
637 | 65405 | cacheMap.put(key, cacheEntry); |
638 | ||
639 | // Signal to any threads waiting on this update that it's now ready for them | |
640 | // in the cache! | |
641 | 65405 | completeUpdate(key); |
642 | ||
643 | 65405 | if (listenerList.getListenerCount() > 0) { |
644 | 199 | CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin); |
645 | ||
646 | 199 | if (isNewEntry) { |
647 | 138 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_ADDED, event); |
648 | } else { | |
649 | 61 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_UPDATED, event); |
650 | } | |
651 | } | |
652 | } | |
653 | ||
654 | /** | |
655 | * Unregister a listener for Cache events. | |
656 | * | |
657 | * @param listener The object that currently listens to events. | |
658 | * @param clazz The registrated class of listening object. | |
659 | * @deprecated use instead {@link #removeCacheEventListener(CacheEventListener)} | |
660 | */ | |
661 | 0 | public void removeCacheEventListener(CacheEventListener listener, Class clazz) { |
662 | 0 | listenerList.remove(clazz, listener); |
663 | } | |
664 | ||
665 | /** | |
666 | * Unregister a listener for Cache events. | |
667 | * | |
668 | * @param listener The object that currently listens to events. | |
669 | * @since 2.4 | |
670 | */ | |
671 | 150 | public void removeCacheEventListener(CacheEventListener listener) { |
672 | // listenerList.remove(CacheEventListener.class, listener); | |
673 | 150 | listenerList.remove(listener.getClass(), listener); |
674 | } | |
675 | ||
676 | /** | |
677 | * Get an entry from this cache or create one if it doesn't exist. | |
678 | * | |
679 | * @param key The key of the cache entry | |
680 | * @param policy Object that implements refresh policy logic | |
681 | * @param origin The origin of request (optional) | |
682 | * @return CacheEntry for the specified key. | |
683 | */ | |
684 | 4631000 | protected CacheEntry getCacheEntry(String key, EntryRefreshPolicy policy, String origin) { |
685 | 4630940 | CacheEntry cacheEntry = null; |
686 | ||
687 | // Verify that the key is valid | |
688 | 4631000 | if ((key == null) || (key.length() == 0)) { |
689 | 20 | throw new IllegalArgumentException("getCacheEntry called with an empty or null key"); |
690 | } | |
691 | ||
692 | 4630755 | cacheEntry = (CacheEntry) cacheMap.get(key); |
693 | ||
694 | // if the cache entry does not exist, create a new one | |
695 | 4630980 | if (cacheEntry == null) { |
696 | 120417 | if (log.isDebugEnabled()) { |
697 | 0 | log.debug("No cache entry exists for key='" + key + "', creating"); |
698 | } | |
699 | ||
700 | 120417 | cacheEntry = new CacheEntry(key, policy); |
701 | } | |
702 | ||
703 | 4630798 | return cacheEntry; |
704 | } | |
705 | ||
706 | /** | |
707 | * Indicates whether or not the cache entry is stale. | |
708 | * | |
709 | * @param cacheEntry The cache entry to test the freshness of. | |
710 | * @param refreshPeriod The maximum allowable age of the entry, in seconds. | |
711 | * @param cronExpiry A cron expression specifying absolute date(s) and/or time(s) | |
712 | * that the cache entry should expire at. If the cache entry was refreshed prior to | |
713 | * the most recent match for the cron expression, the entry will be considered stale. | |
714 | * | |
715 | * @return <code>true</code> if the entry is stale, <code>false</code> otherwise. | |
716 | */ | |
717 | 4565565 | protected boolean isStale(CacheEntry cacheEntry, int refreshPeriod, String cronExpiry) { |
718 | 4564560 | boolean result = cacheEntry.needsRefresh(refreshPeriod) || isFlushed(cacheEntry); |
719 | ||
720 | 4564932 | if ((!result) && (cronExpiry != null) && (cronExpiry.length() > 0)) { |
721 | 0 | try { |
722 | 0 | FastCronParser parser = new FastCronParser(cronExpiry); |
723 | 0 | result = result || parser.hasMoreRecentMatch(cacheEntry.getLastUpdate()); |
724 | } catch (ParseException e) { | |
725 | 0 | log.warn(e); |
726 | } | |
727 | } | |
728 | ||
729 | 4565565 | return result; |
730 | } | |
731 | ||
732 | /** | |
733 | * Get the updating cache entry from the update map. If one is not found, | |
734 | * create a new one (with state {@link EntryUpdateState#NOT_YET_UPDATING}) | |
735 | * and add it to the map. | |
736 | * | |
737 | * @param key The cache key for this entry | |
738 | * | |
739 | * @return the CacheEntry that was found (or added to) the updatingEntries | |
740 | * map. | |
741 | */ | |
742 | 4565262 | protected EntryUpdateState getUpdateState(String key) { |
743 | 4565262 | EntryUpdateState updateState; |
744 | ||
745 | 4565262 | synchronized (updateStates) { |
746 | // Try to find the matching state object in the updating entry map. | |
747 | 4565262 | updateState = (EntryUpdateState) updateStates.get(key); |
748 | ||
749 | 4565262 | if (updateState == null) { |
750 | // It's not there so add it. | |
751 | 205224 | updateState = new EntryUpdateState(); |
752 | 205224 | updateStates.put(key, updateState); |
753 | } else { | |
754 | //Otherwise indicate that we start using it to prevent its removal until all threads are done with it. | |
755 | 4360038 | updateState.incrementUsageCounter(); |
756 | } | |
757 | } | |
758 | ||
759 | 4565262 | return updateState; |
760 | } | |
761 | ||
762 | /** | |
763 | * releases the usage that was made of the specified EntryUpdateState. When this reaches zero, the entry is removed from the map. | |
764 | * @param state the state to release the usage of | |
765 | * @param key the associated key. | |
766 | */ | |
767 | 4565262 | protected void releaseUpdateState(EntryUpdateState state, String key) { |
768 | 4565262 | synchronized (updateStates) { |
769 | 4565262 | int usageCounter = state.decrementUsageCounter(); |
770 | 4565262 | checkEntryStateUpdateUsage(key, state, usageCounter); |
771 | } | |
772 | } | |
773 | ||
774 | /** | |
775 | * Completely clears the cache. | |
776 | */ | |
777 | 35 | protected void clear() { |
778 | 35 | cacheMap.clear(); |
779 | } | |
780 | ||
781 | /** | |
782 | * Removes the update state for the specified key and notifies any other | |
783 | * threads that are waiting on this object. This is called automatically | |
784 | * by the {@link #putInCache} method, so it is possible that no EntryUpdateState was hold | |
785 | * when this method is called. | |
786 | * | |
787 | * @param key The cache key that is no longer being updated. | |
788 | */ | |
789 | 65405 | protected void completeUpdate(String key) { |
790 | 65405 | EntryUpdateState state; |
791 | ||
792 | 65405 | synchronized (updateStates) { |
793 | 65405 | state = (EntryUpdateState) updateStates.get(key); |
794 | ||
795 | 65405 | if (state != null) { |
796 | 55045 | synchronized (state) { |
797 | 55045 | int usageCounter = state.completeUpdate(); |
798 | 55045 | state.notifyAll(); |
799 | ||
800 | 55045 | checkEntryStateUpdateUsage(key, state, usageCounter); |
801 | ||
802 | } | |
803 | } else { | |
804 | //If putInCache() was called directly (i.e. not as a result of a NeedRefreshException) then no EntryUpdateState would be found. | |
805 | } | |
806 | } | |
807 | } | |
808 | ||
809 | /** | |
810 | * Completely removes a cache entry from the cache and its associated cache | |
811 | * groups. | |
812 | * | |
813 | * @param key The key of the entry to remove. | |
814 | */ | |
815 | 0 | public void removeEntry(String key) { |
816 | 0 | removeEntry(key, null); |
817 | } | |
818 | ||
819 | /** | |
820 | * Completely removes a cache entry from the cache and its associated cache | |
821 | * groups. | |
822 | * | |
823 | * @param key The key of the entry to remove. | |
824 | * @param origin The origin of this remove request. | |
825 | */ | |
826 | 0 | protected void removeEntry(String key, String origin) { |
827 | 0 | CacheEntry cacheEntry = (CacheEntry) cacheMap.get(key); |
828 | 0 | cacheMap.remove(key); |
829 | ||
830 | 0 | if (listenerList.getListenerCount() > 0) { |
831 | 0 | CacheEntryEvent event = new CacheEntryEvent(this, cacheEntry, origin); |
832 | 0 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_REMOVED, event); |
833 | } | |
834 | } | |
835 | ||
836 | /** | |
837 | * Dispatch a cache entry event to all registered listeners. | |
838 | * | |
839 | * @param eventType The type of event (used to branch on the proper method) | |
840 | * @param event The event that was fired | |
841 | */ | |
842 | 279 | private void dispatchCacheEntryEvent(CacheEntryEventType eventType, CacheEntryEvent event) { |
843 | // Guaranteed to return a non-null array | |
844 | 279 | Object[] listeners = listenerList.getListenerList(); |
845 | ||
846 | // Process the listeners last to first, notifying | |
847 | // those that are interested in this event | |
848 | 279 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
849 | 565 | if (listeners[i+1] instanceof CacheEntryEventListener) { |
850 | 326 | CacheEntryEventListener listener = (CacheEntryEventListener) listeners[i+1]; |
851 | 326 | if (eventType.equals(CacheEntryEventType.ENTRY_ADDED)) { |
852 | 164 | listener.cacheEntryAdded(event); |
853 | 162 | } else if (eventType.equals(CacheEntryEventType.ENTRY_UPDATED)) { |
854 | 67 | listener.cacheEntryUpdated(event); |
855 | 95 | } else if (eventType.equals(CacheEntryEventType.ENTRY_FLUSHED)) { |
856 | 95 | listener.cacheEntryFlushed(event); |
857 | 0 | } else if (eventType.equals(CacheEntryEventType.ENTRY_REMOVED)) { |
858 | 0 | listener.cacheEntryRemoved(event); |
859 | } | |
860 | } | |
861 | } | |
862 | } | |
863 | ||
864 | /** | |
865 | * Dispatch a cache group event to all registered listeners. | |
866 | * | |
867 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
868 | * @param group The cache group that the event applies to | |
869 | * @param origin The origin of this event (optional) | |
870 | */ | |
871 | 45 | private void dispatchCacheGroupEvent(CacheEntryEventType eventType, String group, String origin) { |
872 | 45 | CacheGroupEvent event = new CacheGroupEvent(this, group, origin); |
873 | ||
874 | // Guaranteed to return a non-null array | |
875 | 45 | Object[] listeners = listenerList.getListenerList(); |
876 | ||
877 | // Process the listeners last to first, notifying | |
878 | // those that are interested in this event | |
879 | 45 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
880 | 99 | if (listeners[i+1] instanceof CacheEntryEventListener) { |
881 | 54 | CacheEntryEventListener listener = (CacheEntryEventListener) listeners[i + 1]; |
882 | 54 | if (eventType.equals(CacheEntryEventType.GROUP_FLUSHED)) { |
883 | 54 | listener.cacheGroupFlushed(event); |
884 | } | |
885 | } | |
886 | } | |
887 | } | |
888 | ||
889 | /** | |
890 | * Dispatch a cache map access event to all registered listeners. | |
891 | * | |
892 | * @param eventType The type of event | |
893 | * @param entry The entry that was affected. | |
894 | * @param origin The origin of this event (optional) | |
895 | */ | |
896 | 4565565 | private void dispatchCacheMapAccessEvent(CacheMapAccessEventType eventType, CacheEntry entry, String origin) { |
897 | 4565565 | CacheMapAccessEvent event = new CacheMapAccessEvent(eventType, entry, origin); |
898 | ||
899 | // Guaranteed to return a non-null array | |
900 | 4565565 | Object[] listeners = listenerList.getListenerList(); |
901 | ||
902 | // Process the listeners last to first, notifying | |
903 | // those that are interested in this event | |
904 | 4565565 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
905 | 597 | if (listeners[i+1] instanceof CacheMapAccessEventListener) { |
906 | 347 | CacheMapAccessEventListener listener = (CacheMapAccessEventListener) listeners[i + 1]; |
907 | 347 | listener.accessed(event); |
908 | } | |
909 | } | |
910 | } | |
911 | ||
912 | /** | |
913 | * Dispatch a cache pattern event to all registered listeners. | |
914 | * | |
915 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
916 | * @param pattern The cache pattern that the event applies to | |
917 | * @param origin The origin of this event (optional) | |
918 | */ | |
919 | 22 | private void dispatchCachePatternEvent(CacheEntryEventType eventType, String pattern, String origin) { |
920 | 22 | CachePatternEvent event = new CachePatternEvent(this, pattern, origin); |
921 | ||
922 | // Guaranteed to return a non-null array | |
923 | 22 | Object[] listeners = listenerList.getListenerList(); |
924 | ||
925 | // Process the listeners last to first, notifying | |
926 | // those that are interested in this event | |
927 | 22 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
928 | 46 | if (listeners[i+1] instanceof CacheEntryEventListener) { |
929 | 26 | if (eventType.equals(CacheEntryEventType.PATTERN_FLUSHED)) { |
930 | 26 | CacheEntryEventListener listener = (CacheEntryEventListener) listeners[i+1]; |
931 | 26 | listener.cachePatternFlushed(event); |
932 | } | |
933 | } | |
934 | } | |
935 | } | |
936 | ||
937 | /** | |
938 | * Dispatches a cache-wide event to all registered listeners. | |
939 | * | |
940 | * @param eventType The type of event (this is used to branch to the correct method handler) | |
941 | * @param origin The origin of this event (optional) | |
942 | */ | |
943 | 6 | private void dispatchCachewideEvent(CachewideEventType eventType, Date date, String origin) { |
944 | 6 | CachewideEvent event = new CachewideEvent(this, date, origin); |
945 | ||
946 | // Guaranteed to return a non-null array | |
947 | 6 | Object[] listeners = listenerList.getListenerList(); |
948 | ||
949 | // Process the listeners last to first, notifying | |
950 | // those that are interested in this event | |
951 | 6 | for (int i = listeners.length - 2; i >= 0; i -= 2) { |
952 | 12 | if (listeners[i+1] instanceof CacheEntryEventListener) { |
953 | 7 | if (eventType.equals(CachewideEventType.CACHE_FLUSHED)) { |
954 | 7 | CacheEntryEventListener listener = (CacheEntryEventListener) listeners[i+1]; |
955 | 7 | listener.cacheFlushed(event); |
956 | } | |
957 | } | |
958 | } | |
959 | } | |
960 | ||
961 | /** | |
962 | * Flush a cache entry. On completion of the flush, a | |
963 | * <tt>CacheEntryEventType.ENTRY_FLUSHED</tt> event is fired. | |
964 | * | |
965 | * @param entry The entry to flush | |
966 | * @param origin The origin of this flush event (optional) | |
967 | */ | |
968 | 84 | private void flushEntry(CacheEntry entry, String origin) { |
969 | 84 | String key = entry.getKey(); |
970 | ||
971 | // Flush the object itself | |
972 | 84 | entry.flush(); |
973 | ||
974 | 84 | if (!entry.isNew()) { |
975 | // Update the entry's state in the map | |
976 | 79 | cacheMap.put(key, entry); |
977 | } | |
978 | ||
979 | // Trigger an ENTRY_FLUSHED event. [CACHE-107] Do this for all flushes. | |
980 | 84 | if (listenerList.getListenerCount() > 0) { |
981 | 80 | CacheEntryEvent event = new CacheEntryEvent(this, entry, origin); |
982 | 80 | dispatchCacheEntryEvent(CacheEntryEventType.ENTRY_FLUSHED, event); |
983 | } | |
984 | } | |
985 | ||
986 | /** | |
987 | * @return the total number of cache entries held in this cache. | |
988 | */ | |
989 | 30 | public int getSize() { |
990 | 30 | synchronized(cacheMap) { |
991 | 30 | return cacheMap.size(); |
992 | } | |
993 | } | |
994 | ||
995 | /** | |
996 | * Test support only: return the number of EntryUpdateState instances within the updateStates map. | |
997 | */ | |
998 | 40 | protected int getNbUpdateState() { |
999 | 40 | synchronized(updateStates) { |
1000 | 40 | return updateStates.size(); |
1001 | } | |
1002 | } | |
1003 | ||
1004 | ||
1005 | /** | |
1006 | * Test support only: return the number of entries currently in the cache map | |
1007 | * @deprecated use getSize() | |
1008 | */ | |
1009 | 0 | public int getNbEntries() { |
1010 | 0 | synchronized(cacheMap) { |
1011 | 0 | return cacheMap.size(); |
1012 | } | |
1013 | } | |
1014 | } |
|