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   * @version $Revision: 14052 $ ($Author: gjoseph $)
71   */
72  public class CacheResponseWrapper extends HttpServletResponseWrapper {
73  
74      public static final int DEFAULT_THRESHOLD = 500 * 1024;
75  
76      private final ServletOutputStream wrappedStream;
77      private PrintWriter wrappedWriter = null;
78      private final MultiMap headers = new MultiValueMap();
79      private int status = SC_OK;
80      private boolean isError;
81      private String redirectionLocation;
82      private HttpServletResponse originalResponse;
83      private ByteArrayOutputStream inMemoryBuffer;
84      private File contentFile;
85      private long contentLength = -1;
86      private ResponseExpirationCalculator responseExpirationCalculator = new ResponseExpirationCalculator();
87  
88      private ThresholdingOutputStream thresholdingOutputStream;
89      private boolean serveIfThresholdReached;
90  
91      private String errorMsg;
92  
93      public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached) {
94          super(response);
95          this.serveIfThresholdReached = serveIfThresholdReached;
96          this.originalResponse = response;
97          this.inMemoryBuffer = new ByteArrayOutputStream();
98          this.thresholdingOutputStream = new ThresholdingCacheOutputStream(threshold);
99          this.wrappedStream = new SimpleServletOutputStream(thresholdingOutputStream);
100     }
101 
102     public boolean isThresholdExceeded() {
103         return thresholdingOutputStream.isThresholdExceeded();
104     }
105 
106     public byte[] getBufferedContent(){
107         return inMemoryBuffer.toByteArray();
108     }
109 
110     public File getContentFile() {
111         return contentFile;
112     }
113 
114     // MAGNOLIA-1996: this can be called multiple times, e.g. by chunk writers, but always from a single thread.
115     @Override
116     public ServletOutputStream getOutputStream() throws IOException {
117         return wrappedStream;
118     }
119     
120     public ThresholdingOutputStream getThresholdingOutputStream() throws IOException {
121         return thresholdingOutputStream;
122     }
123 
124     @Override
125     public PrintWriter getWriter() throws IOException {
126         if (wrappedWriter == null) {
127             String encoding = getCharacterEncoding();
128             wrappedWriter = encoding != null
129                     ? new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding))
130                     : new PrintWriter(new OutputStreamWriter(getOutputStream()));
131         }
132 
133         return wrappedWriter;
134     }
135 
136     @Override
137     public void flushBuffer() throws IOException {
138         flush();
139     }
140 
141     public void flush() throws IOException {
142         wrappedStream.flush();
143 
144         if (wrappedWriter != null) {
145             wrappedWriter.flush();
146         }
147     }
148 
149 
150     @Override
151     public void reset() {
152         super.reset();
153 //        if (wrappedStream instanceof ByteArrayOutputStream) {
154 //            ((ByteArrayOutputStream)wrappedStream).reset();
155 //        }
156         wrappedWriter = null;
157         status = SC_OK;
158 
159 //         cookies.clear();
160         headers.clear();
161 //        contentType = null;
162 //        contentLength = 0;
163     }
164 
165 
166     @Override
167     public void resetBuffer() {
168         super.resetBuffer();
169 //        if (wrappedStream != null) {
170 //            ((ByteArrayOutputStream)wrappedStream).reset();
171 //        }
172         wrappedWriter = null;
173     }
174 
175     public int getStatus() {
176         return status;
177     }
178 
179     public boolean isError() {
180         return isError;
181     }
182 
183     public MultiMap getHeaders() {
184         return headers;
185     }
186 
187     public long getLastModified() {
188         // we're using a MultiMap. And all this is to workaround code that would possibly set the Last-Modified header with a String value
189         // it will also fail if multiple values have been set.
190         final Collection values = (Collection) headers.get("Last-Modified");
191         if (values == null || values.size() != 1) {
192             throw new IllegalStateException("Can't get Last-Modified header : no or multiple values : " + values);
193         }
194         final Object value = values.iterator().next();
195         if (value instanceof String) {
196             return parseStringDate((String) value);
197         } else if (value instanceof Long) {
198             return ((Long)value).longValue();
199         } else {
200             throw new IllegalStateException("Can't get Last-Modified header : " + value);
201         }
202     }
203 
204     private long parseStringDate(String value) {
205         try {
206             return DateUtil.parseDate(value).getTime();
207         } catch (DateParseException e) {
208             throw new IllegalStateException("Could not parse Last-Modified header with value " + value + " : " + e.getMessage());
209         }
210     }
211 
212     /**
213      * Returns the number of seconds the response can be cached, where 0 means that it must not be cached and -1 means
214      * that it there is no indication on how long it can be cached for.
215      */
216     public int getTimeToLiveInSeconds() {
217         return responseExpirationCalculator.getMaxAgeInSeconds();
218     }
219 
220     public String getRedirectionLocation() {
221         return redirectionLocation;
222     }
223 
224     @Override
225     public void setDateHeader(String name, long date) {
226         replaceHeader(name, Long.valueOf(date));
227     }
228 
229     @Override
230     public void addDateHeader(String name, long date) {
231         appendHeader(name, Long.valueOf(date));
232     }
233 
234     @Override
235     public void setHeader(String name, String value) {
236         replaceHeader(name, value);
237     }
238 
239     @Override
240     public void addHeader(String name, String value) {
241         appendHeader(name, value);
242     }
243 
244     @Override
245     public void setIntHeader(String name, int value) {
246         replaceHeader(name, Integer.valueOf(value));
247     }
248 
249     @Override
250     public void addIntHeader(String name, int value) {
251         appendHeader(name, Integer.valueOf(value));
252     }
253 
254     @Override
255     public boolean containsHeader(String name) {
256         return headers.containsKey(name);
257     }
258 
259     private void replaceHeader(String name, Object value) {
260         if (!responseExpirationCalculator.addHeader(name, value)) {
261             headers.remove(name);
262             headers.put(name, value);
263         }
264     }
265 
266     private void appendHeader(String name, Object value) {
267         if (!responseExpirationCalculator.addHeader(name, value)) {
268             headers.put(name, value);
269         }
270     }
271 
272     @Override
273     public void setStatus(int status) {
274         this.status = status;
275     }
276 
277     @Override
278     public void setStatus(int status, String string) {
279         this.status = status;
280     }
281 
282     @Override
283     public void sendRedirect(String location) throws IOException {
284         this.status = SC_MOVED_TEMPORARILY;
285         this.redirectionLocation = location;
286     }
287 
288     @Override
289     public void sendError(int status, String errorMsg) throws IOException {
290         this.errorMsg = errorMsg;
291         this.status = status;
292         this.isError = true;
293     }
294 
295     @Override
296     public void sendError(int status) throws IOException {
297         this.status = status;
298         this.isError = true;
299     }
300 
301     @Override
302     public void setContentLength(int len) {
303         this.contentLength = len;
304     }
305 
306     public int getContentLength() {
307         return (int)(contentLength >=0 ? contentLength : thresholdingOutputStream.getByteCount());
308     }
309 
310     public void replay(HttpServletResponse target) throws IOException {
311         replayHeadersAndStatus(target);
312         replayContent(target, true);
313     }
314 
315     public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
316         if(isError){
317             if(errorMsg != null){
318                 target.sendError(status, errorMsg);
319             }
320             else{
321                 target.sendError(status);
322             }
323         }
324         else if(redirectionLocation != null){
325             target.sendRedirect(redirectionLocation);
326         }
327         else{
328             target.setStatus(status);
329         }
330 
331         target.setStatus(getStatus());
332 
333         final Iterator it = headers.keySet().iterator();
334         while (it.hasNext()) {
335             final String header = (String) it.next();
336 
337             final Collection values = (Collection) headers.get(header);
338             final Iterator valIt = values.iterator();
339             while (valIt.hasNext()) {
340                 final Object val = valIt.next();
341                 RequestHeaderUtil.setHeader(target, header, val);
342             }
343         }
344 
345         // TODO : cookies ?
346         target.setContentType(getContentType());
347         target.setCharacterEncoding(getCharacterEncoding());
348     }
349 
350     public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
351         if(setContentLength){
352             target.setContentLength(getContentLength());
353         }
354         if(getContentLength()>0){
355             if(isThresholdExceeded()){
356                 FileInputStream in = FileUtils.openInputStream(getContentFile());
357                 IOUtils.copy(in, target.getOutputStream());
358                 IOUtils.closeQuietly(in);
359             }
360             else{
361                 IOUtils.copy(new ByteArrayInputStream(inMemoryBuffer.toByteArray()), target.getOutputStream());
362             }
363             target.flushBuffer();
364         }
365     }
366 
367     private final class ThresholdingCacheOutputStream extends ThresholdingOutputStream {
368         OutputStream out = inMemoryBuffer;
369 
370         private ThresholdingCacheOutputStream(int threshold) {
371             super(threshold);
372         }
373 
374         @Override
375         protected OutputStream getStream() throws IOException {
376             return out;
377         }
378 
379         @Override
380         protected void thresholdReached() throws IOException {
381             if(serveIfThresholdReached){
382                 replayHeadersAndStatus(originalResponse);
383                 out = originalResponse.getOutputStream();
384             }
385             else{
386                 contentFile = File.createTempFile("cacheStream", null, Path.getTempDirectory());
387                 contentFile.deleteOnExit();
388                 out = new FileOutputStream(contentFile);
389             }
390             out.write(getBufferedContent());
391         }
392     }
393 
394 }