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