Clover icon

Magnolia Module Cache 5.5.9

  1. Project Clover database Mon Nov 25 2019 16:46:50 CET
  2. Package info.magnolia.module.cache.filter

File CacheResponseWrapper.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart7.png
45% of files have more coverage

Code metrics

50
117
34
2
438
311
62
0.53
3.44
17
1.82
9% of code in this file is excluded from these metrics.

Classes

Class Line # Actions
CacheResponseWrapper 73 114 9.3% 59 62
0.682051368.2%
CacheResponseWrapper.ThresholdingCacheOutputStream 422 3 0% 3 0
1.0100%
 

Contributing tests

This file is covered by 18 tests. .

Source view

1    /**
2    * This file Copyright (c) 2008-2018 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.Date;
50    import java.util.Iterator;
51   
52    import javax.servlet.ServletOutputStream;
53    import javax.servlet.http.HttpServletResponse;
54    import javax.servlet.http.HttpServletResponseWrapper;
55   
56    import org.apache.commons.collections4.MultiMap;
57    import org.apache.commons.collections4.map.MultiValueMap;
58    import org.apache.commons.io.FileUtils;
59    import org.apache.commons.io.IOUtils;
60    import org.apache.commons.io.output.ThresholdingOutputStream;
61    import org.apache.http.client.utils.DateUtils;
62    import org.slf4j.Logger;
63    import org.slf4j.LoggerFactory;
64   
65    /**
66    * A response wrapper which records the status, headers and content. Unless the threshold is reached
67    * the written content gets buffered and the content can get retrieved by {@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    * Set this request attribute to an positive {@link Integer} if you want to raise {@link #DEFAULT_THRESHOLD} for time consuming resources.
78    */
79    public static final String ATTRIBUTE_IN_MEMORY_THRESHOLD = "mgnlInMemoryThreshold";
80   
81    public static final String CACHE_TEMP_FILE_PREFIX = "cacheStream";
82   
83    private static final Logger log = LoggerFactory.getLogger(CacheResponseWrapper.class);
84   
85    private ServletOutputStream wrappedStream;
86    private PrintWriter wrappedWriter = null;
87    private final MultiMap headers = new MultiValueMap();
88    private int status = SC_OK;
89    private boolean isError;
90    private String redirectionLocation;
91    private final HttpServletResponse originalResponse;
92    private File contentFile;
93    private long contentLength = -1;
94    private ResponseExpirationCalculator responseExpirationCalculator;
95   
96    private final AbstractThresholdingCacheOutputStream thresholdingOutputStream;
97    private final boolean serveIfThresholdReached;
98   
99    private String errorMsg;
100   
 
101  17 toggle public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached) {
102  17 this(response, threshold, serveIfThresholdReached, null);
103    // can be avoided only by refactoring whole mixed inner class mess
104    }
105   
 
106  21 toggle public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached, AbstractThresholdingCacheOutputStream stream) {
107  21 super(response);
108  21 this.serveIfThresholdReached = serveIfThresholdReached;
109  21 this.originalResponse = response;
110  21 if (stream == null) {
111  17 this.thresholdingOutputStream = new ThresholdingCacheOutputStream(threshold);
112    } else {
113  4 this.thresholdingOutputStream = stream;
114    }
115  21 this.wrappedStream = new SimpleServletOutputStream(thresholdingOutputStream);
116    }
117   
 
118    toggle public boolean isThresholdExceeded() {
119    return thresholdingOutputStream.isThresholdExceeded();
120    }
121   
 
122  17 toggle public byte[] getBufferedContent() {
123  17 if (this.thresholdingOutputStream.getInMemoryBuffer() instanceof ByteArrayOutputStream) {
124  16 return ((ByteArrayOutputStream) this.thresholdingOutputStream.getInMemoryBuffer()).toByteArray();
125    }
126    // FIXME: Don't drop already buffered data (see MGNLCACHE-191)
127  1 return new byte[]{};
128    }
129   
 
130    toggle public File getContentFile() {
131    return contentFile;
132    }
133   
134    // MAGNOLIA-1996: this can be called multiple times, e.g. by chunk writers, but always from a single thread.
 
135    toggle @Override
136    public ServletOutputStream getOutputStream() throws IOException {
137    return wrappedStream;
138    }
139   
 
140    toggle public ThresholdingOutputStream getThresholdingOutputStream() throws IOException {
141    return thresholdingOutputStream;
142    }
143   
 
144  2 toggle @Override
145    public PrintWriter getWriter() throws IOException {
146  2 if (wrappedWriter == null) {
147  2 String encoding = getCharacterEncoding();
148  2 wrappedWriter = encoding != null
149    ? new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding))
150    : new PrintWriter(new OutputStreamWriter(getOutputStream()));
151    }
152   
153  2 return wrappedWriter;
154    }
155   
 
156  6 toggle @Override
157    public void flushBuffer() throws IOException {
158  6 flush();
159    }
160   
 
161  10 toggle public void flush() throws IOException {
162  10 wrappedStream.flush();
163   
164  10 if (wrappedWriter != null) {
165  2 wrappedWriter.flush();
166    }
167    }
168   
 
169  0 toggle @Override
170    public void reset() {
171  0 super.reset();
172   
173  0 wrappedWriter = null;
174  0 status = SC_OK;
175  0 headers.clear();
176    // cleanup temp file if any
177  0 cleanUp();
178    }
179   
 
180  0 toggle @Override
181    public void resetBuffer() {
182  0 super.resetBuffer();
183  0 wrappedWriter = null;
184  0 cleanUp();
185    }
186   
 
187  0 toggle public void cleanUp() {
188  0 if (contentFile != null && contentFile.exists()) {
189  0 if (!contentFile.delete()) {
190  0 log.error("Can't delete file: " + contentFile);
191    }
192    }
193  0 contentFile = null;
194    }
195   
 
196    toggle public int getStatus() {
197    return status;
198    }
199   
 
200    toggle public boolean isError() {
201    return isError;
202    }
203   
 
204    toggle public MultiMap getHeaders() {
205    return headers;
206    }
207   
 
208  11 toggle public long getLastModified() {
209    // we're using a MultiMap. And all this is to workaround code that would possibly set the Last-Modified header with a String value
210    // it will also fail if multiple values have been set.
211  11 final Collection values = (Collection) headers.get("Last-Modified");
212  11 if (values == null || values.size() != 1) {
213  2 throw new IllegalStateException("Can't get Last-Modified header : no or multiple values : " + values);
214    }
215  9 final Object value = values.iterator().next();
216  9 if (value instanceof String) {
217  1 return parseStringDate((String) value);
218  8 } else if (value instanceof Long) {
219  7 return ((Long) value).longValue();
220    } else {
221  1 throw new IllegalStateException("Can't get Last-Modified header : " + value);
222    }
223    }
224   
 
225  1 toggle private long parseStringDate(String value) {
226  1 Date date = DateUtils.parseDate(value);
227  1 if (date == null) {
228  0 throw new IllegalStateException("Could not parse Last-Modified header with value " + value);
229    }
230   
231  1 return date.getTime();
232    }
233   
234    /**
235    * Enables expiration detection, response headers are then intercepted and suppressed from the response and used
236    * internally to calculate when the response expires (its time to live value). Use {@link #getTimeToLiveInSeconds()} to get the calculated value. See {@link ResponseExpirationCalculator} for more details on how the calculation
237    * is performed.
238    */
 
239  8 toggle public void setResponseExpirationDetectionEnabled() {
240  8 this.responseExpirationCalculator = new ResponseExpirationCalculator();
241    }
242   
243    /**
244    * Returns the number of seconds the response can be cached, where 0 means that it must not be cached and -1 means
245    * that it there is no indication on how long it can be cached for. Will also return -1 if expiration calculation is
246    * disabled.
247    *
248    * @see #setResponseExpirationDetectionEnabled()
249    */
 
250  0 toggle public int getTimeToLiveInSeconds() {
251  0 return responseExpirationCalculator != null ? responseExpirationCalculator.getMaxAgeInSeconds() : -1;
252    }
253   
 
254    toggle public String getRedirectionLocation() {
255    return redirectionLocation;
256    }
257   
 
258  7 toggle @Override
259    public void setDateHeader(String name, long date) {
260  7 replaceHeader(name, Long.valueOf(date));
261    }
262   
 
263  0 toggle @Override
264    public void addDateHeader(String name, long date) {
265  0 appendHeader(name, Long.valueOf(date));
266    }
267   
 
268  6 toggle @Override
269    public void setHeader(String name, String value) {
270  6 replaceHeader(name, value);
271    }
272   
 
273  3 toggle @Override
274    public void addHeader(String name, String value) {
275  3 appendHeader(name, value);
276    }
277   
 
278  1 toggle @Override
279    public void setIntHeader(String name, int value) {
280  1 replaceHeader(name, Integer.valueOf(value));
281    }
282   
 
283  0 toggle @Override
284    public void addIntHeader(String name, int value) {
285  0 appendHeader(name, Integer.valueOf(value));
286    }
287   
 
288  0 toggle @Override
289    public boolean containsHeader(String name) {
290  0 return headers.containsKey(name);
291    }
292   
 
293  14 toggle private void replaceHeader(String name, Object value) {
294  14 if (responseExpirationCalculator != null) {
295  7 responseExpirationCalculator.addHeader(name, value);
296    }
297  14 headers.remove(name);
298  14 headers.put(name, value);
299    }
300   
 
301  3 toggle private void appendHeader(String name, Object value) {
302  3 if (responseExpirationCalculator != null) {
303  2 responseExpirationCalculator.addHeader(name, value);
304    }
305  3 headers.put(name, value);
306    }
307   
 
308    toggle @Override
309    public void setStatus(int status) {
310    this.status = status;
311    }
312   
 
313  0 toggle @Override
314    public void setStatus(int status, String string) {
315  0 this.status = status;
316    }
317   
 
318  0 toggle @Override
319    public void sendRedirect(String location) throws IOException {
320  0 this.status = SC_MOVED_TEMPORARILY;
321  0 this.redirectionLocation = location;
322    }
323   
 
324  0 toggle @Override
325    public void sendError(int status, String errorMsg) throws IOException {
326  0 this.errorMsg = errorMsg;
327  0 this.status = status;
328  0 this.isError = true;
329    }
330   
 
331  1 toggle @Override
332    public void sendError(int status) throws IOException {
333  1 this.status = status;
334  1 this.isError = true;
335    }
336   
 
337    toggle @Override
338    public void setContentLength(int len) {
339    this.contentLength = len;
340    }
341   
 
342  11 toggle public int getContentLength() {
343  11 return (int) (contentLength >= 0 ? contentLength : thresholdingOutputStream.getByteCount());
344    }
345   
 
346  1 toggle public void replay(HttpServletResponse target) throws IOException {
347  1 replayHeadersAndStatus(target);
348  1 replayContent(target, true);
349    }
350   
 
351  5 toggle public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
352  5 if (isError) {
353  1 if (errorMsg != null) {
354  0 target.sendError(status, errorMsg);
355    } else {
356  1 target.sendError(status);
357    }
358  4 } else if (redirectionLocation != null) {
359  0 target.sendRedirect(redirectionLocation);
360    } else {
361  4 target.setStatus(status);
362    }
363   
364  5 target.setStatus(getStatus());
365   
366  5 final Iterator it = headers.keySet().iterator();
367  5 while (it.hasNext()) {
368  0 final String header = (String) it.next();
369   
370  0 final Collection values = (Collection) headers.get(header);
371  0 final Iterator valIt = values.iterator();
372  0 while (valIt.hasNext()) {
373  0 final Object val = valIt.next();
374  0 RequestHeaderUtil.setHeader(target, header, val);
375    }
376    }
377   
378  5 target.setContentType(getContentType());
379  5 target.setCharacterEncoding(getCharacterEncoding());
380  5 target.setContentLength(getContentLength());
381    }
382   
 
383  3 toggle public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
384  3 if (setContentLength) {
385  3 target.setContentLength(getContentLength());
386    }
387  3 if (getContentLength() > 0) {
388  3 if (isThresholdExceeded() && !serveIfThresholdReached) {
389  1 FileInputStream in = FileUtils.openInputStream(getContentFile());
390  1 IOUtils.copy(in, target.getOutputStream());
391  1 IOUtils.closeQuietly(in);
392    } else {
393  2 IOUtils.copy(new ByteArrayInputStream(getBufferedContent()), target.getOutputStream());
394    }
395  3 target.flushBuffer();
396    }
397    }
398   
 
399  5 toggle protected OutputStream thresholdReached(OutputStream out) throws IOException {
400   
401  5 if (serveIfThresholdReached) {
402  1 replayHeadersAndStatus(originalResponse);
403  1 out = originalResponse.getOutputStream();
404  1 log.debug("Reached threshold for in-memory caching. Will not cache and stream response directly to user.");
405    } else {
406  4 contentFile = File.createTempFile(CACHE_TEMP_FILE_PREFIX, null, Path.getTempDirectory());
407  4 if (contentFile != null) {
408  4 log.debug("Reached threshold for in-memory caching. Will continue caching in new cache temp file {}", contentFile.getAbsolutePath());
409  4 contentFile.deleteOnExit();
410  4 out = new FileOutputStream(contentFile);
411    } else {
412  0 log.error("Reached threshold for in-memory caching, but unable to create the new cache temp file. Will not cache and stream response directly to user.");
413  0 replayHeadersAndStatus(originalResponse);
414  0 out = originalResponse.getOutputStream();
415    }
416    }
417  5 out.write(getBufferedContent());
418  5 out.flush();
419  5 return out;
420    }
421   
 
422    private final class ThresholdingCacheOutputStream extends AbstractThresholdingCacheOutputStream {
423   
 
424  17 toggle private ThresholdingCacheOutputStream(int threshold) {
425  17 super(threshold);
426    }
427   
 
428  267 toggle @Override
429    protected OutputStream getStream() throws IOException {
430  267 return out;
431    }
432   
 
433  4 toggle @Override
434    protected void thresholdReached() throws IOException {
435  4 out = CacheResponseWrapper.this.thresholdReached(out);
436    }
437    }
438    }