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