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