0
|
1 package cachingfilter;
|
|
2
|
|
3 import java.io.BufferedOutputStream;
|
|
4 import java.io.File;
|
|
5 import java.io.FileOutputStream;
|
|
6 import java.io.IOException;
|
|
7 import java.io.OutputStream;
|
|
8 import java.io.OutputStreamWriter;
|
|
9 import java.io.PrintWriter;
|
|
10 import java.io.ObjectOutputStream;
|
|
11 import java.io.ObjectInputStream;
|
|
12 import java.net.URLEncoder;
|
|
13 import java.util.Map;
|
|
14 import java.util.HashMap;
|
|
15 import java.util.Set;
|
|
16 import java.util.HashSet;
|
|
17 import java.util.List;
|
|
18 import java.util.ArrayList;
|
|
19 import java.util.Collections;
|
|
20 import java.util.Iterator;
|
|
21 import java.util.Collection;
|
|
22 import javax.servlet.ServletException;
|
|
23 import javax.servlet.ServletOutputStream;
|
|
24 import javax.servlet.ServletRequest;
|
|
25 import javax.servlet.ServletResponse;
|
|
26 import javax.servlet.http.HttpServletRequest;
|
|
27 import javax.servlet.http.HttpServletRequestWrapper;
|
|
28 import javax.servlet.http.HttpServletResponse;
|
|
29 import javax.servlet.http.HttpServletResponseWrapper;
|
|
30 import org.slf4j.Logger;
|
|
31 import org.slf4j.LoggerFactory;
|
|
32
|
|
33
|
|
34 final class CachingResponseWrapper extends HttpServletResponseWrapper {
|
|
35 private static final Logger logger = LoggerFactory.getLogger(CachingResponseWrapper.class);
|
|
36
|
|
37 private static Set<String> cacheableHeaders = new HashSet<String>();
|
|
38 static {
|
|
39 for( String header : new String[]{
|
|
40 "Last-Modified",
|
|
41 "Etag",
|
|
42 "Content-Type",
|
|
43 "Cache-Control",
|
|
44 "Content-Encoding",
|
|
45 } ) {
|
|
46 cacheableHeaders.add( header.toLowerCase() );
|
|
47 }
|
|
48 }
|
|
49
|
|
50 private final CachingFilter cachingFilter;
|
|
51 private final CachingRequestWrapper request;
|
|
52 private final String fullName;
|
|
53 private boolean isCachingToFile;
|
|
54 private CachedPage cachingFile;
|
|
55 private ServletOutputStream outputStream;
|
|
56 private PrintWriter writer;
|
|
57 private int status = SC_OK;
|
|
58 private Long lastModified = null;
|
|
59 private String etag = null;
|
|
60 private FileHandler fileHandler = null;
|
|
61 private ObjectInputStream ois = null;
|
|
62 private final List<ResponseAction> actions = new ArrayList<ResponseAction>();
|
|
63 private boolean isLocked = false; // for debugging
|
|
64 private StringBuilder log = new StringBuilder();
|
|
65
|
|
66 private void log(String msg) {
|
|
67 log.append(msg).append('\n');
|
|
68 }
|
|
69
|
|
70 CachingResponseWrapper( CachingFilter cachingFilter, CachingRequestWrapper request, HttpServletResponse response ) {
|
|
71 super(response);
|
|
72 try {
|
|
73 this.cachingFilter = cachingFilter;
|
|
74 this.request = request;
|
|
75 this.fullName = getFullName();
|
|
76 boolean ok = false;
|
|
77 try {
|
|
78 if( !openObjectInputStream() ) { // calls lock()
|
|
79 ok = true;
|
|
80 return;
|
|
81 }
|
|
82 Long cachedLastModified = (Long)ois.readObject();
|
|
83 if( cachedLastModified==null )
|
|
84 cachedLastModified = -1L;
|
|
85 request.headerMap.put( CachingRequestWrapper.IF_MODIFIED_SINCE, cachedLastModified );
|
|
86 String cachedEtag = (String)ois.readObject();
|
|
87 request.headerMap.put( CachingRequestWrapper.IF_NONE_MATCH, cachedEtag );
|
|
88 ok = true;
|
|
89 } finally {
|
|
90 if( !ok && cachingFile != null )
|
|
91 unlock();
|
|
92 }
|
|
93 } catch(ClassNotFoundException e) {
|
|
94 throw new RuntimeException(e);
|
|
95 } catch(IOException e) {
|
|
96 throw new RuntimeException(e);
|
|
97 }
|
|
98 }
|
|
99
|
|
100 private String getFullName()
|
|
101 throws IOException
|
|
102 {
|
|
103 StringBuffer url = request.getRequestURL();
|
|
104 String queryString = request.getQueryString();
|
|
105 if( queryString != null ) {
|
|
106 url.append( '?' );
|
|
107 url.append( queryString );
|
|
108 }
|
|
109 log("url = "+url);
|
|
110 String fullUrl = URLEncoder.encode(url.toString(),"UTF-8");
|
|
111 String acceptEncoding = request.getHeader("Accept-Encoding");
|
|
112 logger.trace("acceptEncoding = "+acceptEncoding);
|
|
113 if( acceptEncoding == null )
|
|
114 return fullUrl;
|
|
115 Set<String> knownEncodings = cachingFilter.getEncodings();
|
|
116 List<String> list = new ArrayList<String>();
|
|
117 for( String encoding : acceptEncoding.split(",") ) {
|
|
118 encoding = encoding.trim();
|
|
119 if( knownEncodings.contains(encoding) )
|
|
120 list.add(encoding);
|
|
121 }
|
|
122 if( list.isEmpty() ) {
|
|
123 request.headerMap.put( CachingRequestWrapper.ACCEPT_ENCODING, null );
|
|
124 return fullUrl;
|
|
125 }
|
|
126 String encodings = join( list, "," );
|
|
127 request.headerMap.put( CachingRequestWrapper.ACCEPT_ENCODING, encodings );
|
|
128 return fullUrl + '~' + encodings;
|
|
129 }
|
|
130
|
|
131 private void lock() {
|
|
132 CachingFilter.locker.lock(cachingFile.name());
|
|
133 log("lock");
|
|
134 isLocked = true;
|
|
135 }
|
|
136
|
|
137 private boolean unlock() {
|
|
138 boolean wasLocked = CachingFilter.locker.unlock(cachingFile.name());
|
|
139 log("unlock "+wasLocked);
|
|
140 if( isLocked != wasLocked )
|
|
141 logger.error("isLocked="+isLocked+" wasLocked="+wasLocked,new Exception());
|
|
142 isLocked = false;
|
|
143 return wasLocked;
|
|
144 }
|
|
145
|
|
146 private static String join(Collection<?> col,String separator) {
|
|
147 if( col.isEmpty() )
|
|
148 return "";
|
|
149 StringBuilder sb = new StringBuilder();
|
|
150 Iterator<?> iter = col.iterator();
|
|
151 sb.append( iter.next() );
|
|
152 while( iter.hasNext() ) {
|
|
153 sb.append( separator ).append( iter.next() );
|
|
154 }
|
|
155 return sb.toString();
|
|
156 }
|
|
157
|
|
158 private static long hashCode(String s) {
|
|
159 final int len = s.length();
|
|
160 long h = 0;
|
|
161 for( int i = 0; i < len; i++ ) {
|
|
162 h = 31*h + s.charAt(i);
|
|
163 }
|
|
164 return h;
|
|
165 }
|
|
166
|
|
167 private boolean openObjectInputStream() {
|
|
168 for( long hash = hashCode(fullName); true; hash++ ) {
|
|
169 String s = Long.toHexString(hash);
|
|
170 cachingFile = cachingFilter.newCachedPage(s);
|
|
171 lock();
|
|
172 if( !cachingFile.exists() ) {
|
|
173 logger.trace("couldn't find "+cachingFile);
|
|
174 return false;
|
|
175 }
|
|
176 try {
|
|
177 FileHandler fileHandler = FileHandler.factory.newInstance(cachingFile.lastFile());
|
|
178 ObjectInputStream ois = new ObjectInputStream(fileHandler.getInputStream());
|
|
179 if( ois.readUTF().equals(fullName) ) {
|
|
180 logger.trace("found file = "+cachingFile);
|
|
181 this.fileHandler = fileHandler;
|
|
182 this.ois = ois;
|
|
183 return true;
|
|
184 }
|
|
185 fileHandler.close();
|
|
186 } catch(IOException e) {
|
|
187 logger.error("couldn't read "+cachingFile+" length="+cachingFile.lastFile().length(),e);
|
|
188 if( !cachingFile.delete() )
|
|
189 logger.error("couldn't delete "+cachingFile);
|
|
190 return false;
|
|
191 }
|
|
192 unlock();
|
|
193 }
|
|
194 }
|
|
195
|
|
196 public void setContentType(String ct)
|
|
197 {
|
|
198 logger.trace("setContentType "+ct);
|
|
199 super.setContentType(ct);
|
|
200 actions.add( new ResponseAction.SetHeader("Content-Type",ct) );
|
|
201 }
|
|
202
|
|
203 public void setStatus(int sc, String sm)
|
|
204 {
|
|
205 logger.trace("setStatus2");
|
|
206 super.setStatus(sc,sm);
|
|
207 this.status = sc;
|
|
208 }
|
|
209
|
|
210 public void setStatus(int sc)
|
|
211 {
|
|
212 logger.trace("setStatus "+sc);
|
|
213 super.setStatus(sc);
|
|
214 this.status = sc;
|
|
215 }
|
|
216
|
|
217 public void setHeader(String name, String value) {
|
|
218 logger.trace("setHeader "+name+" = "+value);
|
|
219 super.setHeader(name,value);
|
|
220 if( "Etag".equalsIgnoreCase(name) )
|
|
221 etag = value;
|
|
222 if( "Last-Modified".equalsIgnoreCase(name) ) {
|
|
223 if( value==null )
|
|
224 lastModified = null;
|
|
225 else
|
|
226 logger.error("unsupported",new Exception());
|
|
227 }
|
|
228 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
229 actions.add( new ResponseAction.SetHeader(name,value) );
|
|
230 }
|
|
231
|
|
232 public void addHeader(String name, String value) {
|
|
233 logger.trace("addHeader "+name+" = "+value);
|
|
234 super.addHeader(name,value);
|
|
235 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
236 actions.add( new ResponseAction.AddHeader(name,value) );
|
|
237 }
|
|
238
|
|
239 public void setIntHeader(String name, int value) {
|
|
240 logger.trace("setIntHeader "+name);
|
|
241 super.setIntHeader(name,value);
|
|
242 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
243 actions.add( new ResponseAction.SetIntHeader(name,value) );
|
|
244 }
|
|
245
|
|
246 public void addIntHeader(String name, int value) {
|
|
247 logger.trace("addIntHeader "+name);
|
|
248 super.addIntHeader(name,value);
|
|
249 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
250 actions.add( new ResponseAction.AddIntHeader(name,value) );
|
|
251 }
|
|
252
|
|
253 public void setDateHeader(String name, long value) {
|
|
254 logger.trace("setDateHeader "+name);
|
|
255 super.setDateHeader(name,value);
|
|
256 value = value / 1000 * 1000; // round to seconds
|
|
257 if( "Last-Modified".equalsIgnoreCase(name) )
|
|
258 lastModified = value;
|
|
259 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
260 actions.add( new ResponseAction.SetDateHeader(name,value) );
|
|
261 }
|
|
262
|
|
263 public void addDateHeader(String name, long value) {
|
|
264 logger.trace("addDateHeader "+name);
|
|
265 super.setDateHeader(name,value);
|
|
266 if( cacheableHeaders.contains(name.toLowerCase()) )
|
|
267 actions.add( new ResponseAction.AddDateHeader(name,value) );
|
|
268 }
|
|
269
|
|
270 public void reset()
|
|
271 {
|
|
272 logger.trace("reset");
|
|
273 super.reset();
|
|
274 resetOutput();
|
|
275 status = SC_OK;
|
|
276 lastModified = null;
|
|
277 etag = null;
|
|
278 actions.clear();
|
|
279 }
|
|
280
|
|
281 public void resetBuffer()
|
|
282 {
|
|
283 logger.trace("resetBuffer");
|
|
284 super.resetBuffer();
|
|
285 resetOutput();
|
|
286 }
|
|
287
|
|
288 private void resetOutput() {
|
|
289 if( isCachingToFile ) {
|
|
290 try {
|
|
291 outputStream.close();
|
|
292 } catch(IOException e) {
|
|
293 logger.error("resetOutput",e);
|
|
294 }
|
|
295 cachingFile.deleteNewFile();
|
|
296 isCachingToFile = false;
|
|
297 }
|
|
298 outputStream = null;
|
|
299 writer = null;
|
|
300 }
|
|
301
|
|
302 public void sendError(int sc, String msg) throws IOException
|
|
303 {
|
|
304 logger.trace("sendError2");
|
|
305 this.status = sc;
|
|
306 resetBuffer();
|
|
307 if( shouldSendFile() ) {
|
|
308 sendFile();
|
|
309 } else {
|
|
310 super.sendError(sc,msg);
|
|
311 }
|
|
312 }
|
|
313
|
|
314 public void sendError(int sc) throws IOException
|
|
315 {
|
|
316 logger.trace("sendError");
|
|
317 this.status = sc;
|
|
318 resetBuffer();
|
|
319 if( shouldSendFile() ) {
|
|
320 sendFile();
|
|
321 } else {
|
|
322 super.sendError(sc);
|
|
323 }
|
|
324 }
|
|
325
|
|
326 public void sendRedirect(String location) throws IOException
|
|
327 {
|
|
328 logger.trace("sendRedirect");
|
|
329 this.status = SC_MOVED_TEMPORARILY;
|
|
330 resetBuffer();
|
|
331 super.sendRedirect(location);
|
|
332 }
|
|
333
|
|
334 public void flushBuffer() throws IOException
|
|
335 {
|
|
336 logger.trace("flushBuffer "+isCommitted());
|
|
337 if( writer != null )
|
|
338 writer.flush();
|
|
339 if( outputStream != null )
|
|
340 outputStream.flush();
|
|
341 else if( shouldSendFile() )
|
|
342 sendFile();
|
|
343 else
|
|
344 getResponse().flushBuffer();
|
|
345 }
|
|
346
|
|
347 private boolean shouldSendFile() {
|
|
348 if( !(status==SC_NOT_MODIFIED && ois!=null) )
|
|
349 return false;
|
|
350 if( request.isCacheable() )
|
|
351 return false; // no need
|
|
352 return true;
|
|
353 }
|
|
354
|
|
355 private void sendFile() throws IOException {
|
|
356 CachingResponseWrapper.super.setHeader("Via","cache-yes");
|
|
357 sendFile2();
|
|
358 }
|
|
359
|
|
360 private void sendFile2() throws IOException {
|
|
361 logger.trace("sendFile");
|
|
362 unlock();
|
|
363 setStatus(SC_OK);
|
|
364 HttpServletResponse response = (HttpServletResponse)getResponse();
|
|
365 try {
|
|
366 @SuppressWarnings("unchecked")
|
|
367 List<ResponseAction> cachedActions = (List<ResponseAction>)ois.readObject();
|
|
368 for( ResponseAction cachedAction : cachedActions ) {
|
|
369 cachedAction.apply(response);
|
|
370 }
|
|
371 } catch(ClassNotFoundException e) {
|
|
372 throw new RuntimeException(e);
|
|
373 }
|
|
374 ServletOutputStream out = response.getOutputStream();
|
|
375 fileHandler.writeTo(out);
|
|
376 }
|
|
377
|
|
378 public ServletOutputStream getOutputStream()
|
|
379 {
|
|
380 logger.trace("getOutputStream");
|
|
381 if (outputStream==null) {
|
|
382 newOutputStream();
|
|
383 } else if (writer!=null)
|
|
384 throw new IllegalStateException("getWriter() called");
|
|
385
|
|
386 return outputStream;
|
|
387 }
|
|
388
|
|
389 public PrintWriter getWriter() throws IOException
|
|
390 {
|
|
391 logger.trace("getWriter");
|
|
392 if (writer==null)
|
|
393 {
|
|
394 if (outputStream!=null)
|
|
395 throw new IllegalStateException("getOutputStream() called");
|
|
396
|
|
397 newOutputStream();
|
|
398 String encoding = getCharacterEncoding();
|
|
399 writer = encoding==null ? new PrintWriter(outputStream)
|
|
400 : new PrintWriter(new OutputStreamWriter(outputStream,encoding));
|
|
401 }
|
|
402 return writer;
|
|
403 }
|
|
404
|
|
405 private boolean isCacheable() {
|
|
406 if( getResponse().isCommitted() ) {
|
|
407 logger.trace("!isCacheable - isCommitted");
|
|
408 return false;
|
|
409 }
|
|
410 if( status != SC_OK ) {
|
|
411 logger.trace("!isCacheable - status="+status);
|
|
412 return false;
|
|
413 }
|
|
414 if( lastModified==null && etag==null ) {
|
|
415 logger.trace("!isCacheable - no lastModified,etag");
|
|
416 return false;
|
|
417 }
|
|
418 return true;
|
|
419 }
|
|
420
|
|
421 private void newOutputStream() {
|
|
422 outputStream = new ProxyServletOutputStream() {
|
|
423 protected OutputStream newOutputStream()
|
|
424 throws IOException
|
|
425 {
|
|
426 CachingResponseWrapper.super.setHeader("Via","cache-no");
|
|
427 ServletOutputStream out = getResponse().getOutputStream();
|
|
428 if( !isCacheable() ) {
|
|
429 unlock();
|
|
430 logger.trace("return getResponse().getOutputStream() "+out.getClass());
|
|
431 return out;
|
|
432 }
|
|
433 try {
|
|
434 File newFile = cachingFile.newFile();
|
|
435 logger.trace("write to cache");
|
|
436 isCachingToFile = true;
|
|
437 OutputStream outFile = new BufferedOutputStream(new FileOutputStream(newFile));
|
|
438 ObjectOutputStream oos = new ObjectOutputStream(outFile);
|
|
439 oos.writeUTF(fullName);
|
|
440 oos.writeObject(lastModified);
|
|
441 oos.writeObject(etag);
|
|
442 oos.writeObject(actions);
|
|
443 oos.flush();
|
|
444 return outFile;
|
|
445 } catch(IOException e) {
|
|
446 throw new RuntimeException(e);
|
|
447 }
|
|
448 }
|
|
449 };
|
|
450 }
|
|
451
|
|
452 void finish(boolean isDone) throws IOException {
|
|
453 try {
|
|
454 log("finish a");
|
|
455 if( fileHandler != null ) {
|
|
456 fileHandler.close();
|
|
457 logger.trace("closed fileHandler");
|
|
458 }
|
|
459 if( outputStream == null || !isDone && !isCachingToFile )
|
|
460 unlock();
|
|
461 if( isDone ) {
|
|
462 if( writer != null )
|
|
463 writer.flush();
|
|
464 if( outputStream != null ) {
|
|
465 try {
|
|
466 outputStream.flush();
|
|
467 } catch(IOException e) {
|
|
468 logger.trace("",e);
|
|
469 isDone = false;
|
|
470 }
|
|
471 }
|
|
472 }
|
|
473 if( isCachingToFile ) {
|
|
474 log("finish b");
|
|
475 try {
|
|
476 outputStream.close();
|
|
477 } catch(IOException e) {
|
|
478 logger.trace("",e);
|
|
479 isDone = false;
|
|
480 }
|
|
481 if( isDone ) {
|
|
482 log("finish c");
|
|
483 try {
|
|
484 log("file size = "+cachingFile.lastFile().length());
|
|
485 fileHandler = FileHandler.factory.newInstance(cachingFile.lastFile());
|
|
486 ois = new ObjectInputStream(fileHandler.getInputStream());
|
|
487 ois.readUTF(); // full name
|
|
488 ois.readObject(); // lastModified
|
|
489 ois.readObject(); // etag
|
|
490 } catch(ClassNotFoundException e) {
|
|
491 throw new RuntimeException(e);
|
|
492 } catch(IOException e) {
|
|
493 cachingFile.deleteNewFile();
|
|
494 throw new RuntimeException(e);
|
|
495 }
|
|
496 CachingResponseWrapper.super.setHeader("Via","cache-write");
|
|
497 try {
|
|
498 log("finish d");
|
|
499 sendFile2();
|
|
500 log("finish e");
|
|
501 } catch(IOException e) {
|
|
502 unlock();
|
|
503 throw e;
|
|
504 }
|
|
505 fileHandler.close();
|
|
506 } else {
|
|
507 cachingFile.deleteNewFile();
|
|
508 unlock();
|
|
509 }
|
|
510 }
|
|
511 log("finish z");
|
|
512 } catch(RuntimeException e) {
|
|
513 log("finish RuntimeException");
|
|
514 logger.error("RuntimeException in finish()",e);
|
|
515 throw e;
|
|
516 } catch(Error e) {
|
|
517 log("finish Error");
|
|
518 logger.error("Error in finish()",e);
|
|
519 throw e;
|
|
520 } finally {
|
|
521 if( unlock() ) {
|
|
522 logger.error("still locked isDone="+isDone+" isCachingToFile="+isCachingToFile+" outputStream="+(outputStream!=null));
|
|
523 logger.error("log:\n"+log);
|
|
524 }
|
|
525 }
|
|
526 }
|
|
527
|
|
528 }
|