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 || !responseExpirationCalculator.addHeader(name, value)) {
293 headers.remove(name);
294 headers.put(name, value);
295 }
296 }
297
298 private void appendHeader(String name, Object value) {
299 if (responseExpirationCalculator == null || !responseExpirationCalculator.addHeader(name, value)) {
300 headers.put(name, value);
301 }
302 }
303
304 @Override
305 public void setStatus(int status) {
306 this.status = status;
307 }
308
309 @Override
310 public void setStatus(int status, String string) {
311 this.status = status;
312 }
313
314 @Override
315 public void sendRedirect(String location) throws IOException {
316 this.status = SC_MOVED_TEMPORARILY;
317 this.redirectionLocation = location;
318 }
319
320 @Override
321 public void sendError(int status, String errorMsg) throws IOException {
322 this.errorMsg = errorMsg;
323 this.status = status;
324 this.isError = true;
325 }
326
327 @Override
328 public void sendError(int status) throws IOException {
329 this.status = status;
330 this.isError = true;
331 }
332
333 @Override
334 public void setContentLength(int len) {
335 this.contentLength = len;
336 }
337
338 public int getContentLength() {
339 return (int) (contentLength >= 0 ? contentLength : thresholdingOutputStream.getByteCount());
340 }
341
342 public void replay(HttpServletResponse target) throws IOException {
343 replayHeadersAndStatus(target);
344 replayContent(target, true);
345 }
346
347 public void replayHeadersAndStatus(HttpServletResponse target) throws IOException {
348 if (isError) {
349 if (errorMsg != null) {
350 target.sendError(status, errorMsg);
351 } else {
352 target.sendError(status);
353 }
354 } else if (redirectionLocation != null) {
355 target.sendRedirect(redirectionLocation);
356 } else {
357 target.setStatus(status);
358 }
359
360 target.setStatus(getStatus());
361
362 final Iterator it = headers.keySet().iterator();
363 while (it.hasNext()) {
364 final String header = (String) it.next();
365
366 final Collection values = (Collection) headers.get(header);
367 final Iterator valIt = values.iterator();
368 while (valIt.hasNext()) {
369 final Object val = valIt.next();
370 RequestHeaderUtil.setHeader(target, header, val);
371 }
372 }
373
374 target.setContentType(getContentType());
375 target.setCharacterEncoding(getCharacterEncoding());
376 }
377
378 public void replayContent(HttpServletResponse target, boolean setContentLength) throws IOException {
379 if (setContentLength) {
380 target.setContentLength(getContentLength());
381 }
382 if (getContentLength() > 0) {
383 if (isThresholdExceeded()) {
384 FileInputStream in = FileUtils.openInputStream(getContentFile());
385 IOUtils.copy(in, target.getOutputStream());
386 IOUtils.closeQuietly(in);
387 } else {
388 IOUtils.copy(new ByteArrayInputStream(((ByteArrayOutputStream) this.thresholdingOutputStream.getInMemoryBuffer()).toByteArray()), target.getOutputStream());
389 }
390 target.flushBuffer();
391 }
392 }
393
394 protected OutputStream thresholdReached(OutputStream out) throws IOException {
395
396 if (serveIfThresholdReached) {
397 replayHeadersAndStatus(originalResponse);
398 out = originalResponse.getOutputStream();
399 log.debug("Reached threshold for in-memory caching. Will not cache and stream response directly to user.");
400 } else {
401 contentFile = File.createTempFile(CACHE_TEMP_FILE_PREFIX, null, Path.getTempDirectory());
402 if (contentFile != null) {
403 log.debug("Reached threshold for in-memory caching. Will continue caching in new cache temp file {}", contentFile.getAbsolutePath());
404 contentFile.deleteOnExit();
405 out = new FileOutputStream(contentFile);
406 } else {
407 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.");
408 replayHeadersAndStatus(originalResponse);
409 out = originalResponse.getOutputStream();
410 }
411 }
412 out.write(getBufferedContent());
413 out.flush();
414 return out;
415 }
416
417 private final class ThresholdingCacheOutputStream extends AbstractThresholdingCacheOutputStream {
418
419 private ThresholdingCacheOutputStream(int threshold) {
420 super(threshold);
421 }
422
423 @Override
424 protected OutputStream getStream() throws IOException {
425 return out;
426 }
427
428 @Override
429 protected void thresholdReached() throws IOException {
430 out = CacheResponseWrapper.this.thresholdReached(out);
431 }
432 }
433 }