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.text.ParseException;
49  import java.text.SimpleDateFormat;
50  import java.util.Collection;
51  import java.util.Date;
52  import java.util.Iterator;
53  import java.util.Locale;
54  
55  import javax.servlet.ServletOutputStream;
56  import javax.servlet.http.HttpServletResponse;
57  import javax.servlet.http.HttpServletResponseWrapper;
58  
59  import org.apache.commons.collections.MultiMap;
60  import org.apache.commons.collections.map.MultiValueMap;
61  import org.apache.commons.io.FileUtils;
62  import org.apache.commons.io.IOUtils;
63  import org.apache.commons.io.output.ThresholdingOutputStream;
64  
65  
66  /**
67   * A response wrapper which records the status, headers and content. Unless the threshold is reached
68   * the written content gets buffered and the content can get retrieved by
69   * {@link #getBufferedContent()}. Once the threshold is reached either a tmp file is created which
70   * can be retrieved with {@link #getContentFile()} or the content/headers are made transparent to
71   * the original response if {@link #serveIfThresholdReached} is true.
72   * @version $Revision: 14052 $ ($Author: gjoseph $)
73   */
74  public class CacheResponseWrapper extends HttpServletResponseWrapper {
75  
76      public static final int DEFAULT_THRESHOLD = 500 * 1024;
77  
78      private final ServletOutputStream wrappedStream;
79      private PrintWriter wrappedWriter = null;
80      private final MultiMap headers = new MultiValueMap();
81      private int status = SC_OK;
82      private boolean isError;
83      private String redirectionLocation;
84      private HttpServletResponse originalResponse;
85      private ByteArrayOutputStream inMemoryBuffer;
86      private File contentFile;
87      private long contentLength = -1;
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     public ServletOutputStream getOutputStream() throws IOException {
117         return wrappedStream;
118     }
119 
120     public PrintWriter getWriter() throws IOException {
121         if (wrappedWriter == null) {
122             String encoding = getCharacterEncoding();
123             wrappedWriter = encoding != null
124                     ? new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding))
125                     : new PrintWriter(new OutputStreamWriter(getOutputStream()));
126         }
127 
128         return wrappedWriter;
129     }
130 
131     public void flushBuffer() throws IOException {
132         flush();
133     }
134 
135     public void flush() throws IOException {
136         wrappedStream.flush();
137 
138         if (wrappedWriter != null) {
139             wrappedWriter.flush();
140         }
141     }
142 
143 
144     public void reset() {
145         super.reset();
146 //        if (wrappedStream instanceof ByteArrayOutputStream) {
147 //            ((ByteArrayOutputStream)wrappedStream).reset();
148 //        }
149         wrappedWriter = null;
150         status = SC_OK;
151 
152 //         cookies.clear();
153         headers.clear();
154 //        contentType = null;
155 //        contentLength = 0;
156     }
157 
158 
159     public void resetBuffer() {
160         super.resetBuffer();
161 //        if (wrappedStream != null) {
162 //            ((ByteArrayOutputStream)wrappedStream).reset();
163 //        }
164         wrappedWriter = null;
165     }
166 
167     public int getStatus() {
168         return status;
169     }
170 
171     public boolean isError() {
172         return isError;
173     }
174 
175     public MultiMap getHeaders() {
176         return headers;
177     }
178 
179     public long getLastModified() {
180         // we're using a MultiMap. And all this is to workaround code that would possibly set the Last-Modified header with a String value
181         // it will also fail if multiple values have been set.
182         final Collection values = (Collection) headers.get("Last-Modified");
183         if (values == null || values.size() != 1) {
184             throw new IllegalStateException("Can't get Last-Modified header : no or multiple values : " + values);
185         }
186         final Object value = values.iterator().next();
187         if (value instanceof String) {
188             return parseStringDate((String) value);
189         } else if (value instanceof Long) {
190             return ((Long)value).longValue();
191         } else {
192             throw new IllegalStateException("Can't get Last-Modified header : " + value);
193         }
194     }
195 
196     private long parseStringDate(String value) {
197         try {
198             final SimpleDateFormat f = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.ENGLISH);
199             final Date date = f.parse(value);
200             return date.getTime();
201         } catch (ParseException e) {
202             throw new IllegalStateException("Could not parse Last-Modified header with value " + value + " : " + e.getMessage());
203         }
204     }
205 
206     public String getRedirectionLocation() {
207         return redirectionLocation;
208     }
209 
210     public void setDateHeader(String name, long date) {
211         replaceHeader(name, new Long(date));
212     }
213 
214     public void addDateHeader(String name, long date) {
215         appendHeader(name, new Long(date));
216     }
217 
218     public void setHeader(String name, String value) {
219         replaceHeader(name, value);
220     }
221 
222     public void addHeader(String name, String value) {
223         appendHeader(name, value);
224     }
225 
226     public void setIntHeader(String name, int value) {
227         replaceHeader(name, new Integer(value));
228     }
229 
230     public void addIntHeader(String name, int value) {
231         appendHeader(name, new Integer(value));
232     }
233 
234     @Override
235     public boolean containsHeader(String name) {
236         return headers.containsKey(name);
237     }
238 
239     private void replaceHeader(String name, Object value) {
240         headers.remove(name);
241         headers.put(name, value);
242     }
243 
244     private void appendHeader(String name, Object value) {
245         headers.put(name, value);
246     }
247 
248     public void setStatus(int status) {
249         this.status = status;
250     }
251 
252     public void setStatus(int status, String string) {
253         this.status = status;
254     }
255 
256     public void sendRedirect(String location) throws IOException {
257         this.status = SC_MOVED_TEMPORARILY;
258         this.redirectionLocation = location;
259     }
260 
261     public void sendError(int status, String errorMsg) throws IOException {
262         this.errorMsg = errorMsg;
263         this.status = status;
264         this.isError = true;
265     }
266 
267     public void sendError(int status) throws IOException {
268         this.status = status;
269         this.isError = true;
270     }
271 
272     public void setContentLength(int len) {
273         this.contentLength = len;
274     }
275 
276     public int getContentLength() {
277         return (int)(contentLength >=0 ? contentLength : thresholdingOutputStream.getByteCount());
278     }
279 
280     public void replay(HttpServletResponse target) throws IOException {
281         replayHeadersAndStatus(target);
282         replayContent(target, true);
283     }
284 
285     public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
286         if(isError){
287             if(errorMsg != null){
288                 target.sendError(status, errorMsg);
289             }
290             else{
291                 target.sendError(status);
292             }
293         }
294         else if(redirectionLocation != null){
295             target.sendRedirect(redirectionLocation);
296         }
297         else{
298             target.setStatus(status);
299         }
300 
301         target.setStatus(getStatus());
302 
303         final Iterator it = headers.keySet().iterator();
304         while (it.hasNext()) {
305             final String header = (String) it.next();
306 
307             final Collection values = (Collection) headers.get(header);
308             final Iterator valIt = values.iterator();
309             while (valIt.hasNext()) {
310                 final Object val = valIt.next();
311                 RequestHeaderUtil.setHeader(target, header, val);
312             }
313         }
314 
315         // TODO : cookies ?
316         target.setContentType(getContentType());
317         target.setCharacterEncoding(getCharacterEncoding());
318     }
319 
320     public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
321         if(setContentLength){
322             target.setContentLength(getContentLength());
323         }
324         if(getContentLength()>0){
325             if(isThresholdExceeded()){
326                 FileInputStream in = FileUtils.openInputStream(getContentFile());
327                 IOUtils.copy(in, target.getOutputStream());
328                 IOUtils.closeQuietly(in);
329             }
330             else{
331                 IOUtils.copy(new ByteArrayInputStream(inMemoryBuffer.toByteArray()), target.getOutputStream());
332             }
333             target.flushBuffer();
334         }
335     }
336 
337     public void release(){
338         if(isThresholdExceeded()){
339             getContentFile().delete();
340         }
341     }
342 
343     private final class ThresholdingCacheOutputStream extends ThresholdingOutputStream {
344         OutputStream out = inMemoryBuffer;
345 
346         private ThresholdingCacheOutputStream(int threshold) {
347             super(threshold);
348         }
349 
350         @Override
351         protected OutputStream getStream() throws IOException {
352             return out;
353         }
354 
355         @Override
356         protected void thresholdReached() throws IOException {
357             if(serveIfThresholdReached){
358                 replayHeadersAndStatus(originalResponse);
359                 out = originalResponse.getOutputStream();
360             }
361             else{
362                 contentFile = File.createTempFile("cacheStream", null, Path.getTempDirectory());
363                 out = new FileOutputStream(contentFile);
364             }
365             out.write(getBufferedContent());
366         }
367     }
368 
369 }