1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 package info.magnolia.module.cache.filter;
35
36 import info.magnolia.cms.core.FileSystemHelper;
37 import info.magnolia.cms.util.RequestHeaderUtil;
38 import info.magnolia.objectfactory.Components;
39
40 import java.io.ByteArrayInputStream;
41 import java.io.ByteArrayOutputStream;
42 import java.io.File;
43 import java.io.FileInputStream;
44 import java.io.FileOutputStream;
45 import java.io.IOException;
46 import java.io.OutputStream;
47 import java.io.OutputStreamWriter;
48 import java.io.PrintWriter;
49 import java.util.Collection;
50 import java.util.Date;
51 import java.util.Iterator;
52
53 import javax.servlet.ServletOutputStream;
54 import javax.servlet.http.HttpServletResponse;
55 import javax.servlet.http.HttpServletResponseWrapper;
56
57 import org.apache.commons.collections4.MultiMap;
58 import org.apache.commons.collections4.map.MultiValueMap;
59 import org.apache.commons.io.FileUtils;
60 import org.apache.commons.io.IOUtils;
61 import org.apache.commons.io.output.ThresholdingOutputStream;
62 import org.apache.http.client.utils.DateUtils;
63 import org.slf4j.Logger;
64 import org.slf4j.LoggerFactory;
65
66
67
68
69
70
71
72
73
74 public class CacheResponseWrapper extends HttpServletResponseWrapper {
75
76 public static final int DEFAULT_THRESHOLD = 500 * 1024;
77
78
79
80 public static final String ATTRIBUTE_IN_MEMORY_THRESHOLD = "mgnlInMemoryThreshold";
81
82 public static final String CACHE_TEMP_FILE_PREFIX = "cacheStream";
83
84 private static final Logger log = LoggerFactory.getLogger(CacheResponseWrapper.class);
85
86 private ServletOutputStream wrappedStream;
87 private PrintWriter wrappedWriter = null;
88 private final MultiMap headers = new MultiValueMap();
89 private int status = SC_OK;
90 private boolean isError;
91 private String redirectionLocation;
92 private final HttpServletResponse originalResponse;
93 private File contentFile;
94 private long contentLength = -1;
95 private ResponseExpirationCalculator responseExpirationCalculator;
96
97 private final AbstractThresholdingCacheOutputStream thresholdingOutputStream;
98 private final boolean serveIfThresholdReached;
99
100 private String errorMsg;
101
102 public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached) {
103 this(response, threshold, serveIfThresholdReached, null);
104
105 }
106
107 public CacheResponseWrapper(final HttpServletResponse response, int threshold, boolean serveIfThresholdReached, AbstractThresholdingCacheOutputStream stream) {
108 super(response);
109 this.serveIfThresholdReached = serveIfThresholdReached;
110 this.originalResponse = response;
111 if (stream == null) {
112 this.thresholdingOutputStream = new ThresholdingCacheOutputStream(threshold);
113 } else {
114 this.thresholdingOutputStream = stream;
115 }
116 this.wrappedStream = new SimpleServletOutputStream(thresholdingOutputStream);
117 }
118
119 public boolean isThresholdExceeded() {
120 return thresholdingOutputStream.isThresholdExceeded();
121 }
122
123 public byte[] getBufferedContent() {
124 if (this.thresholdingOutputStream.getInMemoryBuffer() instanceof ByteArrayOutputStream) {
125 return ((ByteArrayOutputStream) this.thresholdingOutputStream.getInMemoryBuffer()).toByteArray();
126 }
127
128 return new byte[]{};
129 }
130
131 public File getContentFile() {
132 return contentFile;
133 }
134
135
136 @Override
137 public ServletOutputStream getOutputStream() throws IOException {
138 return wrappedStream;
139 }
140
141 public ThresholdingOutputStream getThresholdingOutputStream() throws IOException {
142 return thresholdingOutputStream;
143 }
144
145 @Override
146 public PrintWriter getWriter() throws IOException {
147 if (wrappedWriter == null) {
148 String encoding = getCharacterEncoding();
149 wrappedWriter = encoding != null
150 ? new PrintWriter(new OutputStreamWriter(getOutputStream(), encoding))
151 : new PrintWriter(new OutputStreamWriter(getOutputStream()));
152 }
153
154 return wrappedWriter;
155 }
156
157 @Override
158 public void flushBuffer() throws IOException {
159 flush();
160 }
161
162 public void flush() throws IOException {
163 wrappedStream.flush();
164
165 if (wrappedWriter != null) {
166 wrappedWriter.flush();
167 }
168 }
169
170 @Override
171 public void reset() {
172 super.reset();
173
174 wrappedWriter = null;
175 status = SC_OK;
176 headers.clear();
177
178 cleanUp();
179 }
180
181 @Override
182 public void resetBuffer() {
183 super.resetBuffer();
184 wrappedWriter = null;
185 cleanUp();
186 }
187
188 public void cleanUp() {
189 if (contentFile != null && contentFile.exists()) {
190 if (!contentFile.delete()) {
191 log.error("Can't delete file: " + contentFile);
192 }
193 }
194 contentFile = null;
195 }
196
197 public int getStatus() {
198 return status;
199 }
200
201 public boolean isError() {
202 return isError;
203 }
204
205 public MultiMap getHeaders() {
206 return headers;
207 }
208
209 public long getLastModified() {
210
211
212 final Collection values = (Collection) headers.get("Last-Modified");
213 if (values == null || values.size() != 1) {
214 throw new IllegalStateException("Can't get Last-Modified header : no or multiple values : " + values);
215 }
216 final Object value = values.iterator().next();
217 if (value instanceof String) {
218 return parseStringDate((String) value);
219 } else if (value instanceof Long) {
220 return ((Long) value).longValue();
221 } else {
222 throw new IllegalStateException("Can't get Last-Modified header : " + value);
223 }
224 }
225
226 private long parseStringDate(String value) {
227 Date date = DateUtils.parseDate(value);
228 if (date == null) {
229 throw new IllegalStateException("Could not parse Last-Modified header with value " + value);
230 }
231
232 return date.getTime();
233 }
234
235
236
237
238
239
240 public void setResponseExpirationDetectionEnabled() {
241 this.responseExpirationCalculator = new ResponseExpirationCalculator();
242 }
243
244
245
246
247
248
249
250
251 public int getTimeToLiveInSeconds() {
252 return responseExpirationCalculator != null ? responseExpirationCalculator.getMaxAgeInSeconds() : -1;
253 }
254
255 public String getRedirectionLocation() {
256 return redirectionLocation;
257 }
258
259 @Override
260 public void setDateHeader(String name, long date) {
261 replaceHeader(name, Long.valueOf(date));
262 }
263
264 @Override
265 public void addDateHeader(String name, long date) {
266 appendHeader(name, Long.valueOf(date));
267 }
268
269 @Override
270 public void setHeader(String name, String value) {
271 replaceHeader(name, value);
272 }
273
274 @Override
275 public void addHeader(String name, String value) {
276 appendHeader(name, value);
277 }
278
279 @Override
280 public void setIntHeader(String name, int value) {
281 replaceHeader(name, Integer.valueOf(value));
282 }
283
284 @Override
285 public void addIntHeader(String name, int value) {
286 appendHeader(name, Integer.valueOf(value));
287 }
288
289 @Override
290 public boolean containsHeader(String name) {
291 return headers.containsKey(name);
292 }
293
294 private void replaceHeader(String name, Object value) {
295 if (responseExpirationCalculator != null) {
296 responseExpirationCalculator.addHeader(name, value);
297 }
298 headers.remove(name);
299 headers.put(name, value);
300 }
301
302 private void appendHeader(String name, Object value) {
303 if (responseExpirationCalculator != null) {
304 responseExpirationCalculator.addHeader(name, value);
305 }
306 headers.put(name, value);
307 }
308
309 @Override
310 public void setStatus(int status) {
311 this.status = status;
312 }
313
314 @Override
315 @Deprecated
316 public void setStatus(int status, String string) {
317 this.status = status;
318 }
319
320 @Override
321 public void sendRedirect(String location) throws IOException {
322 this.status = SC_MOVED_TEMPORARILY;
323 this.redirectionLocation = location;
324 }
325
326 @Override
327 public void sendError(int status, String errorMsg) throws IOException {
328 this.errorMsg = errorMsg;
329 this.status = status;
330 this.isError = true;
331 }
332
333 @Override
334 public void sendError(int status) throws IOException {
335 this.status = status;
336 this.isError = true;
337 }
338
339 @Override
340 public void setContentLength(int len) {
341 this.contentLength = len;
342 }
343
344 public int getContentLength() {
345 return (int) (contentLength >= 0 ? contentLength : thresholdingOutputStream.getByteCount());
346 }
347
348 public void replay(HttpServletResponse target) throws IOException {
349 replayHeadersAndStatus(target);
350 replayContent(target, true);
351 }
352
353 public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
354 if (isError) {
355 if (errorMsg != null) {
356 target.sendError(status, errorMsg);
357 } else {
358 target.sendError(status);
359 }
360 } else if (redirectionLocation != null) {
361 target.sendRedirect(redirectionLocation);
362 } else {
363 target.setStatus(status);
364 }
365
366 target.setStatus(getStatus());
367
368 final Iterator it = headers.keySet().iterator();
369 while (it.hasNext()) {
370 final String header = (String) it.next();
371
372 final Collection values = (Collection) headers.get(header);
373 final Iterator valIt = values.iterator();
374 while (valIt.hasNext()) {
375 final Object val = valIt.next();
376 RequestHeaderUtil.setHeader(target, header, val);
377 }
378 }
379
380 target.setContentType(getContentType());
381 target.setCharacterEncoding(getCharacterEncoding());
382 target.setContentLength(getContentLength());
383 }
384
385 public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
386 if (setContentLength) {
387 target.setContentLength(getContentLength());
388 }
389 if (getContentLength() > 0) {
390 if (isThresholdExceeded() && !serveIfThresholdReached) {
391 FileInputStream in = FileUtils.openInputStream(getContentFile());
392 IOUtils.copy(in, target.getOutputStream());
393 IOUtils.closeQuietly(in);
394 } else {
395 IOUtils.copy(new ByteArrayInputStream(getBufferedContent()), target.getOutputStream());
396 }
397 target.flushBuffer();
398 }
399 }
400
401 protected OutputStream thresholdReached(OutputStream out) throws IOException {
402
403 if (serveIfThresholdReached) {
404 replayHeadersAndStatus(originalResponse);
405 out = originalResponse.getOutputStream();
406 log.debug("Reached threshold for in-memory caching. Will not cache and stream response directly to user.");
407 } else {
408 contentFile = File.createTempFile(CACHE_TEMP_FILE_PREFIX, null, Components.getComponent(FileSystemHelper.class).getTempDirectory());
409 if (contentFile != null) {
410 log.debug("Reached threshold for in-memory caching. Will continue caching in new cache temp file {}", contentFile.getAbsolutePath());
411 contentFile.deleteOnExit();
412 out = new FileOutputStream(contentFile);
413 } else {
414 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.");
415 replayHeadersAndStatus(originalResponse);
416 out = originalResponse.getOutputStream();
417 }
418 }
419 out.write(getBufferedContent());
420 out.flush();
421 return out;
422 }
423
424 private final class ThresholdingCacheOutputStream extends AbstractThresholdingCacheOutputStream {
425
426 private ThresholdingCacheOutputStream(int threshold) {
427 super(threshold);
428 }
429
430 @Override
431 protected OutputStream getStream() {
432 return out;
433 }
434
435 @Override
436 protected void thresholdReached() throws IOException {
437 out = CacheResponseWrapper.this.thresholdReached(out);
438 }
439 }
440 }