Mercurial Hosting > nabble
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/src/cachingfilter/CachingResponseWrapper.java Thu Mar 21 19:15:52 2019 -0600 @@ -0,0 +1,528 @@ +package cachingfilter; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.ObjectOutputStream; +import java.io.ObjectInputStream; +import java.net.URLEncoder; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +import java.util.List; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.Collection; +import javax.servlet.ServletException; +import javax.servlet.ServletOutputStream; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +final class CachingResponseWrapper extends HttpServletResponseWrapper { + private static final Logger logger = LoggerFactory.getLogger(CachingResponseWrapper.class); + + private static Set<String> cacheableHeaders = new HashSet<String>(); + static { + for( String header : new String[]{ + "Last-Modified", + "Etag", + "Content-Type", + "Cache-Control", + "Content-Encoding", + } ) { + cacheableHeaders.add( header.toLowerCase() ); + } + } + + private final CachingFilter cachingFilter; + private final CachingRequestWrapper request; + private final String fullName; + private boolean isCachingToFile; + private CachedPage cachingFile; + private ServletOutputStream outputStream; + private PrintWriter writer; + private int status = SC_OK; + private Long lastModified = null; + private String etag = null; + private FileHandler fileHandler = null; + private ObjectInputStream ois = null; + private final List<ResponseAction> actions = new ArrayList<ResponseAction>(); + private boolean isLocked = false; // for debugging + private StringBuilder log = new StringBuilder(); + + private void log(String msg) { + log.append(msg).append('\n'); + } + + CachingResponseWrapper( CachingFilter cachingFilter, CachingRequestWrapper request, HttpServletResponse response ) { + super(response); + try { + this.cachingFilter = cachingFilter; + this.request = request; + this.fullName = getFullName(); + boolean ok = false; + try { + if( !openObjectInputStream() ) { // calls lock() + ok = true; + return; + } + Long cachedLastModified = (Long)ois.readObject(); + if( cachedLastModified==null ) + cachedLastModified = -1L; + request.headerMap.put( CachingRequestWrapper.IF_MODIFIED_SINCE, cachedLastModified ); + String cachedEtag = (String)ois.readObject(); + request.headerMap.put( CachingRequestWrapper.IF_NONE_MATCH, cachedEtag ); + ok = true; + } finally { + if( !ok && cachingFile != null ) + unlock(); + } + } catch(ClassNotFoundException e) { + throw new RuntimeException(e); + } catch(IOException e) { + throw new RuntimeException(e); + } + } + + private String getFullName() + throws IOException + { + StringBuffer url = request.getRequestURL(); + String queryString = request.getQueryString(); + if( queryString != null ) { + url.append( '?' ); + url.append( queryString ); + } + log("url = "+url); + String fullUrl = URLEncoder.encode(url.toString(),"UTF-8"); + String acceptEncoding = request.getHeader("Accept-Encoding"); + logger.trace("acceptEncoding = "+acceptEncoding); + if( acceptEncoding == null ) + return fullUrl; + Set<String> knownEncodings = cachingFilter.getEncodings(); + List<String> list = new ArrayList<String>(); + for( String encoding : acceptEncoding.split(",") ) { + encoding = encoding.trim(); + if( knownEncodings.contains(encoding) ) + list.add(encoding); + } + if( list.isEmpty() ) { + request.headerMap.put( CachingRequestWrapper.ACCEPT_ENCODING, null ); + return fullUrl; + } + String encodings = join( list, "," ); + request.headerMap.put( CachingRequestWrapper.ACCEPT_ENCODING, encodings ); + return fullUrl + '~' + encodings; + } + + private void lock() { + CachingFilter.locker.lock(cachingFile.name()); + log("lock"); + isLocked = true; + } + + private boolean unlock() { + boolean wasLocked = CachingFilter.locker.unlock(cachingFile.name()); + log("unlock "+wasLocked); + if( isLocked != wasLocked ) + logger.error("isLocked="+isLocked+" wasLocked="+wasLocked,new Exception()); + isLocked = false; + return wasLocked; + } + + private static String join(Collection<?> col,String separator) { + if( col.isEmpty() ) + return ""; + StringBuilder sb = new StringBuilder(); + Iterator<?> iter = col.iterator(); + sb.append( iter.next() ); + while( iter.hasNext() ) { + sb.append( separator ).append( iter.next() ); + } + return sb.toString(); + } + + private static long hashCode(String s) { + final int len = s.length(); + long h = 0; + for( int i = 0; i < len; i++ ) { + h = 31*h + s.charAt(i); + } + return h; + } + + private boolean openObjectInputStream() { + for( long hash = hashCode(fullName); true; hash++ ) { + String s = Long.toHexString(hash); + cachingFile = cachingFilter.newCachedPage(s); + lock(); + if( !cachingFile.exists() ) { + logger.trace("couldn't find "+cachingFile); + return false; + } + try { + FileHandler fileHandler = FileHandler.factory.newInstance(cachingFile.lastFile()); + ObjectInputStream ois = new ObjectInputStream(fileHandler.getInputStream()); + if( ois.readUTF().equals(fullName) ) { + logger.trace("found file = "+cachingFile); + this.fileHandler = fileHandler; + this.ois = ois; + return true; + } + fileHandler.close(); + } catch(IOException e) { + logger.error("couldn't read "+cachingFile+" length="+cachingFile.lastFile().length(),e); + if( !cachingFile.delete() ) + logger.error("couldn't delete "+cachingFile); + return false; + } + unlock(); + } + } + + public void setContentType(String ct) + { + logger.trace("setContentType "+ct); + super.setContentType(ct); + actions.add( new ResponseAction.SetHeader("Content-Type",ct) ); + } + + public void setStatus(int sc, String sm) + { + logger.trace("setStatus2"); + super.setStatus(sc,sm); + this.status = sc; + } + + public void setStatus(int sc) + { + logger.trace("setStatus "+sc); + super.setStatus(sc); + this.status = sc; + } + + public void setHeader(String name, String value) { + logger.trace("setHeader "+name+" = "+value); + super.setHeader(name,value); + if( "Etag".equalsIgnoreCase(name) ) + etag = value; + if( "Last-Modified".equalsIgnoreCase(name) ) { + if( value==null ) + lastModified = null; + else + logger.error("unsupported",new Exception()); + } + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.SetHeader(name,value) ); + } + + public void addHeader(String name, String value) { + logger.trace("addHeader "+name+" = "+value); + super.addHeader(name,value); + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.AddHeader(name,value) ); + } + + public void setIntHeader(String name, int value) { + logger.trace("setIntHeader "+name); + super.setIntHeader(name,value); + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.SetIntHeader(name,value) ); + } + + public void addIntHeader(String name, int value) { + logger.trace("addIntHeader "+name); + super.addIntHeader(name,value); + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.AddIntHeader(name,value) ); + } + + public void setDateHeader(String name, long value) { + logger.trace("setDateHeader "+name); + super.setDateHeader(name,value); + value = value / 1000 * 1000; // round to seconds + if( "Last-Modified".equalsIgnoreCase(name) ) + lastModified = value; + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.SetDateHeader(name,value) ); + } + + public void addDateHeader(String name, long value) { + logger.trace("addDateHeader "+name); + super.setDateHeader(name,value); + if( cacheableHeaders.contains(name.toLowerCase()) ) + actions.add( new ResponseAction.AddDateHeader(name,value) ); + } + + public void reset() + { + logger.trace("reset"); + super.reset(); + resetOutput(); + status = SC_OK; + lastModified = null; + etag = null; + actions.clear(); + } + + public void resetBuffer() + { + logger.trace("resetBuffer"); + super.resetBuffer(); + resetOutput(); + } + + private void resetOutput() { + if( isCachingToFile ) { + try { + outputStream.close(); + } catch(IOException e) { + logger.error("resetOutput",e); + } + cachingFile.deleteNewFile(); + isCachingToFile = false; + } + outputStream = null; + writer = null; + } + + public void sendError(int sc, String msg) throws IOException + { + logger.trace("sendError2"); + this.status = sc; + resetBuffer(); + if( shouldSendFile() ) { + sendFile(); + } else { + super.sendError(sc,msg); + } + } + + public void sendError(int sc) throws IOException + { + logger.trace("sendError"); + this.status = sc; + resetBuffer(); + if( shouldSendFile() ) { + sendFile(); + } else { + super.sendError(sc); + } + } + + public void sendRedirect(String location) throws IOException + { + logger.trace("sendRedirect"); + this.status = SC_MOVED_TEMPORARILY; + resetBuffer(); + super.sendRedirect(location); + } + + public void flushBuffer() throws IOException + { + logger.trace("flushBuffer "+isCommitted()); + if( writer != null ) + writer.flush(); + if( outputStream != null ) + outputStream.flush(); + else if( shouldSendFile() ) + sendFile(); + else + getResponse().flushBuffer(); + } + + private boolean shouldSendFile() { + if( !(status==SC_NOT_MODIFIED && ois!=null) ) + return false; + if( request.isCacheable() ) + return false; // no need + return true; + } + + private void sendFile() throws IOException { + CachingResponseWrapper.super.setHeader("Via","cache-yes"); + sendFile2(); + } + + private void sendFile2() throws IOException { + logger.trace("sendFile"); + unlock(); + setStatus(SC_OK); + HttpServletResponse response = (HttpServletResponse)getResponse(); + try { + @SuppressWarnings("unchecked") + List<ResponseAction> cachedActions = (List<ResponseAction>)ois.readObject(); + for( ResponseAction cachedAction : cachedActions ) { + cachedAction.apply(response); + } + } catch(ClassNotFoundException e) { + throw new RuntimeException(e); + } + ServletOutputStream out = response.getOutputStream(); + fileHandler.writeTo(out); + } + + public ServletOutputStream getOutputStream() + { + logger.trace("getOutputStream"); + if (outputStream==null) { + newOutputStream(); + } else if (writer!=null) + throw new IllegalStateException("getWriter() called"); + + return outputStream; + } + + public PrintWriter getWriter() throws IOException + { + logger.trace("getWriter"); + if (writer==null) + { + if (outputStream!=null) + throw new IllegalStateException("getOutputStream() called"); + + newOutputStream(); + String encoding = getCharacterEncoding(); + writer = encoding==null ? new PrintWriter(outputStream) + : new PrintWriter(new OutputStreamWriter(outputStream,encoding)); + } + return writer; + } + + private boolean isCacheable() { + if( getResponse().isCommitted() ) { + logger.trace("!isCacheable - isCommitted"); + return false; + } + if( status != SC_OK ) { + logger.trace("!isCacheable - status="+status); + return false; + } + if( lastModified==null && etag==null ) { + logger.trace("!isCacheable - no lastModified,etag"); + return false; + } + return true; + } + + private void newOutputStream() { + outputStream = new ProxyServletOutputStream() { + protected OutputStream newOutputStream() + throws IOException + { + CachingResponseWrapper.super.setHeader("Via","cache-no"); + ServletOutputStream out = getResponse().getOutputStream(); + if( !isCacheable() ) { + unlock(); + logger.trace("return getResponse().getOutputStream() "+out.getClass()); + return out; + } + try { + File newFile = cachingFile.newFile(); + logger.trace("write to cache"); + isCachingToFile = true; + OutputStream outFile = new BufferedOutputStream(new FileOutputStream(newFile)); + ObjectOutputStream oos = new ObjectOutputStream(outFile); + oos.writeUTF(fullName); + oos.writeObject(lastModified); + oos.writeObject(etag); + oos.writeObject(actions); + oos.flush(); + return outFile; + } catch(IOException e) { + throw new RuntimeException(e); + } + } + }; + } + + void finish(boolean isDone) throws IOException { + try { + log("finish a"); + if( fileHandler != null ) { + fileHandler.close(); + logger.trace("closed fileHandler"); + } + if( outputStream == null || !isDone && !isCachingToFile ) + unlock(); + if( isDone ) { + if( writer != null ) + writer.flush(); + if( outputStream != null ) { + try { + outputStream.flush(); + } catch(IOException e) { + logger.trace("",e); + isDone = false; + } + } + } + if( isCachingToFile ) { + log("finish b"); + try { + outputStream.close(); + } catch(IOException e) { + logger.trace("",e); + isDone = false; + } + if( isDone ) { + log("finish c"); + try { + log("file size = "+cachingFile.lastFile().length()); + fileHandler = FileHandler.factory.newInstance(cachingFile.lastFile()); + ois = new ObjectInputStream(fileHandler.getInputStream()); + ois.readUTF(); // full name + ois.readObject(); // lastModified + ois.readObject(); // etag + } catch(ClassNotFoundException e) { + throw new RuntimeException(e); + } catch(IOException e) { + cachingFile.deleteNewFile(); + throw new RuntimeException(e); + } + CachingResponseWrapper.super.setHeader("Via","cache-write"); + try { + log("finish d"); + sendFile2(); + log("finish e"); + } catch(IOException e) { + unlock(); + throw e; + } + fileHandler.close(); + } else { + cachingFile.deleteNewFile(); + unlock(); + } + } + log("finish z"); + } catch(RuntimeException e) { + log("finish RuntimeException"); + logger.error("RuntimeException in finish()",e); + throw e; + } catch(Error e) { + log("finish Error"); + logger.error("Error in finish()",e); + throw e; + } finally { + if( unlock() ) { + logger.error("still locked isDone="+isDone+" isCachingToFile="+isCachingToFile+" outputStream="+(outputStream!=null)); + logger.error("log:\n"+log); + } + } + } + +}