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 }