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