View Javadoc

1   /**
2    * This file Copyright (c) 2008-2011 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.module.cache.filter;
35  
36  import info.magnolia.cms.core.Path;
37  import info.magnolia.cms.util.RequestHeaderUtil;
38  
39  import java.io.ByteArrayInputStream;
40  import java.io.ByteArrayOutputStream;
41  import java.io.File;
42  import java.io.FileInputStream;
43  import java.io.FileOutputStream;
44  import java.io.IOException;
45  import java.io.OutputStream;
46  import java.io.OutputStreamWriter;
47  import java.io.PrintWriter;
48  import java.util.Collection;
49  import java.util.Iterator;
50  
51  import javax.servlet.ServletOutputStream;
52  import javax.servlet.http.HttpServletResponse;
53  import javax.servlet.http.HttpServletResponseWrapper;
54  
55  import org.apache.commons.collections.MultiMap;
56  import org.apache.commons.collections.map.MultiValueMap;
57  import org.apache.commons.httpclient.util.DateParseException;
58  import org.apache.commons.httpclient.util.DateUtil;
59  import org.apache.commons.io.FileUtils;
60  import org.apache.commons.io.IOUtils;
61  import org.apache.commons.io.output.ThresholdingOutputStream;
62  
63  
64  /**
65   * A response wrapper which records the status, headers and content. Unless the threshold is reached
66   * the written content gets buffered and the content can get retrieved by
67   * {@link #getBufferedContent()}. Once the threshold is reached either a tmp file is created which
68   * can be retrieved with {@link #getContentFile()} or the content/headers are made transparent to
69   * the original response if {@link #serveIfThresholdReached} is true.
70   *
71   * @version $Id$
72   */
73  public class CacheResponseWrapper extends HttpServletResponseWrapper {
74  
75      public static final int DEFAULT_THRESHOLD = 500 * 1024;
76  
77      private final ServletOutputStream wrappedStream;
78      private PrintWriter wrappedWriter = null;
79      private final MultiMap headers = new MultiValueMap();
80      private int status = SC_OK;
81      private boolean isError;
82      private String redirectionLocation;
83      private HttpServletResponse originalResponse;
84      private ByteArrayOutputStream inMemoryBuffer;
85      private File contentFile;
86      private long contentLength = -1;
87      private ResponseExpirationCalculator responseExpirationCalculator;
88  
89      private ThresholdingOutputStream thresholdingOutputStream;
90      private boolean serveIfThresholdReached;
91  
92      private String errorMsg;
93  
94      public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached) {
95          super(response);
96          this.serveIfThresholdReached = serveIfThresholdReached;
97          this.originalResponse = response;
98          this.inMemoryBuffer = new ByteArrayOutputStream();
99          this.thresholdingOutputStream = new ThresholdingCacheOutputStream(threshold);
100         this.wrappedStream = new SimpleServletOutputStream(thresholdingOutputStream);
101     }
102 
103     public boolean isThresholdExceeded() {
104         return thresholdingOutputStream.isThresholdExceeded();
105     }
106 
107     public byte[] getBufferedContent(){
108         return inMemoryBuffer.toByteArray();
109     }
110 
111     public File getContentFile() {
112         return contentFile;
113     }
114 
115     // MAGNOLIA-1996: this can be called multiple times, e.g. by chunk writers, but always from a single thread.
116     @Override
117     public ServletOutputStream getOutputStream() throws IOException {
118         return wrappedStream;
119     }
120     
121     public ThresholdingOutputStream getThresholdingOutputStream() throws IOException {
122         return thresholdingOutputStream;
123     }
124 
125     @Override
126     public PrintWriter getWriter() throws IOException {
127         if (wrappedWriter == null) {
128             String encoding = getCharacterEncoding();
129             wrappedWriter = encoding != null
130                     ? new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding))
131                     : new PrintWriter(new OutputStreamWriter(getOutputStream()));
132         }
133 
134         return wrappedWriter;
135     }
136 
137     @Override
138     public void flushBuffer() throws IOException {
139         flush();
140     }
141 
142     public void flush() throws IOException {
143         wrappedStream.flush();
144 
145         if (wrappedWriter != null) {
146             wrappedWriter.flush();
147         }
148     }
149 
150 
151     @Override
152     public void reset() {
153         super.reset();
154 //        if (wrappedStream instanceof ByteArrayOutputStream) {
155 //            ((ByteArrayOutputStream)wrappedStream).reset();
156 //        }
157         wrappedWriter = null;
158         status = SC_OK;
159 
160 //         cookies.clear();
161         headers.clear();
162 //        contentType = null;
163 //        contentLength = 0;
164     }
165 
166 
167     @Override
168     public void resetBuffer() {
169         super.resetBuffer();
170 //        if (wrappedStream != null) {
171 //            ((ByteArrayOutputStream)wrappedStream).reset();
172 //        }
173         wrappedWriter = null;
174     }
175 
176     public int getStatus() {
177         return status;
178     }
179 
180     public boolean isError() {
181         return isError;
182     }
183 
184     public MultiMap getHeaders() {
185         return headers;
186     }
187 
188     public long getLastModified() {
189         // we're using a MultiMap. And all this is to workaround code that would possibly set the Last-Modified header with a String value
190         // it will also fail if multiple values have been set.
191         final Collection values = (Collection) headers.get("Last-Modified");
192         if (values == null || values.size() != 1) {
193             throw new IllegalStateException("Can't get Last-Modified header : no or multiple values : " + values);
194         }
195         final Object value = values.iterator().next();
196         if (value instanceof String) {
197             return parseStringDate((String) value);
198         } else if (value instanceof Long) {
199             return ((Long)value).longValue();
200         } else {
201             throw new IllegalStateException("Can't get Last-Modified header : " + value);
202         }
203     }
204 
205     private long parseStringDate(String value) {
206         try {
207             return DateUtil.parseDate(value).getTime();
208         } catch (DateParseException e) {
209             throw new IllegalStateException("Could not parse Last-Modified header with value " + value + " : " + e.getMessage());
210         }
211     }
212 
213     /**
214      * Enables expiration detection, response headers are then intercepted and suppressed from the response and used
215      * internally to calculate when the response expires (its time to live value). Use {@link #getTimeToLiveInSeconds()}
216      * to get the calculated value. See {@link ResponseExpirationCalculator} for more details on how the calculation
217      * is performed.
218      */
219     public void setResponseExpirationDetectionEnabled() {
220         this.responseExpirationCalculator = new ResponseExpirationCalculator();
221     }
222 
223     /**
224      * Returns the number of seconds the response can be cached, where 0 means that it must not be cached and -1 means
225      * that it there is no indication on how long it can be cached for. Will also return -1 if expiration calculation is
226      * disabled.
227      *
228      * @see #setResponseExpirationDetectionEnabled()
229      */
230     public int getTimeToLiveInSeconds() {
231         return responseExpirationCalculator != null ? responseExpirationCalculator.getMaxAgeInSeconds() : -1;
232     }
233 
234     public String getRedirectionLocation() {
235         return redirectionLocation;
236     }
237 
238     @Override
239     public void setDateHeader(String name, long date) {
240         replaceHeader(name, Long.valueOf(date));
241     }
242 
243     @Override
244     public void addDateHeader(String name, long date) {
245         appendHeader(name, Long.valueOf(date));
246     }
247 
248     @Override
249     public void setHeader(String name, String value) {
250         replaceHeader(name, value);
251     }
252 
253     @Override
254     public void addHeader(String name, String value) {
255         appendHeader(name, value);
256     }
257 
258     @Override
259     public void setIntHeader(String name, int value) {
260         replaceHeader(name, Integer.valueOf(value));
261     }
262 
263     @Override
264     public void addIntHeader(String name, int value) {
265         appendHeader(name, Integer.valueOf(value));
266     }
267 
268     @Override
269     public boolean containsHeader(String name) {
270         return headers.containsKey(name);
271     }
272 
273     private void replaceHeader(String name, Object value) {
274         if (responseExpirationCalculator == null || !responseExpirationCalculator.addHeader(name, value)) {
275             headers.remove(name);
276             headers.put(name, value);
277         }
278     }
279 
280     private void appendHeader(String name, Object value) {
281         if (responseExpirationCalculator == null || !responseExpirationCalculator.addHeader(name, value)) {
282             headers.put(name, value);
283         }
284     }
285 
286     @Override
287     public void setStatus(int status) {
288         this.status = status;
289     }
290 
291     @Override
292     public void setStatus(int status, String string) {
293         this.status = status;
294     }
295 
296     @Override
297     public void sendRedirect(String location) throws IOException {
298         this.status = SC_MOVED_TEMPORARILY;
299         this.redirectionLocation = location;
300     }
301 
302     @Override
303     public void sendError(int status, String errorMsg) throws IOException {
304         this.errorMsg = errorMsg;
305         this.status = status;
306         this.isError = true;
307     }
308 
309     @Override
310     public void sendError(int status) throws IOException {
311         this.status = status;
312         this.isError = true;
313     }
314 
315     @Override
316     public void setContentLength(int len) {
317         this.contentLength = len;
318     }
319 
320     public int getContentLength() {
321         return (int)(contentLength >=0 ? contentLength : thresholdingOutputStream.getByteCount());
322     }
323 
324     public void replay(HttpServletResponse target) throws IOException {
325         replayHeadersAndStatus(target);
326         replayContent(target, true);
327     }
328 
329     public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
330         if(isError){
331             if(errorMsg != null){
332                 target.sendError(status, errorMsg);
333             }
334             else{
335                 target.sendError(status);
336             }
337         }
338         else if(redirectionLocation != null){
339             target.sendRedirect(redirectionLocation);
340         }
341         else{
342             target.setStatus(status);
343         }
344 
345         target.setStatus(getStatus());
346 
347         final Iterator it = headers.keySet().iterator();
348         while (it.hasNext()) {
349             final String header = (String) it.next();
350 
351             final Collection values = (Collection) headers.get(header);
352             final Iterator valIt = values.iterator();
353             while (valIt.hasNext()) {
354                 final Object val = valIt.next();
355                 RequestHeaderUtil.setHeader(target, header, val);
356             }
357         }
358 
359         // TODO : cookies ?
360         target.setContentType(getContentType());
361         target.setCharacterEncoding(getCharacterEncoding());
362     }
363 
364     public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
365         if(setContentLength){
366             target.setContentLength(getContentLength());
367         }
368         if(getContentLength()>0){
369             if(isThresholdExceeded()){
370                 FileInputStream in = FileUtils.openInputStream(getContentFile());
371                 IOUtils.copy(in, target.getOutputStream());
372                 IOUtils.closeQuietly(in);
373             }
374             else{
375                 IOUtils.copy(new ByteArrayInputStream(inMemoryBuffer.toByteArray()), target.getOutputStream());
376             }
377             target.flushBuffer();
378         }
379     }
380 
381     private final class ThresholdingCacheOutputStream extends ThresholdingOutputStream {
382         OutputStream out = inMemoryBuffer;
383 
384         private ThresholdingCacheOutputStream(int threshold) {
385             super(threshold);
386         }
387 
388         @Override
389         protected OutputStream getStream() throws IOException {
390             return out;
391         }
392 
393         @Override
394         protected void thresholdReached() throws IOException {
395             if(serveIfThresholdReached){
396                 replayHeadersAndStatus(originalResponse);
397                 out = originalResponse.getOutputStream();
398             }
399             else{
400                 contentFile = File.createTempFile("cacheStream", null, Path.getTempDirectory());
401                 contentFile.deleteOnExit();
402                 out = new FileOutputStream(contentFile);
403             }
404             out.write(getBufferedContent());
405         }
406     }
407 
408 }