001: /******************************************************************************
002: * RequestCache.java
003: * ****************************************************************************/package org.openlaszlo.cache;
004:
005: import javax.servlet.http.*;
006: import java.io.InputStream;
007: import java.io.IOException;
008: import java.io.File;
009: import java.io.Serializable;
010: import java.io.OutputStream;
011: import java.util.Properties;
012: import java.net.MalformedURLException;
013: import org.openlaszlo.data.*;
014: import org.openlaszlo.utils.LZHttpUtils;
015: import org.openlaszlo.utils.FileUtils;
016: import org.apache.log4j.*;
017:
018: /**
019: * A class for maintaining a disk-backed cache of HTTP requests.
020: *
021: * The main entry point is the <code>getAsSWF</code> method.
022: * Given a specific HTTP request and response, the item returns
023: * an InputStream for a possibly-converted SWF that represents
024: * the item requested. This method may return NULL, indicating
025: * that a special HTTP status code has been stuck in the response.
026: *
027: * @author <a href="mailto:bloch@laszlosystems.com">Eric Bloch</a>
028: */
029: public abstract class RequestCache extends Cache {
030:
031: /** logger */
032: private static Logger mLogger = Logger.getLogger(Cache.class);
033:
034: /** converter */
035: protected Converter mConverter;
036:
037: /**
038: * Creates a new <code>RequestCache</code> instance.
039: *
040: * @param name
041: * @param cacheDirectory a <code>File</code> naming a directory
042: * where cache files should be kept
043: * @param converter
044: * @param props
045: */
046: public RequestCache(String name, File cacheDirectory,
047: Converter converter, Properties props) throws IOException {
048:
049: super (name, cacheDirectory, props);
050: mConverter = converter;
051: }
052:
053: /**
054: * @return a serializable cache key for the given request
055: */
056: public Serializable getKey(HttpServletRequest req)
057: throws MalformedURLException {
058:
059: // This is a nice, easy to read key
060: String enc = mConverter.chooseEncoding(req);
061: if (enc == null)
062: enc = "";
063: StringBuffer key = new StringBuffer();
064: key.append(DataSource.getURL(req));
065: // note: space not allowed in URLS so it's good to
066: // use here as a separator to distinguish encoded keys
067: key.append(" ");
068: key.append(enc);
069: return key.toString();
070: }
071:
072: /**
073: * Using the given datasource from the incoming request,
074: * get the data if it's out of data from the cache, convert it
075: * to SWF, and write it out to the given response.
076: *
077: * @param app absolute path name to app
078: * @param req servlet request that triggered the get
079: * @param res servlet response to fill out
080: * @param dataSource source of data
081: * @throws IOException if there's an IOException while creating
082: * the response
083: * @throws DataSourceException if there's an error getting the data
084: * @throws ConversionException if there's an error convering the data
085: */
086: public void getAsSWF(String app, HttpServletRequest req,
087: HttpServletResponse res, DataSource dataSource)
088: throws IOException, DataSourceException,
089: ConversionException {
090:
091: // Skip the cache if it's 0 size
092: if (getMaxMemSize() == 0 && getMaxDiskSize() == 0) {
093: dataSource.getAsSWF(app, req, res, mConverter);
094: return;
095: }
096:
097: StringBuffer kb = new StringBuffer();
098: kb.append(dataSource.name());
099: kb.append(" ");
100: String rk = getKey(req).toString();
101: kb.append(rk);
102: String key = kb.toString();
103: String enc = mConverter.chooseEncoding(req);
104:
105: mLogger.info("requesting '" + rk + "'");
106: if (enc != null) {
107: mLogger.debug("encoding " + enc);
108: }
109:
110: Item item = null;
111: InputStream input = null;
112: OutputStream output = null;
113: try {
114: item = findItem(key, enc, /* lock it and leave active */
115: true);
116:
117: // Get an input stream for a SWF version of this item;
118: try {
119: input = getItemStreamAsSWF(item, app, req, res,
120: dataSource);
121: } finally {
122: // Unlock it while we send out the response
123: // FIXME: [2003-08-27 bloch] there is a slight race here since the source
124: // of the stream could be removed before we are finished; this would
125: // be rare since we're MRU now.
126: item.unlock();
127: }
128:
129: // Send out the response
130: if (input != null) {
131: try {
132: output = res.getOutputStream();
133: long n = FileUtils.sendToStream(input, output);
134: mLogger.info(n + " bytes sent");
135: } catch (FileUtils.StreamWritingException e) {
136: mLogger
137: .warn("StreamWritingException while responding: "
138: + e.getMessage());
139: }
140: } else {
141: mLogger.info("Cache responding with NOT_MODIFIED");
142: }
143: } finally {
144: FileUtils.close(output);
145: FileUtils.close(input);
146:
147: // If there's an item, unlock it and update the cache
148: if (item != null) {
149: updateCacheAndDeactivateItem(item);
150: }
151: }
152: }
153:
154: /**
155: * @return true if the request is cacheable.
156: *
157: * If response headers are to be sentback, then the request is
158: * not cacheable on the server.
159: *
160: * If req headers are sent, then the request is not cacheable
161: * on the server.
162: *
163: * If the cache parameter is present and set to true,
164: * the request is cacheable. Otherwise it's not.
165: */
166: public boolean isCacheable(HttpServletRequest req) {
167: String hds = req.getParameter("sendheaders");
168: if (hds != null) {
169: if (hds.equals("true")) {
170: return false;
171: }
172: }
173: hds = req.getParameter("headers");
174: if (hds != null) {
175: return false;
176: }
177: String c = req.getParameter("cache");
178: if (c == null)
179: return false;
180: return c.equals("true");
181: }
182:
183: /**
184: * Return the converter for this cache
185: */
186: public Converter getConverter() {
187: return mConverter;
188: }
189:
190: /**
191: * Get the item if it's been updated since the given time. Item
192: * must be locked when you call this.
193: * @return null if the url hasn't been modified since the given time
194: * or an input stream that can be used to read the item's content
195: * as SWF.
196: */
197: InputStream getItemStreamAsSWF(Item item, String app,
198: HttpServletRequest req, HttpServletResponse res,
199: DataSource dataSource) throws IOException,
200: DataSourceException, ConversionException {
201:
202: long ifModifiedSince = -1;
203: long lastModified = -1;
204:
205: CachedInfo info = item.getInfo();
206: String enc = info.getEncoding();
207:
208: String hdr = req.getHeader(LZHttpUtils.IF_MODIFIED_SINCE);
209: if (hdr != null) {
210: mLogger.debug("req last modified time: " + hdr);
211: lastModified = LZHttpUtils.getDate(hdr);
212: }
213:
214: boolean doClientCache = DataSource.isClientCacheable(req);
215:
216: // FIXME[2003-05-21 bloch]: Max and I worked through the
217: // logic in this comment below and it seemed correct
218: // but as I paste it in here it's missing an else clause...
219: //
220: // If (there is an entry in the cache)
221: // If there's no timestamp in the request
222: // Use the timestamp from the cache.
223: // else
224: // If request time is <= cache time
225: // use cache time
226: // else (no entry in the cache)
227: // use -1
228: //
229: // Max prefers the following code to implement but I'm leaving the extant code
230: // because it's too close to release for me to be making changes
231: // like that. Here's the code Max, liked:
232: //
233: // if (info.getLastModified() != -1) {
234: // if (ifModifiedSince == -1) {
235: // ifModifiedSince = info.getLastModified();
236: // } else {
237: // if (ifModifiedSince <= info.getLastModified()) {
238: // ifModifiedSince = info.getLastModified();
239: // }
240: // }
241: // } else {
242: // ifModifiedSince = -1;
243: // }
244: //
245: //
246: // The code below has existed for a while and actually implements the logic above
247: // in a slightly convoluted way because of the special dual meaning of -1; it means no
248: // last modified time at all when coming from data or the cache and it
249: // means get fresh data when making a data request (as the 3rd parameter to
250: // the getData() method below.
251:
252: if (info.getLastModified() > lastModified
253: || info.getLastModified() == -1) {
254: ifModifiedSince = info.getLastModified();
255: mLogger.debug("using cached last modified time: "
256: + ifModifiedSince);
257: } else {
258: ifModifiedSince = lastModified;
259: mLogger.debug("using req last modified time: "
260: + ifModifiedSince);
261: }
262:
263: Data data = null;
264:
265: try {
266:
267: try {
268: data = dataSource.getData(app, req, res,
269: ifModifiedSince);
270: } catch (DataSourceException e) {
271: // When we get an error from the back end,
272: // we must nuke the item from the cache.
273: item.markDirty();
274: throw e;
275: } catch (IOException e) {
276: item.markDirty();
277: throw e;
278: } catch (RuntimeException e) {
279: item.markDirty();
280: throw e;
281: }
282:
283: if (data.notModified()) {
284:
285: mLogger.debug("Remote response: NOT_MODIFIED");
286: if (lastModified >= info.getLastModified()
287: && doClientCache) {
288: res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
289: return null;
290: }
291:
292: if (item.validForData(data)) {
293:
294: if (enc != null) {
295: res
296: .setHeader(
297: LZHttpUtils.CONTENT_ENCODING,
298: enc);
299: }
300: if (doClientCache) {
301: long l = info.getLastModified();
302: if (l != -1) {
303: res.setDateHeader(
304: LZHttpUtils.LAST_MODIFIED, l);
305: }
306: } else {
307: LZHttpUtils.noStore(res);
308: }
309: res.setContentLength((int) info.getSize());
310: return item.getStream();
311: }
312: }
313:
314: mLogger.debug("path name: " + item.getPathName());
315:
316: // We know the cached item is dirty
317: item.markDirty();
318:
319: // Mark the info with the new last modified time
320: info.setLastModified(data.lastModified());
321:
322: // Convert the data to SWF
323: InputStream input = mConverter.convertToSWF(data, req, res);
324: // TODO: [2003-09-22 bloch] add content length when
325: // converter api provides this info; input.available() is not reliable
326: // TODO: [2003-09-22 bloch] could handle case when conversion is a no-op from swf,
327: // without the above
328:
329: // Update the item with the data
330: try {
331: item.update(input);
332: item.updateInfo();
333: item.markClean();
334: } finally {
335: FileUtils.close(input);
336: }
337:
338: // FIXME: [2003-03-13 bloch] hope that no one
339: // removes the file before we're done with this
340: // input stream. This would happen only
341: // when the cache is full and small; a rare
342: // case in production.
343:
344: InputStream str = item.getStream();
345:
346: if (enc != null) {
347: res.setHeader(LZHttpUtils.CONTENT_ENCODING, enc);
348: }
349:
350: if (doClientCache) {
351: long l = info.getLastModified();
352: if (l != -1) {
353: res.setDateHeader(LZHttpUtils.LAST_MODIFIED, l);
354: }
355:
356: } else {
357: LZHttpUtils.noStore(res);
358: }
359:
360: res.setContentLength((int) info.getSize());
361:
362: return str;
363:
364: } finally {
365: if (data != null) {
366: data.release();
367: }
368: }
369: }
370: }
|