001: /* ***** BEGIN LICENSE BLOCK *****
002: * Version: MPL 1.1
003: * The contents of this file are subject to the Mozilla Public License Version
004: * 1.1 (the "License"); you may not use this file except in compliance with
005: * the License. You may obtain a copy of the License at
006: * http://www.mozilla.org/MPL/
007: *
008: * Software distributed under the License is distributed on an "AS IS" basis,
009: * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
010: * for the specific language governing rights and limitations under the
011: * License.
012: *
013: * The Original Code is Riot.
014: *
015: * The Initial Developer of the Original Code is
016: * Neteye GmbH.
017: * Portions created by the Initial Developer are Copyright (C) 2006
018: * the Initial Developer. All Rights Reserved.
019: *
020: * Contributor(s):
021: * Felix Gnass [fgnass at neteye dot de]
022: *
023: * ***** END LICENSE BLOCK ***** */
024: package org.riotfamily.cachius;
025:
026: import java.io.BufferedInputStream;
027: import java.io.BufferedReader;
028: import java.io.File;
029: import java.io.FileInputStream;
030: import java.io.FileNotFoundException;
031: import java.io.FileOutputStream;
032: import java.io.IOException;
033: import java.io.InputStream;
034: import java.io.InputStreamReader;
035: import java.io.ObjectInputStream;
036: import java.io.OutputStream;
037: import java.io.OutputStreamWriter;
038: import java.io.Reader;
039: import java.io.Serializable;
040: import java.io.UnsupportedEncodingException;
041: import java.io.Writer;
042: import java.util.Map;
043: import java.util.Set;
044: import java.util.zip.GZIPOutputStream;
045:
046: import javax.servlet.http.HttpServletResponse;
047:
048: import org.apache.commons.logging.Log;
049: import org.apache.commons.logging.LogFactory;
050: import org.riotfamily.cachius.support.Cookies;
051: import org.riotfamily.cachius.support.Headers;
052: import org.riotfamily.cachius.support.ReaderWriterLock;
053: import org.riotfamily.cachius.support.TokenFilterWriter;
054: import org.riotfamily.common.io.IOUtils;
055: import org.springframework.util.FileCopyUtils;
056:
057: /**
058: * Representation of cached item that is backed by a file. The captured
059: * HTTP headers are kept in memory and are serialized by the default java
060: * serialization mechanism. The actual content is read from a file to avoid
061: * the overhead of object deserialization on each request.
062: * <br>
063: * If URL rewriting is used to track the session, the sessionId is
064: * replaced by a special token in the cache file. When such an item is
065: * send to a client, the token is replaced with the current sessionId.
066: * <br>
067: * If the sessionId comes from a cookie or the controller outputs binary data,
068: * a direct stream copy is performed instead of using Readers/Writers to
069: * improve performance.
070: *
071: * @author Felix Gnass
072: */
073: class CacheItem implements Serializable {
074:
075: private static final String ITEM_PREFIX = "item";
076:
077: private static final String ITEM_SUFFIX = "";
078:
079: private static final long NOT_YET = -1L;
080:
081: private static final String FILE_ENCODING = "UTF-8";
082:
083: private Log log = LogFactory.getLog(CacheItem.class);
084:
085: /** The key used for lookups */
086: private String key;
087:
088: /** Set of tags to categorize the item */
089: private Set tags;
090:
091: /** Flag indicating whether session IDs are filtered */
092: private boolean filterSessionId;
093:
094: /** The file containing the actual data */
095: private File file;
096:
097: /** The Content-Type of the cached data */
098: private String contentType;
099:
100: /** Captured HTTP headers that will be sent */
101: private Headers headers;
102:
103: /** Captured cookies that will be sent */
104: private Cookies cookies;
105:
106: /** Map with extra properties */
107: private Map properties;
108:
109: /** Flag indicating whether the content is binary or character data */
110: private boolean binary = true;
111:
112: /**
113: * Reader/writer lock to prevent concurrent threads from updating
114: * the cached content while others are reading.
115: */
116: private transient ReaderWriterLock lock = new ReaderWriterLock();
117:
118: /** Time of the last usage */
119: private long lastUsed;
120:
121: /** Time of the last modification */
122: private long lastModified;
123:
124: /** Time of the last up-to-date check */
125: private long lastCheck;
126:
127: /** Whether to set Content-Length header or not */
128: private boolean setContentLength;
129:
130: /**
131: * Creates a new CacheItem with the given key in the specified directory.
132: */
133: protected CacheItem(String key, File cacheDir) throws IOException {
134: this .key = key;
135: file = File.createTempFile(ITEM_PREFIX, ITEM_SUFFIX, cacheDir);
136: lastModified = NOT_YET;
137: }
138:
139: /**
140: * Returns the key.
141: */
142: protected String getKey() {
143: return key;
144: }
145:
146: /**
147: * Sets tags which can be used to look up the item for invalidation.
148: */
149: protected void setTags(Set tags) {
150: this .tags = tags;
151: }
152:
153: /**
154: * Returns the item's tags.
155: */
156: public Set getTags() {
157: return this .tags;
158: }
159:
160: /**
161: * Returns whether the item is new. An item is considered as new if the
162: * {@link #getLastModified() lastModified} timestamp is set to
163: * {@value #NOT_YET}.
164: */
165: protected boolean isNew() {
166: return lastModified == NOT_YET;
167: }
168:
169: /**
170: * Sets the lastUsed timestamp to the current time.
171: */
172: protected void touch() {
173: this .lastUsed = System.currentTimeMillis();
174: }
175:
176: /**
177: * Returns the last usage time.
178: */
179: public long getLastUsed() {
180: return this .lastUsed;
181: }
182:
183: /**
184: * Returns the last modification time.
185: */
186: protected long getLastModified() {
187: return lastModified;
188: }
189:
190: /**
191: * Sets the last modification time.
192: */
193: protected void setLastModified(long lastModified) {
194: this .lastModified = lastModified;
195: }
196:
197: /**
198: * Invalidates the item by setting the {@link #setLastModified(long)
199: * lastModified} timestamp to {@value #NOT_YET}.
200: */
201: protected void invalidate() {
202: lastModified = NOT_YET;
203: }
204:
205: /**
206: * Returns the time when the last up-to-date check was performed.
207: */
208: protected long getLastCheck() {
209: return this .lastCheck;
210: }
211:
212: /**
213: * Sets the time of the last up-to-date check.
214: */
215: protected void setLastCheck(long lastCheck) {
216: this .lastCheck = lastCheck;
217: }
218:
219: /**
220: * Checks whether the cache file exists an is a regular file.
221: */
222: protected boolean exists() {
223: return file != null && file.isFile();
224: }
225:
226: /**
227: * Returns the size of the cached data in bytes.
228: */
229: protected int getSize() {
230: return file != null ? (int) file.length() : 0;
231: }
232:
233: /**
234: * Sets the Content-Type.
235: */
236: protected void setContentType(String contentType) {
237: this .contentType = contentType;
238: }
239:
240: /**
241: * Sets whether a Content-Length header should be set.
242: */
243: public void setSetContentLength(boolean setContentLength) {
244: this .setContentLength = setContentLength;
245: }
246:
247: /**
248: * Sets HTTP headers.
249: */
250: protected void setHeaders(Headers headers) {
251: this .headers = headers;
252: }
253:
254: /**
255: * Sets cookies.
256: */
257: protected void setCookies(Cookies cookies) {
258: this .cookies = cookies;
259: }
260:
261: /**
262: * Sets shared properties.
263: * @see org.riotfamily.common.web.collaboration.SharedProperties
264: */
265: protected void setProperties(Map properties) {
266: this .properties = properties;
267: }
268:
269: /**
270: * Returns the properties.
271: */
272: public Map getProperties() {
273: return properties;
274: }
275:
276: /**
277: * Returns the lock.
278: */
279: protected ReaderWriterLock getLock() {
280: return this .lock;
281: }
282:
283: /**
284: * Sets whether to filter jsessionid tokens.
285: */
286: protected void setFilterSessionId(boolean filterSessionId) {
287: this .filterSessionId = filterSessionId;
288: }
289:
290: protected Writer getWriter(String sessionId)
291: throws UnsupportedEncodingException, FileNotFoundException {
292:
293: binary = false;
294: Writer writer = new OutputStreamWriter(getOutputStream(),
295: FILE_ENCODING);
296: if (filterSessionId) {
297: writer = new TokenFilterWriter(sessionId, "${jsessionid}",
298: writer);
299: }
300: return writer;
301: }
302:
303: protected OutputStream getOutputStream()
304: throws FileNotFoundException {
305: return new FileOutputStream(file);
306: }
307:
308: protected void gzipContent() throws IOException {
309: InputStream in = new BufferedInputStream(new FileInputStream(
310: file));
311: File zipFile = new File(file.getParentFile(), file.getName()
312: + ".gz");
313: OutputStream out = new GZIPOutputStream(new FileOutputStream(
314: zipFile));
315: FileCopyUtils.copy(in, out);
316: binary = true;
317: file.delete();
318: zipFile.renameTo(file);
319: }
320:
321: protected void writeTo(HttpServletResponse response,
322: String sessionId) throws IOException {
323:
324: try {
325: if (contentType != null) {
326: response.setContentType(contentType);
327: }
328: if (headers != null) {
329: headers.addToResponse(response);
330: }
331: if (cookies != null) {
332: cookies.addToResponse(response);
333: }
334: int contentLength = getSize();
335: if (contentLength > 0) {
336: if (setContentLength) {
337: response.setContentLength(contentLength);
338: }
339: if (binary) {
340: InputStream in = new BufferedInputStream(
341: new FileInputStream(file));
342:
343: IOUtils.copy(in, response.getOutputStream());
344: } else {
345: Reader in = new BufferedReader(
346: new InputStreamReader(new FileInputStream(
347: file), FILE_ENCODING));
348:
349: Writer out = response.getWriter();
350: if (filterSessionId) {
351: out = new TokenFilterWriter("${jsessionid}",
352: sessionId, out);
353: }
354:
355: IOUtils.copy(in, out);
356: }
357: }
358: } catch (FileNotFoundException e) {
359: log
360: .warn("Cache file not found. Invalidating item to trigger "
361: + "an update on the next request.");
362:
363: invalidate();
364: }
365: }
366:
367: protected void delete() {
368: try {
369: lock.lockForWriting();
370: if (file.exists()) {
371: if (!file.delete()) {
372: log.warn("Failed to delete cache file: " + file);
373: }
374: }
375: } finally {
376: lock.releaseWriterLock();
377: }
378: }
379:
380: /**
381: * Calls <code>in.defaultReadObject()</code> and creates a new lock.
382: */
383: private void readObject(ObjectInputStream in) throws IOException,
384: ClassNotFoundException {
385:
386: in.defaultReadObject();
387: lock = new ReaderWriterLock();
388: }
389:
390: public int hashCode() {
391: return key.hashCode();
392: }
393:
394: public boolean equals(Object obj) {
395: if (obj == this ) {
396: return true;
397: }
398: if (obj instanceof CacheItem) {
399: CacheItem other = (CacheItem) obj;
400: return key.equals(other.key);
401: }
402: return false;
403: }
404:
405: }
|