2
* Copyright (c) 2002-2003 by OpenSymphony
5
package com.opensymphony.oscache.web;
7
import com.opensymphony.oscache.base.*;
8
import com.opensymphony.oscache.base.events.CacheEventListener;
9
import com.opensymphony.oscache.base.events.ScopeEvent;
10
import com.opensymphony.oscache.base.events.ScopeEventListener;
11
import com.opensymphony.oscache.base.events.ScopeEventType;
13
import org.apache.commons.logging.Log;
14
import org.apache.commons.logging.LogFactory;
16
import java.io.Serializable;
20
import javax.servlet.ServletContext;
21
import javax.servlet.http.HttpServletRequest;
22
import javax.servlet.http.HttpSession;
23
import javax.servlet.jsp.PageContext;
26
* A ServletCacheAdministrator creates, flushes and administers the cache.
28
* This is a "servlet Singleton". This means it's not a Singleton in the traditional sense,
29
* that is stored in a static instance. It's a Singleton _per web app context_.
31
* Once created it manages the cache path on disk through the oscache.properties
32
* file, and also keeps track of the flush times.
34
* @author <a href="mailto:mike@atlassian.com">Mike Cannon-Brookes</a>
35
* @author <a href="mailto:tgochenour@peregrine.com">Todd Gochenour</a>
36
* @author <a href="mailto:fbeauregard@pyxis-tech.com">Francois Beauregard</a>
37
* @author <a href="mailto:abergevin@pyxis-tech.com">Alain Bergevin</a>
38
* @author <a href="mailto:chris@swebtec.com">Chris Miller</a>
39
* @version $Revision: 1.6 $
41
public class ServletCacheAdministrator extends AbstractCacheAdministrator implements Serializable {
42
private static final transient Log log = LogFactory.getLog(ServletCacheAdministrator.class);
45
* Constants for properties read/written from/to file
47
private final static String CACHE_USE_HOST_DOMAIN_KEY = "cache.use.host.domain.in.key";
48
private final static String CACHE_KEY_KEY = "cache.key";
51
* The default cache key that is used to store the cache in context.
53
private final static String DEFAULT_CACHE_KEY = "__oscache_cache";
56
* Constants for scope's name
58
public final static String SESSION_SCOPE_NAME = "session";
59
public final static String APPLICATION_SCOPE_NAME = "application";
62
* The key under which the CacheAdministrator will be stored in the ServletContext
64
private final static String CACHE_ADMINISTRATOR_KEY = "__oscache_admin";
67
* Key used to store the current scope in the configuration. This is a hack
68
* to let the scope information get passed through to the DiskPersistenceListener,
69
* and will be removed in a future release.
71
public final static String HASH_KEY_SCOPE = "scope";
74
* Key used to store the current session ID in the configuration. This is a hack
75
* to let the scope information get passed through to the DiskPersistenceListener,
76
* and will be removed in a future release.
78
public final static String HASH_KEY_SESSION_ID = "sessionId";
81
* Key used to store the servlet container temporary directory in the configuration.
82
* This is a hack to let the scope information get passed through to the
83
* DiskPersistenceListener, and will be removed in a future release.
85
public final static String HASH_KEY_CONTEXT_TMPDIR = "context.tempdir";
88
* The string to use as a file separator.
90
private final static String FILE_SEPARATOR = "/";
93
* The character to use as a file separator.
95
private final static char FILE_SEPARATOR_CHAR = FILE_SEPARATOR.charAt(0);
98
* Constant for Key generation.
100
private final static short AVERAGE_KEY_LENGTH = 30;
103
* Usable caracters for key generation
105
private static final String m_strBase64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
108
* Map containing the flush times of different scopes
110
private Map flushTimes;
112
//private transient ServletContext context;
115
* Key to use for storing and retrieving Object in contexts (Servlet, session).
117
private String cacheKey;
120
* Set property cache.use.host.domain.in.key=true to add domain information to key
121
* generation for hosting multiple sites.
123
private boolean useHostDomainInKey = false;
126
* Create the cache administrator.
128
* This will reset all the flush times and load the properties file.
130
private ServletCacheAdministrator(ServletContext context, Properties p) {
132
config.set(HASH_KEY_CONTEXT_TMPDIR, context.getAttribute("javax.servlet.context.tempdir"));
134
//this.context = context;
135
flushTimes = new HashMap();
136
initHostDomainInKey();
138
if (log.isInfoEnabled()) {
139
log.info("Constructed ServletCacheAdministrator()");
144
* Obtain an instance of the CacheAdministrator
146
* @param context The ServletContext that this CacheAdministrator is a Singleton under
147
* @return Returns the CacheAdministrator instance for this context
149
public static ServletCacheAdministrator getInstance(ServletContext context) {
150
return getInstance(context, null);
154
* Obtain an instance of the CacheAdministrator
156
* @param context The ServletContext that this CacheAdministrator is a Singleton under
157
* @param p the properties to use for the cache if the cache administrator has not been
158
* created yet. Once the administrator has been created, the properties parameter is
159
* ignored for all future invocations. If a null value is passed in, then the properties
160
* are loaded from the oscache.properties file in the classpath.
161
* @return Returns the CacheAdministrator instance for this context
163
public static ServletCacheAdministrator getInstance(ServletContext context, Properties p) {
164
ServletCacheAdministrator admin = null;
165
admin = (ServletCacheAdministrator) context.getAttribute(CACHE_ADMINISTRATOR_KEY);
167
// First time we need to create the administrator and store it in the
170
admin = new ServletCacheAdministrator(context, p);
171
context.setAttribute(CACHE_ADMINISTRATOR_KEY, admin);
173
if (log.isInfoEnabled()) {
174
log.info("Created new instance of ServletCacheAdministrator");
177
admin.getAppScopeCache(context);
184
* Shuts down the cache administrator. This should usually only be called
185
* when the controlling application shuts down.
187
public static void destroyInstance(ServletContext context) {
188
ServletCacheAdministrator admin = null;
189
admin = (ServletCacheAdministrator) context.getAttribute(CACHE_ADMINISTRATOR_KEY);
192
// Finalize the application scope cache
193
Cache cache = (Cache) context.getAttribute(admin.getCacheKey());
196
admin.finalizeListeners(cache);
197
context.removeAttribute(admin.getCacheKey());
198
context.removeAttribute(CACHE_ADMINISTRATOR_KEY);
201
if (log.isInfoEnabled()) {
202
log.info("Shut down the ServletCacheAdministrator");
211
* Grabs the cache for the specified scope
213
* @param request The current request
214
* @param scope The scope of this cache (<code>PageContext.APPLICATION_SCOPE</code>
215
* or <code>PageContext.SESSION_SCOPE</code>)
218
public Cache getCache(HttpServletRequest request, int scope) {
219
if (scope == PageContext.APPLICATION_SCOPE) {
220
return getAppScopeCache(request.getSession(true).getServletContext());
223
if (scope == PageContext.SESSION_SCOPE) {
224
return getSessionScopeCache(request.getSession(true));
227
throw new RuntimeException("The supplied scope value of " + scope + " is invalid. Acceptable values are PageContext.APPLICATION_SCOPE and PageContext.SESSION_SCOPE");
231
* A convenience method to retrieve the application scope cache
233
* @param context the current <code>ServletContext</code>
234
* @return the application scope cache. If none is present, one will
237
public Cache getAppScopeCache(ServletContext context) {
239
Object obj = context.getAttribute(getCacheKey());
241
if ((obj == null) || !(obj instanceof Cache)) {
242
if (log.isInfoEnabled()) {
243
log.info("Created new application-scoped cache at key: " + getCacheKey());
246
cache = createCache(PageContext.APPLICATION_SCOPE, null);
247
context.setAttribute(getCacheKey(), cache);
256
* A convenience method to retrieve the session scope cache
258
* @param session the current <code>HttpSession</code>
259
* @return the session scope cache for this session. If none is present,
260
* one will be created.
262
public Cache getSessionScopeCache(HttpSession session) {
264
Object obj = session.getAttribute(getCacheKey());
266
if ((obj == null) || !(obj instanceof Cache)) {
267
if (log.isInfoEnabled()) {
268
log.info("Created new session-scoped cache in session " + session.getId() + " at key: " + getCacheKey());
271
cache = createCache(PageContext.SESSION_SCOPE, session.getId());
272
session.setAttribute(getCacheKey(), cache);
281
* Get the cache key from the properties. Set it to a default value if it
282
* is not present in the properties
284
* @return The cache.key property or the DEFAULT_CACHE_KEY
286
public String getCacheKey() {
287
if (cacheKey == null) {
288
cacheKey = getProperty(CACHE_KEY_KEY);
290
if (cacheKey == null) {
291
cacheKey = DEFAULT_CACHE_KEY;
299
* Set the flush time for a specific scope to a specific time
301
* @param date The time to flush the scope
302
* @param scope The scope to be flushed
304
public void setFlushTime(Date date, int scope) {
305
if (log.isInfoEnabled()) {
306
log.info("Flushing scope " + scope + " at " + date);
309
synchronized (flushTimes) {
311
// Trigger a SCOPE_FLUSHED event
312
dispatchScopeEvent(ScopeEventType.SCOPE_FLUSHED, scope, date, null);
313
flushTimes.put(new Integer(scope), date);
315
logError("setFlushTime called with a null date.");
316
throw new IllegalArgumentException("setFlushTime called with a null date.");
322
* Set the flush time for a specific scope to the current time.
324
* @param scope The scope to be flushed
326
public void setFlushTime(int scope) {
327
setFlushTime(new Date(), scope);
331
* Get the flush time for a particular scope.
333
* @param scope The scope to get the flush time for.
334
* @return A date representing the time this scope was last flushed.
335
* Returns null if it has never been flushed.
337
public Date getFlushTime(int scope) {
338
synchronized (flushTimes) {
339
return (Date) flushTimes.get(new Integer(scope));
344
* Retrieve an item from the cache
346
* @param scope The cache scope
347
* @param request The servlet request
348
* @param key The key of the object to retrieve
349
* @param refreshPeriod The time interval specifying if an entry needs refresh
350
* @return The requested object
351
* @throws NeedsRefreshException
353
public Object getFromCache(int scope, HttpServletRequest request, String key, int refreshPeriod) throws NeedsRefreshException {
354
Cache cache = getCache(request, scope);
355
key = this.generateEntryKey(key, request, scope);
356
return cache.getFromCache(key, refreshPeriod);
360
* Checks if the given scope was flushed more recently than the CacheEntry provided.
361
* Used to determine whether to refresh the particular CacheEntry.
363
* @param cacheEntry The cache entry which we're seeing whether to refresh
364
* @param scope The scope we're checking
366
* @return Whether or not the scope has been flushed more recently than this cache entry was updated.
368
public boolean isScopeFlushed(CacheEntry cacheEntry, int scope) {
369
Date flushDateTime = getFlushTime(scope);
371
if (flushDateTime != null) {
372
long lastUpdate = cacheEntry.getLastUpdate();
373
return (flushDateTime.getTime() >= lastUpdate);
380
* Register a listener for Cache Map events.
382
* @param listener The object that listens to events.
384
public void addScopeEventListener(ScopeEventListener listener) {
385
listenerList.add(ScopeEventListener.class, listener);
389
* Cancels a pending cache update. This should only be called by a thread
390
* that received a {@link NeedsRefreshException} and was unable to generate
391
* some new cache content.
393
* @param scope The cache scope
394
* @param request The servlet request
395
* @param key The cache entry key to cancel the update of.
397
public void cancelUpdate(int scope, HttpServletRequest request, String key) {
398
Cache cache = getCache(request, scope);
399
key = this.generateEntryKey(key, request, scope);
400
cache.cancelUpdate(key);
404
* Flush all scopes at a particular time
406
* @param date The time to flush the scope
408
public void flushAll(Date date) {
409
synchronized (flushTimes) {
410
setFlushTime(date, PageContext.APPLICATION_SCOPE);
411
setFlushTime(date, PageContext.SESSION_SCOPE);
412
setFlushTime(date, PageContext.REQUEST_SCOPE);
413
setFlushTime(date, PageContext.PAGE_SCOPE);
416
// Trigger a flushAll event
417
dispatchScopeEvent(ScopeEventType.ALL_SCOPES_FLUSHED, -1, date, null);
421
* Flush all scopes instantly.
423
public void flushAll() {
424
flushAll(new Date());
428
* Generates a cache entry key.
430
* If the string key is not specified, the HTTP request URI and QueryString is used.
431
* Operating systems that have a filename limitation less than 255 or have
432
* filenames that are case insensitive may have issues with key generation where
433
* two distinct pages map to the same key.
435
* POST Requests (which have no distinguishing
436
* query string) may also generate identical keys for what is actually different pages.
437
* In these cases, specify an explicit key attribute for the CacheTag.
439
* @param key The key entered by the user
440
* @param request The current request
441
* @param scope The scope this cache entry is under
442
* @return The generated cache key
444
public String generateEntryKey(String key, HttpServletRequest request, int scope) {
445
return generateEntryKey(key, request, scope, null, null);
449
* Generates a cache entry key.
451
* If the string key is not specified, the HTTP request URI and QueryString is used.
452
* Operating systems that have a filename limitation less than 255 or have
453
* filenames that are case insensitive may have issues with key generation where
454
* two distinct pages map to the same key.
456
* POST Requests (which have no distinguishing
457
* query string) may also generate identical keys for what is actually different pages.
458
* In these cases, specify an explicit key attribute for the CacheTag.
460
* @param key The key entered by the user
461
* @param request The current request
462
* @param scope The scope this cache entry is under
463
* @param language The ISO-639 language code to distinguish different pages in application scope
464
* @return The generated cache key
466
public String generateEntryKey(String key, HttpServletRequest request, int scope, String language) {
467
return generateEntryKey(key, request, scope, language, null);
471
* Generates a cache entry key.
473
* If the string key is not specified, the HTTP request URI and QueryString is used.
474
* Operating systems that have a filename limitation less than 255 or have
475
* filenames that are case insensitive may have issues with key generation where
476
* two distinct pages map to the same key.
478
* POST Requests (which have no distinguishing
479
* query string) may also generate identical keys for what is actually different pages.
480
* In these cases, specify an explicit key attribute for the CacheTag.
482
* @param key The key entered by the user
483
* @param request The current request
484
* @param scope The scope this cache entry is under
485
* @param language The ISO-639 language code to distinguish different pages in application scope
486
* @param suffix The ability to put a suffix at the end of the key
487
* @return The generated cache key
489
public String generateEntryKey(String key, HttpServletRequest request, int scope, String language, String suffix) {
491
* Used for generating cache entry keys.
493
StringBuffer cBuffer = new StringBuffer(AVERAGE_KEY_LENGTH);
495
// Append the language if available
496
if (language != null) {
497
cBuffer.append(FILE_SEPARATOR).append(language);
500
// Servers for multiple host domains need this distinction in the key
501
if (useHostDomainInKey) {
502
cBuffer.append(FILE_SEPARATOR).append(request.getServerName());
506
cBuffer.append(FILE_SEPARATOR).append(key);
508
String generatedKey = request.getRequestURI();
510
if (generatedKey.charAt(0) != FILE_SEPARATOR_CHAR) {
511
cBuffer.append(FILE_SEPARATOR_CHAR);
514
cBuffer.append(generatedKey);
515
cBuffer.append("_").append(request.getMethod()).append("_");
517
generatedKey = getSortedQueryString(request);
519
if (generatedKey != null) {
521
java.security.MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
522
byte[] b = digest.digest(generatedKey.getBytes());
525
// Base64 encoding allows for unwanted slash characters.
526
cBuffer.append(toBase64(b).replace('/', '_'));
527
} catch (Exception e) {
528
// Ignore query string
533
// Do we want a suffix
534
if ((suffix != null) && (suffix.length() > 0)) {
535
cBuffer.append(suffix);
538
return cBuffer.toString();
542
* Creates a string that contains all of the request parameters and their
543
* values in a single string. This is very similar to
544
* <code>HttpServletRequest.getQueryString()</code> except the parameters are
545
* sorted by name, and if there is a <code>jsessionid</code> parameter it is
547
* If the request has no parameters, this method returns <code>null</code>.
549
protected String getSortedQueryString(HttpServletRequest request) {
550
Map paramMap = request.getParameterMap();
552
if (paramMap.isEmpty()) {
556
Set paramSet = new TreeMap(paramMap).entrySet();
558
StringBuffer buf = new StringBuffer();
560
boolean first = true;
562
for (Iterator it = paramSet.iterator(); it.hasNext();) {
563
Map.Entry entry = (Map.Entry) it.next();
564
String[] values = (String[]) entry.getValue();
566
for (int i = 0; i < values.length; i++) {
567
String key = (String) entry.getKey();
569
if ((key.length() != 10) || !"jsessionid".equals(key)) {
576
buf.append(key).append('=').append(values[i]);
581
// We get a 0 length buffer if the only parameter was a jsessionid
582
if (buf.length() == 0) {
585
return buf.toString();
590
* Log error messages to commons logging.
592
* @param message Message to log.
594
public void logError(String message) {
595
log.error("[oscache]: " + message);
599
* Put an object in the cache
601
* @param scope The cache scope
602
* @param request The servlet request
603
* @param key The object key
604
* @param content The object to add
606
public void putInCache(int scope, HttpServletRequest request, String key, Object content) {
607
putInCache(scope, request, key, content, null);
611
* Put an object in the cache
613
* @param scope The cache scope
614
* @param request The servlet request
615
* @param key The object key
616
* @param content The object to add
617
* @param policy The refresh policy
619
public void putInCache(int scope, HttpServletRequest request, String key, Object content, EntryRefreshPolicy policy) {
620
Cache cache = getCache(request, scope);
621
key = this.generateEntryKey(key, request, scope);
622
cache.putInCache(key, content, policy);
626
* Sets the cache capacity (number of items). If the cache contains
627
* more than <code>capacity</code> items then items will be removed
628
* to bring the cache back down to the new size.
630
* @param scope The cache scope
631
* @param request The servlet request
632
* @param capacity The new capacity
634
public void setCacheCapacity(int scope, HttpServletRequest request, int capacity) {
635
setCacheCapacity(capacity);
636
getCache(request, scope).setCapacity(capacity);
640
* Unregister a listener for Cache Map events.
642
* @param listener The object that currently listens to events.
644
public void removeScopeEventListener(ScopeEventListener listener) {
645
listenerList.remove(ScopeEventListener.class, listener);
649
* Finalizes all the listeners that are associated with the given cache object
651
protected void finalizeListeners(Cache cache) {
652
super.finalizeListeners(cache);
656
* Convert a byte array into a Base64 string (as used in mime formats)
658
private static String toBase64(byte[] aValue) {
662
int iByteLen = aValue.length;
663
StringBuffer tt = new StringBuffer();
665
for (int i = 0; i < iByteLen; i += 3) {
666
boolean bByte2 = (i + 1) < iByteLen;
667
boolean bByte3 = (i + 2) < iByteLen;
668
byte1 = aValue[i] & 0xFF;
669
byte2 = (bByte2) ? (aValue[i + 1] & 0xFF) : 0;
670
byte3 = (bByte3) ? (aValue[i + 2] & 0xFF) : 0;
672
tt.append(m_strBase64Chars.charAt(byte1 / 4));
673
tt.append(m_strBase64Chars.charAt((byte2 / 16) + ((byte1 & 0x3) * 16)));
674
tt.append(((bByte2) ? m_strBase64Chars.charAt((byte3 / 64) + ((byte2 & 0xF) * 4)) : '='));
675
tt.append(((bByte3) ? m_strBase64Chars.charAt(byte3 & 0x3F) : '='));
678
return tt.toString();
684
* @param scope The cache scope
685
* @param sessionId The sessionId for with the cache will be created
686
* @return A new cache
688
private ServletCache createCache(int scope, String sessionId) {
689
if (log.isInfoEnabled()) {
690
log.info("Created new cache in scope " + scope);
693
ServletCache newCache = new ServletCache(this, algorithmClass, cacheCapacity, scope);
695
// TODO - Fix me please!
696
// Hack! This is nasty - if two sessions are created within a short
697
// space of time it is possible they will end up with duplicate
698
// session IDs being passed to the DiskPersistenceListener!...
699
config.set(HASH_KEY_SCOPE, "" + scope);
700
config.set(HASH_KEY_SESSION_ID, sessionId);
702
newCache = (ServletCache) configureStandardListeners(newCache);
704
if (config.getProperty(CACHE_ENTRY_EVENT_LISTENERS) != null) {
705
// Add any event listeners that have been specified in the configuration
706
CacheEventListener[] listeners = getCacheEventListeners();
708
for (int i = 0; i < listeners.length; i++) {
709
if (listeners[i] instanceof ScopeEventListener) {
710
newCache.addCacheEventListener(listeners[i], ScopeEventListener.class);
719
* Dispatch a scope event to all registered listeners.
721
* @param eventType The type of event
722
* @param scope Scope that was flushed (Does not apply for FLUSH_ALL event)
723
* @param date Date of flushing
724
* @param origin The origin of the event
726
private void dispatchScopeEvent(ScopeEventType eventType, int scope, Date date, String origin) {
728
ScopeEvent event = new ScopeEvent(eventType, scope, date, origin);
730
// Guaranteed to return a non-null array
731
Object[] listeners = listenerList.getListenerList();
733
// Process the listeners last to first, notifying
734
// those that are interested in this event
735
for (int i = listeners.length - 2; i >= 0; i -= 2) {
736
if (listeners[i] == ScopeEventListener.class) {
737
((ScopeEventListener) listeners[i + 1]).scopeFlushed(event);
743
* Set property cache.use.host.domain.in.key=true to add domain information to key
744
* generation for hosting multiple sites
746
private void initHostDomainInKey() {
747
String propStr = getProperty(CACHE_USE_HOST_DOMAIN_KEY);
749
useHostDomainInKey = "true".equalsIgnoreCase(propStr);