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