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.Iterator;
50
51 import javax.servlet.ServletOutputStream;
52 import javax.servlet.http.HttpServletResponse;
53 import javax.servlet.http.HttpServletResponseWrapper;
54
55 import org.apache.commons.collections.MultiMap;
56 import org.apache.commons.collections.map.MultiValueMap;
57 import org.apache.commons.httpclient.util.DateParseException;
58 import org.apache.commons.httpclient.util.DateUtil;
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.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 try {
226 return DateUtil.parseDate(value).getTime();
227 } catch (DateParseException e) {
228 throw new IllegalStateException("Could not parse Last-Modified header with value " + value + " : " + e.getMessage());
229 }
230 }
231
232
233
234
235
236
237 public void setResponseExpirationDetectionEnabled() {
238 this.responseExpirationCalculator = new ResponseExpirationCalculator();
239 }
240
241
242
243
244
245
246
247
248 public int getTimeToLiveInSeconds() {
249 return responseExpirationCalculator != null ? responseExpirationCalculator.getMaxAgeInSeconds() : -1;
250 }
251
252 public String getRedirectionLocation() {
253 return redirectionLocation;
254 }
255
256 @Override
257 public void setDateHeader(String name, long date) {
258 replaceHeader(name, Long.valueOf(date));
259 }
260
261 @Override
262 public void addDateHeader(String name, long date) {
263 appendHeader(name, Long.valueOf(date));
264 }
265
266 @Override
267 public void setHeader(String name, String value) {
268 replaceHeader(name, value);
269 }
270
271 @Override
272 public void addHeader(String name, String value) {
273 appendHeader(name, value);
274 }
275
276 @Override
277 public void setIntHeader(String name, int value) {
278 replaceHeader(name, Integer.valueOf(value));
279 }
280
281 @Override
282 public void addIntHeader(String name, int value) {
283 appendHeader(name, Integer.valueOf(value));
284 }
285
286 @Override
287 public boolean containsHeader(String name) {
288 return headers.containsKey(name);
289 }
290
291 private void replaceHeader(String name, Object value) {
292 if (responseExpirationCalculator != null) {
293 responseExpirationCalculator.addHeader(name, value);
294 }
295 headers.remove(name);
296 headers.put(name, value);
297 }
298
299 private void appendHeader(String name, Object value) {
300 if (responseExpirationCalculator != null) {
301 responseExpirationCalculator.addHeader(name, value);
302 }
303 headers.put(name, value);
304 }
305
306 @Override
307 public void setStatus(int status) {
308 this.status = status;
309 }
310
311 @Override
312 public void setStatus(int status, String string) {
313 this.status = status;
314 }
315
316 @Override
317 public void sendRedirect(String location) throws IOException {
318 this.status = SC_MOVED_TEMPORARILY;
319 this.redirectionLocation = location;
320 }
321
322 @Override
323 public void sendError(int status, String errorMsg) throws IOException {
324 this.errorMsg = errorMsg;
325 this.status = status;
326 this.isError = true;
327 }
328
329 @Override
330 public void sendError(int status) throws IOException {
331 this.status = status;
332 this.isError = true;
333 }
334
335 @Override
336 public void setContentLength(int len) {
337 this.contentLength = len;
338 }
339
340 public int getContentLength() {
341 return (int) (contentLength >= 0 ? contentLength : thresholdingOutputStream.getByteCount());
342 }
343
344 public void replay(HttpServletResponse target) throws IOException {
345 replayHeadersAndStatus(target);
346 replayContent(target, true);
347 }
348
349 public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
350 if (isError) {
351 if (errorMsg != null) {
352 target.sendError(status, errorMsg);
353 } else {
354 target.sendError(status);
355 }
356 } else if (redirectionLocation != null) {
357 target.sendRedirect(redirectionLocation);
358 } else {
359 target.setStatus(status);
360 }
361
362 target.setStatus(getStatus());
363
364 final Iterator it = headers.keySet().iterator();
365 while (it.hasNext()) {
366 final String header = (String) it.next();
367
368 final Collection values = (Collection) headers.get(header);
369 final Iterator valIt = values.iterator();
370 while (valIt.hasNext()) {
371 final Object val = valIt.next();
372 RequestHeaderUtil.setHeader(target, header, val);
373 }
374 }
375
376 target.setContentType(getContentType());
377 target.setCharacterEncoding(getCharacterEncoding());
378 }
379
380 public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
381 if (setContentLength) {
382 target.setContentLength(getContentLength());
383 }
384 if (getContentLength() > 0) {
385 if (isThresholdExceeded()) {
386 FileInputStream in = FileUtils.openInputStream(getContentFile());
387 IOUtils.copy(in, target.getOutputStream());
388 IOUtils.closeQuietly(in);
389 } else {
390 IOUtils.copy(new ByteArrayInputStream(((ByteArrayOutputStream) this.thresholdingOutputStream.getInMemoryBuffer()).toByteArray()), target.getOutputStream());
391 }
392 target.flushBuffer();
393 }
394 }
395
396 protected OutputStream thresholdReached(OutputStream out) throws IOException {
397
398 if (serveIfThresholdReached) {
399 replayHeadersAndStatus(originalResponse);
400 out = originalResponse.getOutputStream();
401 log.debug("Reached threshold for in-memory caching. Will not cache and stream response directly to user.");
402 } else {
403 contentFile = File.createTempFile(CACHE_TEMP_FILE_PREFIX, null, Path.getTempDirectory());
404 if (contentFile != null) {
405 log.debug("Reached threshold for in-memory caching. Will continue caching in new cache temp file {}", contentFile.getAbsolutePath());
406 contentFile.deleteOnExit();
407 out = new FileOutputStream(contentFile);
408 } else {
409 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.");
410 replayHeadersAndStatus(originalResponse);
411 out = originalResponse.getOutputStream();
412 }
413 }
414 out.write(getBufferedContent());
415 out.flush();
416 return out;
417 }
418
419 private final class ThresholdingCacheOutputStream extends AbstractThresholdingCacheOutputStream {
420
421 private ThresholdingCacheOutputStream(int threshold) {
422 super(threshold);
423 }
424
425 @Override
426 protected OutputStream getStream() throws IOException {
427 return out;
428 }
429
430 @Override
431 protected void thresholdReached() throws IOException {
432 out = CacheResponseWrapper.this.thresholdReached(out);
433 }
434 }
435 }