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