--- /dev/null
+// CacheFilter.java\r
+// $Id: CacheFilter.java,v 1.2 2010/06/15 17:52:54 smhuang Exp $\r
+// (c) COPYRIGHT MIT and INRIA, 1996.\r
+// Please first read the full copyright statement in file COPYRIGHT.html\r
+\r
+package org.w3c.jigsaw.filters ;\r
+\r
+import java.util.Dictionary;\r
+import java.util.Hashtable;\r
+\r
+import java.io.ByteArrayInputStream;\r
+import java.io.ByteArrayOutputStream;\r
+import java.io.IOException;\r
+import java.io.InputStream;\r
+import java.io.PrintStream;\r
+\r
+import java.net.URL ;\r
+\r
+import org.w3c.tools.resources.Attribute;\r
+import org.w3c.tools.resources.AttributeRegistry;\r
+import org.w3c.tools.resources.FilterInterface;\r
+import org.w3c.tools.resources.IntegerAttribute;\r
+import org.w3c.tools.resources.ProtocolException;\r
+import org.w3c.tools.resources.ReplyInterface;\r
+import org.w3c.tools.resources.RequestInterface;\r
+import org.w3c.tools.resources.Resource;\r
+import org.w3c.tools.resources.ResourceFilter;\r
+\r
+import org.w3c.tools.resources.ProtocolException;\r
+\r
+import org.w3c.www.http.HTTP;\r
+import org.w3c.www.http.HttpEntityMessage;\r
+import org.w3c.www.http.HttpEntityTag;\r
+import org.w3c.www.http.HttpMessage;\r
+import org.w3c.www.http.HttpReplyMessage;\r
+import org.w3c.www.http.HttpRequestMessage;\r
+\r
+import org.w3c.util.AsyncLRUList;\r
+import org.w3c.util.LRUList;\r
+import org.w3c.util.LRUNode;\r
+\r
+import org.w3c.jigsaw.http.Reply;\r
+import org.w3c.jigsaw.http.Request;\r
+\r
+class CacheException extends Exception {\r
+ public CacheException(String msg) { super(msg) ; }\r
+}\r
+\r
+class CacheEntry extends LRUNode {\r
+ /** The normalized url. (Ready to be used as key) */\r
+ private String url ;\r
+\r
+ /** The actual cached content */\r
+ byte[] content ;\r
+\r
+ /** The model reply */\r
+ Reply reply ;\r
+\r
+ /** The maximum allowed age */\r
+ private int maxage ;\r
+\r
+ public String toString()\r
+ {\r
+ return "[\"" + url + "\" " + maxage + "]" ;\r
+ }\r
+\r
+ public final String getURL()\r
+ {\r
+ return url ;\r
+ }\r
+\r
+ public final int getSize()\r
+ {\r
+ return content.length ;\r
+ }\r
+\r
+ private void readContent(Reply reply)\r
+ throws IOException\r
+ {\r
+ ByteArrayOutputStream out = null ;\r
+ InputStream in = reply.openStream() ;\r
+\r
+ if(reply.hasContentLength()) \r
+ out = new ByteArrayOutputStream(reply.getContentLength()) ;\r
+ else\r
+ out = new ByteArrayOutputStream(8192) ;\r
+\r
+ byte[] buf = new byte[4096] ;\r
+ int len = 0 ;\r
+ while( (len = in.read(buf)) != -1)\r
+ out.write(buf,0,len) ;\r
+\r
+ in.close() ;\r
+ out.close() ;\r
+\r
+ content = out.toByteArray() ;\r
+\r
+ reply.setStream(new ByteArrayInputStream(content)) ;\r
+ }\r
+\r
+ /**\r
+ * Construct a CacheEntry from the given reply,\r
+ * maybe using the given default max age\r
+ */\r
+ CacheEntry(Request request, Reply reply, int defMaxAge)\r
+ throws CacheException \r
+ {\r
+ url = Cache.getNormalizedURL(request) ;\r
+ \r
+ try {\r
+ readContent(reply) ;\r
+ } catch(IOException ex) {\r
+ throw new CacheException("cannot read reply content") ;\r
+ }\r
+\r
+ this.reply = (Reply) reply.getClone() ;\r
+ this.reply.setStream((InputStream) null) ;\r
+\r
+ // Set the date artificially, since Jigsaw only sets the date\r
+ // header on ultimate emission of the reply.\r
+ long date = this.reply.getDate() ;\r
+ if(date == -1) {\r
+ date = System.currentTimeMillis() ;\r
+ date -= date % 1000 ;\r
+ this.reply.setDate(date) ;\r
+ }\r
+\r
+ setMaxAge(reply,defMaxAge) ;\r
+\r
+ }\r
+\r
+ /**\r
+ * Sets this entry's maxage from available data, or\r
+ * falls back to the specified default.\r
+ */\r
+ private void setMaxAge(Reply reply, int def)\r
+ {\r
+ if( ( maxage = reply.getMaxAge() ) == -1 ) {\r
+ long exp = reply.getExpires() ;\r
+ long date = reply.getDate() ;\r
+ \r
+ if(exp != -1 && date != -1) {\r
+ \r
+ maxage = (int) (reply.getExpires() - reply.getDate()) ;\r
+ if(maxage<0) maxage = 0 ; \r
+ \r
+ } else {\r
+ maxage = def ;\r
+ }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * Sets this entry's maxage from available data, or\r
+ * leaves it unchanged if reply doesn't say anything.\r
+ */\r
+ private void setMaxAge(Reply reply)\r
+ {\r
+ setMaxAge(reply,maxage) ;\r
+ }\r
+\r
+ /**\r
+ * Make a reply for this entry.\r
+ */\r
+ Reply getReply(Request request)\r
+ {\r
+ Reply newReply = (Reply) reply.getClone() ;\r
+\r
+ int age = getAge() ; \r
+ if(age!=-1) newReply.setAge(age) ;\r
+\r
+ boolean notMod = false ;\r
+\r
+ HttpEntityTag[] etags = request.getIfNoneMatch() ;\r
+ HttpEntityTag tag = this.reply.getETag() ;\r
+ if(etags != null && tag != null) {\r
+ boolean noneMatch = true ;\r
+ String sTag = tag.getTag() ;\r
+ for(int i=0;i<etags.length;i++) {\r
+ if(sTag.equals(etags[i].getTag())) {\r
+ noneMatch = false ;\r
+ break ;\r
+ }\r
+ }\r
+ notMod = !noneMatch ;\r
+ } else {\r
+ long ims = request.getIfModifiedSince() ;\r
+ long lmd = this.reply.getLastModified() ;\r
+ if(ims != -1 && lmd != -1) \r
+ notMod = lmd > ims ;\r
+ }\r
+\r
+ if(notMod) {\r
+ System.out.println("**** replying NOT_MODIFIED") ;\r
+ newReply.setStatus(HTTP.NOT_MODIFIED) ;\r
+ }\r
+ else if(! request.getMethod().equals("HEAD"))\r
+ newReply.setStream(new ByteArrayInputStream(content)) ;\r
+ else\r
+ newReply.setStream((InputStream) null) ;\r
+ return newReply ; \r
+ }\r
+\r
+ /**\r
+ * Returns the age of this entry in seconds,\r
+ * or -1 if age cannot be determined.\r
+ */\r
+ int getAge()\r
+ {\r
+ int age1 = reply.getAge() ;\r
+ \r
+ long age2 = -1 ;\r
+ long date = reply.getDate() ;\r
+ if(date != -1 ) {\r
+ age2 = System.currentTimeMillis() ;\r
+ age2 -= date ;\r
+ age2 /= 1000 ;\r
+ }\r
+ \r
+ return age1>=age2 ? age1 : (int) age2 ;\r
+ } \r
+\r
+ /**\r
+ * Make a reply for this entry, which was validated\r
+ * by the server with the given reply.\r
+ */\r
+ final Reply getReply(Request request, Reply servReply)\r
+ {\r
+ System.out.println("**** Validated entry") ;\r
+ setMaxAge(servReply) ;\r
+ long date = servReply.getDate() ;\r
+ if(date == -1) {\r
+ date = System.currentTimeMillis() ;\r
+ date -= date % 1000 ;\r
+ }\r
+ this.reply.setDate(date) ;\r
+ return getReply(request) ;\r
+ }\r
+\r
+ /** \r
+ * Turn the given request into a conditional request,\r
+ * using the appropriate validators (if any). \r
+ */\r
+ void makeConditional(Request request)\r
+ {\r
+ System.out.println("**** Making conditional request for validation") ;\r
+ HttpEntityTag[] et = { reply.getETag() } ;\r
+ if(et[0] != null) request.setIfNoneMatch(et) ;\r
+ \r
+ long lm = reply.getLastModified() ;\r
+ if(lm != -1) request.setIfModifiedSince(lm) ;\r
+ }\r
+\r
+ /**\r
+ * Is this entry fresh, according to the requirements\r
+ * of the request?\r
+ */\r
+ boolean isFresh(Request request)\r
+ {\r
+ int age = getAge() ;\r
+ System.out.println("**** age: "+age+" maxage: "+maxage) ;\r
+ return age != -1 ? (maxage > age) : (maxage > 0) ;\r
+ }\r
+}\r
+\r
+class Cache {\r
+\r
+ private static final String STATE_NORM_URL =\r
+ "org.w3c.jigsaw.filters.Cache.normURL" ;\r
+\r
+ /** Our maximum size in bytes */\r
+ private int maxSize ;\r
+ /** Our maximum size in entries */\r
+ private int maxEntries ;\r
+\r
+ /** Current size in bytes */\r
+ private int size ;\r
+\r
+ /** The default max age */\r
+ private int defaultMaxAge ;\r
+\r
+ /**\r
+ * This maps URLs (maybe processed) vs entries\r
+ */\r
+ Dictionary /*<String,CacheEntry>*/ entries ;\r
+\r
+ /**\r
+ * This keeps track of LRU entries\r
+ */\r
+ LRUList /*<CacheEntry>*/ lruList ;\r
+\r
+ public Cache(int maxSize, int maxEntries, int defaultMaxAge)\r
+ {\r
+ this.maxSize = maxSize ;\r
+ this.maxEntries = maxEntries ;\r
+ this.defaultMaxAge = defaultMaxAge ;\r
+\r
+ this.size = 0 ;\r
+\r
+ lruList = new AsyncLRUList() ;\r
+ entries = new Hashtable(20) ;\r
+ }\r
+\r
+ /**\r
+ * Stores a new reply in a CacheEntry.\r
+ * Takes care of handling the LRU list, and of possible overwriting\r
+ * @exception CacheException fixme doc\r
+ */\r
+ public void store(Request request, Reply reply)\r
+ throws CacheException\r
+ {\r
+ System.out.println("**** Storing reply in cache") ;\r
+ // Enforce maxEntries\r
+ if(maxEntries > 0 && entries.size() == maxEntries)\r
+ flushLRU() ;\r
+\r
+ // Try to enforce maxSize\r
+ if(maxSize > 0 && reply.hasContentLength()) {\r
+ int maxEntSize = maxSize - reply.getContentLength() ;\r
+ while(entries.size() > maxEntSize)\r
+ if(!flushLRU()) break ;\r
+ }\r
+\r
+ CacheEntry ce = new CacheEntry(request, reply, defaultMaxAge) ;\r
+\r
+ synchronized(this) {\r
+ size += ce.getSize() ;\r
+ CacheEntry old = (CacheEntry) entries.put(ce.getURL(),ce) ;\r
+ if(old!=null) lruList.remove(old) ;\r
+ lruList.toHead(ce) ;\r
+ }\r
+ }\r
+ \r
+\r
+ /**\r
+ * Retrieves a CacheEntry corresponding to the request.\r
+ * Should mark it as MRU\r
+ */\r
+ public CacheEntry retrieve(Request request)\r
+ {\r
+ String url = getNormalizedURL(request) ;\r
+ CacheEntry ce = (CacheEntry) entries.get(url) ;\r
+ return ce==null ? null : ce ;\r
+ }\r
+\r
+ /**\r
+ * Removes the CacheEntry corresponding to the request.\r
+ */\r
+ public synchronized void remove(Request request)\r
+ {\r
+ System.out.println("**** Removing from cache") ;\r
+ CacheEntry ce = (CacheEntry)\r
+ entries.remove(getNormalizedURL(request)) ;\r
+ if(ce == null) return ;\r
+ \r
+ lruList.remove(ce) ;\r
+ }\r
+\r
+ /**\r
+ * Gets rid of the LRU element\r
+ */\r
+ private synchronized final boolean flushLRU()\r
+ {\r
+ if(entries.size() == 0) return false ;\r
+\r
+ CacheEntry ce = (CacheEntry) lruList.removeTail() ;\r
+ entries.remove(ce.getURL()) ;\r
+ size -= ce.getSize() ;\r
+\r
+ return true ;\r
+ }\r
+\r
+ /** This might be unnecessary */\r
+ static String getNormalizedURL(Request request) {\r
+ String nurl = (String) request.getState(STATE_NORM_URL) ;\r
+ if(nurl!=null) return nurl ;\r
+\r
+ URL url = request.getURL() ;\r
+ nurl = url.getFile() ;\r
+ \r
+ request.setState(STATE_NORM_URL,nurl) ;\r
+ return nurl ;\r
+ }\r
+}\r
+\r
+public class CacheFilter extends ResourceFilter {\r
+ protected Cache cache = null ;\r
+\r
+ protected final static String STATE_TAG\r
+ = "org.w3c.jigsaw.filters.CacheFilter.tag" ;\r
+\r
+ protected static int ATTR_MAX_SIZE = -1 ;\r
+ protected static int ATTR_MAX_ENTRIES = -1 ;\r
+ protected static int ATTR_DEFAULT_MAX_AGE = -1 ;\r
+\r
+ static {\r
+ Attribute a = null;\r
+ Class cls = null;\r
+ \r
+ try {\r
+ cls = Class.forName("org.w3c.jigsaw.filters.CacheFilter");\r
+ //Added by Jeff Huang\r
+ //TODO: FIXIT\r
+ } catch (Exception ex) {\r
+ ex.printStackTrace();\r
+ System.exit(1);\r
+ }\r
+ // Declare the maximum cache size attribute:\r
+ a = new IntegerAttribute("maxSize"\r
+ , new Integer(8192)\r
+ , Attribute.EDITABLE);\r
+ ATTR_MAX_SIZE= AttributeRegistry.registerAttribute(cls, a);\r
+ // Declare the maximum number of entries attribute:\r
+ a = new IntegerAttribute("maxEntries"\r
+ , new Integer(-1)\r
+ , Attribute.EDITABLE);\r
+ ATTR_MAX_ENTRIES = AttributeRegistry.registerAttribute(cls, a);\r
+ // Declare the default maxage attribute\r
+ a = new IntegerAttribute("defaultMaxAge"\r
+ , new Integer(300) // 5min\r
+ , Attribute.EDITABLE);\r
+ ATTR_DEFAULT_MAX_AGE = AttributeRegistry.registerAttribute(cls, a);\r
+ }\r
+\r
+ public int getMaxSize()\r
+ {\r
+ return ((Integer) getValue(ATTR_MAX_SIZE, \r
+ new Integer(-1))).intValue() ;\r
+ }\r
+\r
+ public int getMaxEntries()\r
+ {\r
+ return ((Integer) getValue(ATTR_MAX_ENTRIES, \r
+ new Integer(-1))).intValue() ;\r
+ }\r
+\r
+ public int getDefaultMaxAge() {\r
+ return ((Integer) getValue(ATTR_DEFAULT_MAX_AGE, new Integer(300)))\r
+ .intValue() ;\r
+ }\r
+\r
+ private final void tag(Request request)\r
+ {\r
+ request.setState(STATE_TAG,Boolean.TRUE) ;\r
+ }\r
+\r
+ private final boolean isTagged(Request request)\r
+ {\r
+ return request.hasState(STATE_TAG) ;\r
+ }\r
+\r
+ private Reply applyIn(Request request,\r
+ FilterInterface[] filters,\r
+ int fidx)\r
+ throws ProtocolException\r
+ {\r
+ // Apply remaining ingoing filters\r
+ Reply fr = null ;\r
+ for(int i = fidx+1 ;\r
+ i<filters.length && filters[i] != null ;\r
+ ++i) {\r
+ fr = (Reply) (filters[i].ingoingFilter(request, filters, i)) ;\r
+ if(fr != null) \r
+ return fr ;\r
+ }\r
+ return null ;\r
+ }\r
+\r
+ private Reply applyOut(Request request,\r
+ Reply reply,\r
+ FilterInterface[] filters,\r
+ int fidx)\r
+ throws ProtocolException\r
+ {\r
+ Reply fr = null ;\r
+ for(int i=fidx-1;\r
+ i>=0 && filters[i] != null;\r
+ i--) {\r
+ fr = (Reply) (filters[i].outgoingFilter(request,reply,filters,i)) ;\r
+ if(fr != null)\r
+ return fr ;\r
+ }\r
+ return null ;\r
+ }\r
+\r
+ private final Reply applyOut(Request request,\r
+ Reply reply,\r
+ FilterInterface[] filters )\r
+ throws ProtocolException\r
+ {\r
+ return applyOut(request,reply,filters,filters.length) ;\r
+ }\r
+\r
+ private void makeInconditional(Request request) {\r
+ request.setHeaderValue(request.H_IF_MATCH, null) ;\r
+ request.setHeaderValue(request.H_IF_MODIFIED_SINCE, null) ;\r
+ request.setHeaderValue(request.H_IF_NONE_MATCH, null) ;\r
+ request.setHeaderValue(request.H_IF_RANGE, null) ;\r
+ request.setHeaderValue(request.H_IF_UNMODIFIED_SINCE, null) ;\r
+ }\r
+ \r
+ /**\r
+ * @return A Reply instance, if the filter did know how to answer\r
+ * the request without further processing, <strong>null</strong> \r
+ * otherwise. \r
+ * @exception ProtocolException \r
+ * If processing should be interrupted,\r
+ * because an abnormal situation occured. \r
+ */ \r
+ public ReplyInterface ingoingFilter(RequestInterface req,\r
+ FilterInterface[] filters,\r
+ int fidx)\r
+ throws ProtocolException\r
+ {\r
+ Request request = (Request) req;\r
+ if(cache == null)\r
+ cache = new Cache(getMaxSize(),\r
+ getMaxEntries(),\r
+ getDefaultMaxAge()) ;\r
+\r
+ String method = request.getMethod() ;\r
+ if(! ( method.equals("HEAD") ||\r
+ method.equals("GET") ) )\r
+ return null ; // Enforce write-through\r
+\r
+ tag(request) ;\r
+\r
+ if(isCachable(request)) {\r
+ CacheEntry cachEnt = cache.retrieve(request) ;\r
+ if(cachEnt != null) {\r
+ System.out.println("**** Examining entry: "+cachEnt) ;\r
+ Reply fRep = null ;\r
+ if( cachEnt.isFresh(request) ) {\r
+ \r
+ fRep = applyIn(request,filters,fidx) ;\r
+ if(fRep != null) return fRep ;\r
+\r
+ // Get the reply (adjusting age too) [?]\r
+ Reply reply = cachEnt.getReply(request) ;\r
+\r
+ fRep = applyOut(request,reply,filters) ;\r
+ if(fRep != null) return fRep ;\r
+ \r
+ System.out.println("**** Replying from cache") ;\r
+ return reply ;\r
+ \r
+ } else {\r
+ cachEnt.makeConditional(request) ;\r
+ return null ;\r
+ }\r
+ } else {\r
+ System.out.println("**** Not in cache") ;\r
+ }\r
+ } else {\r
+ System.out.println("**** Request not cachable") ;\r
+ }\r
+\r
+ makeInconditional(request) ;\r
+ \r
+ return null ;\r
+ }\r
+\r
+ /**\r
+ * @param request The original request.\r
+ * @param reply It's original reply. \r
+ * @return A Reply instance, or <strong>null</strong> if processing \r
+ * should continue normally. \r
+ * @exception ProtocolException If processing should be interrupted\r
+ * because an abnormal situation occured. \r
+ */\r
+ public ReplyInterface outgoingFilter(RequestInterface req,\r
+ ReplyInterface rep,\r
+ FilterInterface[] filters,\r
+ int fidx)\r
+ throws ProtocolException \r
+ {\r
+ Request request = (Request) req;\r
+ Reply reply = (Reply) rep;\r
+ // Be transparent if request is not "ours"\r
+ if(!isTagged(request))\r
+ return null ;\r
+ \r
+ if(isCachable(reply)) {\r
+ switch(reply.getStatus()) {\r
+ case HTTP.OK:\r
+ case HTTP.NO_CONTENT:\r
+ case HTTP.MULTIPLE_CHOICE:\r
+ case HTTP.MOVED_PERMANENTLY:\r
+ // Store the reply and let it through\r
+ try {\r
+ cache.store(request,reply) ;\r
+ } catch(CacheException ex) {\r
+ // not much to do...\r
+ } finally {\r
+ return null ;\r
+ }\r
+ case HTTP.NOT_MODIFIED:\r
+ // This means we're validating\r
+ CacheEntry cachEnt = cache.retrieve(request) ;\r
+ if(cachEnt != null)\r
+ reply = cachEnt.getReply(request,reply) ;\r
+ break ;\r
+ default:\r
+ cache.remove(request) ; \r
+ return null ;\r
+ }\r
+ } else {\r
+ System.out.println("**** Reply not cachable") ;\r
+ cache.remove(request) ;\r
+ }\r
+ \r
+ // Apply remaining filters and return modified reply\r
+ Reply fRep = applyOut(request,reply,filters,fidx) ;\r
+\r
+ if(fRep != null) return fRep ;\r
+ else return reply ;\r
+ }\r
+\r
+ /**\r
+ * Does this request permit caching?\r
+ * (It's still half-baked)\r
+ */\r
+ private boolean isCachable(Request request)\r
+ {\r
+ if(request.checkNoStore()) return false ;\r
+\r
+ String[] nc = request.getNoCache() ;\r
+ if(nc != null) return false ; // for now\r
+ \r
+ return true ;\r
+ }\r
+\r
+ /**\r
+ * Does this reply permit caching?\r
+ * (It's still half-baked)\r
+ */\r
+ private boolean isCachable(Reply reply)\r
+ {\r
+ if(reply.checkNoStore() ||\r
+ reply.getPrivate() != null) return false ;\r
+ \r
+ return true ;\r
+ \r
+ }\r
+\r
+}\r
+\r
+\r