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