001: /* ***** BEGIN LICENSE BLOCK *****
002: * Version: MPL 1.1
003: * The contents of this file are subject to the Mozilla Public License Version
004: * 1.1 (the "License"); you may not use this file except in compliance with
005: * the License. You may obtain a copy of the License at
006: * http://www.mozilla.org/MPL/
007: *
008: * Software distributed under the License is distributed on an "AS IS" basis,
009: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
010: * for the specific language governing rights and limitations under the
011: * License.
012: *
013: * The Original Code is Riot.
014: *
015: * The Initial Developer of the Original Code is
016: * Neteye GmbH.
017: * Portions created by the Initial Developer are Copyright (C) 2007
018: * the Initial Developer. All Rights Reserved.
019: *
020: * Contributor(s):
021: * Felix Gnass [fgnass at neteye dot de]
022: *
023: * ***** END LICENSE BLOCK ***** */
024: package org.riotfamily.cachius;
025:
026: import java.io.IOException;
027: import java.util.Enumeration;
028: import java.util.Map;
029: import java.util.regex.Matcher;
030: import java.util.regex.Pattern;
031:
032: import javax.servlet.http.HttpServletRequest;
033: import javax.servlet.http.HttpServletResponse;
034:
035: import org.apache.commons.logging.Log;
036: import org.apache.commons.logging.LogFactory;
037: import org.riotfamily.cachius.spring.CacheableController;
038: import org.riotfamily.cachius.support.ReaderWriterLock;
039: import org.riotfamily.cachius.support.SessionCreationPreventingRequestWrapper;
040: import org.riotfamily.cachius.support.SessionIdEncoder;
041: import org.riotfamily.common.web.collaboration.SharedProperties;
042: import org.riotfamily.common.web.util.ServletUtils;
043: import org.springframework.util.StringUtils;
044: import org.springframework.web.util.WebUtils;
045:
046: /**
047: * @author Felix Gnass [fgnass at neteye dot de]
048: * @since 6.5
049: */
050: public class CacheService {
051:
052: private static Pattern IE_MAJOR_VERSION_PATTERN = Pattern
053: .compile("^Mozilla/\\d\\.\\d+ \\(compatible[-;] MSIE (\\d)");
054:
055: private static Pattern BUGGY_NETSCAPE_PATTERN = Pattern
056: .compile("^Mozilla/4\\.0[678]");
057:
058: private static Log log = LogFactory.getLog(CacheService.class);
059:
060: private Cache cache;
061:
062: public CacheService(Cache cache) {
063: this .cache = cache;
064: }
065:
066: public long getLastModified(HttpServletRequest request,
067: CacheableRequestProcessor processor) {
068:
069: String cacheKey = processor.getCacheKey(request);
070: boolean zip = processor.responseShouldBeZipped(request)
071: && responseCanBeZipped(request);
072:
073: SessionIdEncoder sessionIdEncoder = new SessionIdEncoder(
074: request);
075: CacheItem cacheItem = getCacheItem(cacheKey, sessionIdEncoder,
076: zip);
077: if (cacheItem != null) {
078: if (!cacheItem.isNew()) {
079: long now = System.currentTimeMillis();
080: long ttl = processor.getTimeToLive();
081: if (ttl == CacheableController.CACHE_ETERNALLY
082: || cacheItem.getLastCheck() + ttl <= now) {
083:
084: return cacheItem.getLastModified();
085: }
086: }
087: try {
088: return processor.getLastModified(request);
089: } catch (Exception e) {
090: log.error("Error invoking the last-modified method", e);
091: }
092: }
093: return -1L;
094: }
095:
096: public void serve(HttpServletRequest request,
097: HttpServletResponse response,
098: CacheableRequestProcessor processor) throws Exception {
099:
100: boolean shouldZip = processor.responseShouldBeZipped(request);
101: boolean zip = shouldZip && responseCanBeZipped(request);
102:
103: String cacheKey = processor.getCacheKey(request);
104: SessionIdEncoder sessionIdEncoder = new SessionIdEncoder(
105: request);
106: CacheItem cacheItem = getCacheItem(cacheKey, sessionIdEncoder,
107: zip);
108:
109: if (cacheItem == null) {
110: log.debug("No CacheItem for "
111: + ServletUtils.getRequestUri(request)
112: + " - Response won't be cached.");
113:
114: processor.processRequest(request, response);
115: } else {
116: long mtime = getLastModified(cacheItem, processor, request);
117: if (mtime > cacheItem.getLastModified()) {
118: capture(cacheItem, request, response, sessionIdEncoder,
119: mtime, processor, shouldZip, zip);
120: } else {
121: if (!serve(cacheItem, request, response,
122: sessionIdEncoder)) {
123: // The rare case, that the item was deleted due to a cleanup
124: capture(cacheItem, request, response,
125: sessionIdEncoder, mtime, processor,
126: shouldZip, zip);
127: }
128: }
129: }
130: }
131:
132: private CacheItem getCacheItem(String cacheKey,
133: SessionIdEncoder sessionIdEncoder, boolean zip) {
134:
135: if (cacheKey == null) {
136: return null;
137: }
138:
139: if (sessionIdEncoder.urlsNeedEncoding()) {
140: cacheKey += ";jsessionid";
141: }
142:
143: if (zip) {
144: cacheKey += ".gz";
145: }
146:
147: CacheItem cacheItem = cache.getItem(cacheKey);
148: if (cacheItem != null
149: && (cacheItem.isNew() || !cacheItem.exists())) {
150: cacheItem.setFilterSessionId(sessionIdEncoder
151: .urlsNeedEncoding());
152: }
153: return cacheItem;
154: }
155:
156: /**
157: *
158: */
159: private long getLastModified(CacheItem cacheItem,
160: CacheableRequestProcessor processor,
161: HttpServletRequest request) throws Exception {
162:
163: long now = System.currentTimeMillis();
164:
165: // No need to check if the item has just been constructed or
166: // the cache file has been deleted
167: if (cacheItem.isNew() || !cacheItem.exists()) {
168: return now;
169: }
170:
171: long ttl = processor.getTimeToLive();
172: if (ttl == CacheableController.CACHE_ETERNALLY) {
173: return 0;
174: }
175: if (cacheItem.getLastCheck() + ttl < now) {
176: long mtime = processor.getLastModified(request);
177: cacheItem.setLastCheck(now);
178: if (mtime > cacheItem.getLastModified()) {
179: return mtime;
180: }
181: }
182: return cacheItem.getLastModified();
183: }
184:
185: private void capture(CacheItem cacheItem,
186: HttpServletRequest request, HttpServletResponse response,
187: SessionIdEncoder sessionIdEncoder, long mtime,
188: CacheableRequestProcessor processor, boolean shouldZip,
189: boolean zip) throws Exception {
190:
191: if (log.isDebugEnabled()) {
192: log.debug("Updating cache item " + cacheItem.getKey());
193: }
194: CachiusResponseWrapper wrapper = new CachiusResponseWrapper(
195: response, cacheItem, sessionIdEncoder);
196:
197: ReaderWriterLock lock = cacheItem.getLock();
198: try {
199: // Acquire a writer lock ...
200: lock.lockForWriting();
201: // Check if another writer has already updated the item
202: if (mtime > cacheItem.getLastModified()) {
203: TaggingContext ctx = TaggingContext
204: .openNestedContext(request);
205: Map propertySnapshot = SharedProperties
206: .getSnapshot(request);
207: request = new SessionCreationPreventingRequestWrapper(
208: request);
209: processor.processRequest(request, wrapper);
210: ctx.close();
211: Map props = SharedProperties.getDiff(request,
212: propertySnapshot);
213: cacheItem.setProperties(props);
214: cache.tagItem(cacheItem, ctx.getTags());
215: wrapper.stopCapturing();
216: if (wrapper.isOk() && !ctx.isPreventCaching()) {
217: cacheItem.setLastModified(mtime);
218: } else {
219: cacheItem.invalidate();
220: }
221: if (cacheItem.getSize() > 0) {
222: if (shouldZip) {
223: wrapper.setHeader("Vary",
224: "Accept-Encoding, User-Agent");
225: if (zip) {
226: cacheItem.gzipContent();
227: wrapper.setHeader("Content-Encoding",
228: "gzip");
229: }
230: }
231: }
232: wrapper.updateHeaders();
233: } else {
234: log
235: .debug("Item has already been updated by another thread");
236: }
237: cacheItem
238: .writeTo(response, sessionIdEncoder.getSessionId());
239: } finally {
240: lock.releaseWriterLock();
241: }
242: }
243:
244: private boolean serve(CacheItem cacheItem,
245: HttpServletRequest request, HttpServletResponse response,
246: SessionIdEncoder sessionIdEncoder) throws IOException {
247:
248: if (log.isDebugEnabled()) {
249: log
250: .debug("Serving cached version of "
251: + cacheItem.getKey());
252: }
253:
254: ReaderWriterLock lock = cacheItem.getLock();
255: try {
256: // Acquire a reader lock and serve the cached version
257: lock.lockForReading();
258: if (!cacheItem.exists()) {
259: return false;
260: }
261: cacheItem
262: .writeTo(response, sessionIdEncoder.getSessionId());
263: SharedProperties.setProperties(request, cacheItem
264: .getProperties());
265: } finally {
266: lock.releaseReaderLock();
267: }
268: return true;
269: }
270:
271: public boolean isCached(String key) {
272: return cache.containsKey(key);
273: }
274:
275: /**
276: * Checks whether the response can be compressed. This is the case when
277: * {@link #clientAcceptsGzip(HttpServletRequest) the client accepts gzip
278: * encoded content}, the {@link #userAgentHasGzipBugs(HttpServletRequest)
279: * user-agent has no known gzip-related bugs} and the request is not an
280: * {@link WebUtils#isIncludeRequest(javax.servlet.ServletRequest)
281: * include request}.
282: */
283: protected boolean responseCanBeZipped(HttpServletRequest request) {
284: return clientAcceptsGzip(request)
285: && !userAgentHasGzipBugs(request)
286: && !WebUtils.isIncludeRequest(request);
287: }
288:
289: /**
290: * Returns whether the Accept-Encoding header contains "gzip".
291: */
292: protected boolean clientAcceptsGzip(HttpServletRequest request) {
293: Enumeration values = request.getHeaders("Accept-Encoding");
294: if (values != null) {
295: while (values.hasMoreElements()) {
296: String value = (String) values.nextElement();
297: if (value.indexOf("gzip") != -1) {
298: return true;
299: }
300: }
301: }
302: return false;
303: }
304:
305: /**
306: * Returns whether the User-Agent has known gzip-related bugs. This is true
307: * for Internet Explorer < 6.0 SP2 and Mozilla 4.06, 4.07 and 4.08. The
308: * method will also return true if the User-Agent header is not present or
309: * empty.
310: */
311: protected boolean userAgentHasGzipBugs(HttpServletRequest request) {
312: String ua = request.getHeader("User-Agent");
313: if (!StringUtils.hasLength(ua)) {
314: return true;
315: }
316: Matcher m = IE_MAJOR_VERSION_PATTERN.matcher(ua);
317: if (m.find()) {
318: int major = Integer.parseInt(m.group(1));
319: if (major > 6) {
320: // Bugs are fixed in IE 7
321: return false;
322: }
323: if (ua.indexOf("Opera") != -1) {
324: // Opera has no known gzip bugs
325: return false;
326: }
327: if (major == 6) {
328: // Bugs are fixed in Service Pack 2
329: return ua.indexOf("SV1") == -1;
330: }
331: // All other version are buggy.
332: return true;
333: }
334: return BUGGY_NETSCAPE_PATTERN.matcher(ua).find();
335: }
336:
337: }
|