001: /*
002: * Copyright 2005 Joe Walker
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License");
005: * you may not use this file except in compliance with the License.
006: * You may obtain a copy of the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS,
012: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013: * See the License for the specific language governing permissions and
014: * limitations under the License.
015: */
016: package org.directwebremoting.servlet;
017:
018: import java.io.IOException;
019: import java.io.InputStream;
020: import java.io.PrintWriter;
021: import java.io.StringWriter;
022: import java.util.HashMap;
023: import java.util.Map;
024:
025: import javax.servlet.http.HttpServletRequest;
026: import javax.servlet.http.HttpServletResponse;
027:
028: import org.apache.commons.logging.Log;
029: import org.apache.commons.logging.LogFactory;
030: import org.directwebremoting.extend.Handler;
031: import org.directwebremoting.util.CopyUtils;
032:
033: /**
034: * @author Joe Walker [joe at getahead dot ltd dot uk]
035: */
036: public abstract class CachingFileHandler implements Handler {
037: /* (non-Javadoc)
038: * @see org.directwebremoting.Handler#handle(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
039: */
040: public void handle(HttpServletRequest request,
041: HttpServletResponse response) throws IOException {
042: if (isUpToDate(request)) {
043: response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
044: return;
045: }
046:
047: String output;
048:
049: synchronized (scriptCache) {
050: String url = request.getPathInfo();
051: output = scriptCache.get(url);
052: if (output == null) {
053: output = generateCachableContent(request, response);
054: }
055: scriptCache.put(url, output);
056: }
057:
058: response.setContentType(mimeType);
059: response.setDateHeader(HttpConstants.HEADER_LAST_MODIFIED,
060: CONTAINER_START_TIME);
061: response.setHeader(HttpConstants.HEADER_ETAG, ETAG);
062:
063: PrintWriter out = response.getWriter();
064: out.println(output);
065: }
066:
067: /**
068: * Create a String which can be cached and sent as a 302
069: * @param request The HTTP request data
070: * @param response Where we write the HTTP response data
071: * @return The string to output for this resource
072: * @throws IOException
073: */
074: protected abstract String generateCachableContent(
075: HttpServletRequest request, HttpServletResponse response)
076: throws IOException;
077:
078: /**
079: * An easy way to implement {@link #generateCachableContent(HttpServletRequest, HttpServletResponse)}
080: * is to simply <code>return {@link #readResource(String)};</code> using the
081: * path to some resource provided in dwr.jar.
082: * @param resource The fully qualified path (i.e. includes package) to a resource in dwr.jar
083: * @return The contents of the resource as a string
084: * @throws IOException If the resource can not be found or read
085: */
086: protected String readResource(String resource) throws IOException {
087: InputStream raw = getClass().getResourceAsStream(resource);
088: if (raw == null) {
089: throw new IOException("Failed to find resource: "
090: + resource);
091: }
092:
093: StringWriter sw = new StringWriter();
094: CopyUtils.copy(raw, sw);
095:
096: return sw.toString();
097: }
098:
099: /**
100: * Do we need to send the content for this file
101: * @param req The HTTP request
102: * @return true iff the ETags and If-Modified-Since headers say we have not changed
103: */
104: protected boolean isUpToDate(HttpServletRequest req) {
105: if (ignoreLastModified) {
106: return false;
107: }
108:
109: long modifiedSince = -1;
110: try {
111: // HACK: Websphere appears to get confused sometimes
112: modifiedSince = req
113: .getDateHeader(HttpConstants.HEADER_IF_MODIFIED);
114: } catch (RuntimeException ex) {
115: // TODO: Check for "length" and re-parse
116: // Normally clients send If-Modified-Since in rfc-complaint form
117: // ("If-Modified-Since: Tue, 13 Mar 2007 13:11:09 GMT") some proxies
118: // or browsers add length to this header so it comes like
119: // ("If-Modified-Since: Tue, 13 Mar 2007 13:11:09 GMT; length=35946")
120: // Servlet spec says container can throw IllegalArgumentException
121: // if header value can not be parsed as http-date.
122: // We might want to check for "; length=" and then do our own parsing
123: // See: http://getahead.org/bugs/browse/DWR-20
124: // And: http://www-1.ibm.com/support/docview.wss?uid=swg1PK20062
125: }
126:
127: if (modifiedSince != -1) {
128: // Browsers are only accurate to the second
129: modifiedSince -= modifiedSince % 1000;
130: }
131: String givenEtag = req.getHeader(HttpConstants.HEADER_IF_NONE);
132: String pathInfo = req.getPathInfo();
133:
134: // Deal with missing etags
135: if (givenEtag == null) {
136: // There is no ETag, just go with If-Modified-Since
137: if (modifiedSince > CONTAINER_START_TIME) {
138: if (log.isDebugEnabled()) {
139: log
140: .debug("Sending 304 for " + pathInfo
141: + " If-Modified-Since="
142: + modifiedSince
143: + ", Last-Modified="
144: + CONTAINER_START_TIME);
145: }
146: return true;
147: }
148:
149: // There are no modified settings, carry on
150: return false;
151: }
152:
153: // Deal with missing If-Modified-Since
154: if (modifiedSince == -1) {
155: if (!ETAG.equals(givenEtag)) {
156: // There is an ETag, but no If-Modified-Since
157: if (log.isDebugEnabled()) {
158: log.debug("Sending 304 for " + pathInfo
159: + ", If-Modified-Since=-1, Old ETag="
160: + givenEtag + ", New ETag=" + ETAG);
161: }
162: return true;
163: }
164:
165: // There are no modified settings, carry on
166: return false;
167: }
168:
169: // Do both values indicate that we are in-date?
170: if (ETAG.equals(givenEtag)
171: && modifiedSince < CONTAINER_START_TIME) {
172: if (log.isDebugEnabled()) {
173: log.debug("Sending 304 for " + pathInfo
174: + ", If-Modified-Since=" + modifiedSince
175: + ", Container Start=" + CONTAINER_START_TIME
176: + ", Old ETag=" + givenEtag + ", New ETag="
177: + ETAG);
178: }
179: return true;
180: }
181:
182: log.debug("Sending content for " + pathInfo
183: + ", If-Modified-Since=" + modifiedSince
184: + ", Container Start=" + CONTAINER_START_TIME
185: + ", Old ETag=" + givenEtag + ", New ETag=" + ETAG);
186: return false;
187: }
188:
189: /**
190: * @param ignoreLastModified The ignoreLastModified to set.
191: */
192: public void setIgnoreLastModified(boolean ignoreLastModified) {
193: this .ignoreLastModified = ignoreLastModified;
194: }
195:
196: /**
197: * The mime type to send the output under
198: * @param mimeType the mimeType to set
199: */
200: public void setMimeType(String mimeType) {
201: this .mimeType = mimeType;
202: }
203:
204: /**
205: * @return the current mime type
206: */
207: public String getMimeType() {
208: return mimeType;
209: }
210:
211: /**
212: * The time on the script files
213: */
214: private static final long CONTAINER_START_TIME;
215:
216: /**
217: * The ETAG (=time for us) on the script files
218: */
219: private static final String ETAG;
220:
221: /**
222: * Initialize the container start time
223: */
224: static {
225: // Browsers are only accurate to the second
226: long now = System.currentTimeMillis();
227: CONTAINER_START_TIME = now - (now % 1000);
228:
229: ETAG = "\"" + CONTAINER_START_TIME + '\"';
230: }
231:
232: /**
233: * The mime type to send the output under
234: */
235: private String mimeType;
236:
237: /**
238: * We cache the script output for speed
239: */
240: private final Map<String, String> scriptCache = new HashMap<String, String>();
241:
242: /**
243: * Do we ignore all the Last-Modified/ETags blathering?
244: */
245: private boolean ignoreLastModified = false;
246:
247: /**
248: * The log stream
249: */
250: private static final Log log = LogFactory
251: .getLog(CachingFileHandler.class);
252: }
|