Mercurial Hosting > nabble
comparison src/cachingfilter/CachingResponseWrapper.java @ 0:7ecd1a4ef557
add content
author | Franklin Schmidt <fschmidt@gmail.com> |
---|---|
date | Thu, 21 Mar 2019 19:15:52 -0600 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:7ecd1a4ef557 |
---|---|
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 } |