Package org.xlightweb.client

Source Code of org.xlightweb.client.HttpCache

/*
*  Copyright (c) xlightweb.org, 2008 - 2009. All rights reserved.
*
*  This library is free software; you can redistribute it and/or
*  modify it under the terms of the GNU Lesser General Public
*  License as published by the Free Software Foundation; either
*  version 2.1 of the License, or (at your option) any later version.
*
*  This library is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
*  Lesser General Public License for more details.
*
*  You should have received a copy of the GNU Lesser General Public
*  License along with this library; if not, write to the Free Software
*  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
*
* Please refer to the LGPL license at: http://www.gnu.org/copyleft/lesser.txt
* The latest copy of this software may be found on http://www.xlightweb.org/
*/
package org.xlightweb.client;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TimerTask;
import java.util.logging.Level;
import java.util.logging.Logger;


import org.xlightweb.HttpUtils;
import org.xlightweb.IHttpRequest;
import org.xlightweb.IHttpResponse;
import org.xlightweb.IHttpResponseHandler;
import org.xlightweb.IHttpResponseHeader;
import org.xlightweb.InvokeOn;
import org.xsocket.Execution;




/**
* cache
* @author grro@xlightweb.org
*/
final class HttpCache implements IHttpCache {
   
    private static final Logger LOG = Logger.getLogger(HttpCache.class.getName());

    private static final String SEPARATOR = "*";

    private static final int DEFAULT_VALIDATION_BASED_CACHE_ENTRY_MAX_TIME_MILLIS = 1 * 60 * 1000;
    private int validationBasedCacheMaxTimeMillis = DEFAULT_VALIDATION_BASED_CACHE_ENTRY_MAX_TIME_MILLIS;

    private static final long TIMERTASK_PERIOD = 60 * 1000;
    private final TimerTask timerTask;


    private final HttpClient httpClient;
   
    private boolean isSharedCache = true;

    private final EntryCache entries = new EntryCache();
   
    
    public HttpCache(HttpClient httpClient) {
        this.httpClient = httpClient;
       
        timerTask = new TimerTask() {
            public void run() {
                entries.checkTimeouts();
            }
        };
               
        HttpClientConnection.schedule(timerTask, TIMERTASK_PERIOD, TIMERTASK_PERIOD);           
    }
   
   
    public void setSharedCache(boolean isSharedCache) {
        this.isSharedCache = isSharedCache;
        entries.clear();
    }
   
    public boolean isSharedCache() {
        return isSharedCache;
    }
     
    public void setMaxSize(int sizeBytes) {
        entries.setMaxSize(sizeBytes);
    }
   
    public int getMaxSize() {
        return entries.getMaxSize();
    }
   
   
    public int getMaxSizeCacheEntry() {
        return entries.getMaxSizeCacheEntry();
    }

    public int getCurrentSize() {
        return entries.getCurrentSize();
    }
   
    public Collection<CacheEntry> getEntries() {
        return entries.getEntries();
    }
   
    public void close() throws IOException {
        entries.close();
        timerTask.cancel();
    }
   
   
    public static boolean isCacheable(IHttpRequest request) {
       
        try {
            // If the request contains an "Authorization:" header, the response will not be cached
            if (request.getHeader("Authorization") != null) {
                return false;
            }
           
            // only GET or POST is supported
            if (!request.getMethod().equalsIgnoreCase("GET") && !request.getMethod().equalsIgnoreCase("POST")) {
               return false;
            }
   
            // do not handle validation based cache request (ETag, Modification-Date) -> could be done in the future
            if ((request.getHeader("If-None-Match") != null) || (request.getHeader("If-Modified-Since") != null)) {
                return false;        
            }
           
            return true;
           
        } catch (Exception e) {
            return false;
        }
    }
   
    public static boolean isCacheable(IHttpResponse response, boolean isSharedCache) {
       
        try {
            // cache only specific response status
            if (!isCacheableSuccess(response.getStatus()) && !isCacheableRedirect(response.getStatus())) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("non-cacheable response received (status: " + response.getStatus() + ")");
                }
   
                return false;
            }
   
           
            // do not cache pragma header 'no-cache'
            String pragmaHeader = response.getHeader("Pragma");
            if ((pragmaHeader != null) && (pragmaHeader.equalsIgnoreCase("no-cache"))) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("non-cacheable response received (Pragma: no-cache)");
                }
   
                return false;
            }
   
           
           
            // check expired based           
            String expires = response.getHeader("Expires");
            if ((expires != null) && (HttpUtils.parseHttpDateString(expires).getTime() < System.currentTimeMillis())) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("non-cacheable response received (Expires: " + expires + ")");
                }
   
                return false;
            }
           
           
            // check cache control header           
            String cacheControl = response.getHeader("Cache-Control");
            if (cacheControl != null) {
                for (String directive : cacheControl.split(",")) {
                    directive = directive.trim();
                    if (directive.equalsIgnoreCase("no-cache") || directive.equalsIgnoreCase("no-store")) {
                        if (LOG.isLoggable(Level.FINE)) {
                            LOG.fine("non-cacheable response received (Cache-Control: " + cacheControl + ")");
                        }
   
                        return false;
                    }
                   
                    if (isSharedCache && directive.equalsIgnoreCase("private")) {
                        return false;
                    }
                }
            }
           
           
            // do not cache response contains vary
            if (response.getHeader("Vary") != null) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("non-cacheable response received (includes Vary header)");
                }
   
                return false;
            }
           
           
            return true;
           
        } catch (Exception e) {
            return false;
        }
    }
   
   
   
    private static boolean isCacheableSuccess(int statusCode) {
        return ((statusCode >= 200) && (statusCode <= 201));
    }
   
    private static boolean isCacheableRedirect(int statusCode) {
        return ((statusCode >= 301) && (statusCode <= 302));
    }

   
     
    public CacheEntry get(IHttpRequest request, Date minFresh) throws IOException {
        CacheEntry ce = null;
       
        synchronized (this) {
            ce = entries.getEntry(request);
        }
       
        if (ce != null) {
            if (ce.isExpired(minFresh)) {
                return null;
               
            } else {               
                return ce;
            }
        } else {
            return null;
        }
           
    }

       
   
     
    private CacheEntry newCacheEntry(IHttpRequest request, long networkLatency, IHttpResponse response) {
        try {
           
           
            ///////////////////////////////
            // CHECK EXPIRED BASED
            String cacheControl = response.getHeader("Cache-Control");
           
            // handle cache-control header
            if (cacheControl != null) {
                CacheEntry cacheEntry = new CacheControlCacheEntry(request, response, networkLatency, cacheControl, isSharedCache);
                if (cacheEntry.isExpired(new Date())) {
                    return null;
                } else {
                    return cacheEntry;
                }
            }

           
            // handler expires header
            String expire = response.getHeader("Expires");
            if (expire != null) {
                CacheEntry cacheEntry = new ExpiresCacheEntry(request, response, networkLatency, expire, isSharedCache);
                if (cacheEntry.isExpired(new Date())) {
                    return null;
                } else {
                    return cacheEntry;
                }
            }

          
           
            if (request.getMethod().equalsIgnoreCase("GET"))  {
                   
                ///////////////////////////////
                // CHECK VALIDATION BASED
                String eTag = response.getHeader("ETag");
                String lastModified = response.getHeader("Last-Modified");
                if ((eTag != null) || (lastModified != null)) {
                    CacheEntry cacheEntry = new CacheEntry(request, response);
                    if (cacheEntry.isExpired(new Date())) {
                        return null;
                    } else {
                        return cacheEntry;
                    }
                }
            }       
           
           
            //////////////////////////////////
            // DEFAULT Handling
           
           
            // handle permantent redirect
            if (response.getStatus() == 301)  {
                return new ExpiresCacheEntry(request, response, networkLatency, new Date(System.currentTimeMillis() + (30L * 24L * 60L * 60L * 1000L)), isSharedCache);
            }
      
           
        } catch (Exception e) {
            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("error occured by checking if cacheable" + e.toString());
            }
        }
       
        return null;
    }
   
   
    public void register(IHttpRequest request, long networkLatency, IHttpResponse response) throws IOException {
        CacheEntry ce = newCacheEntry(request, networkLatency, response);
        register(ce);
    }
   
   
    private void register(CacheEntry ce) throws IOException {
        if (ce == null) {
            return;
        }
       
        synchronized (this) {
            entries.putEntry(ce);
        }
    }
   
    public void deregister(IHttpRequest request) throws IOException {
        synchronized (this) {
            entries.removeEntry(request);
        }
    }
   
   
    @Override
    public String toString() {
        return entries.toString();
    }
   
   
    private static final class EntryCache extends LinkedHashMap<String, CacheEntry> {
    
        private static final long serialVersionUID = -2886392185640417068L;


        private static final int DEFAULT_ENTRY_SIZE_THRESHOLD = 10
        private int entrySizeThreshold = DEFAULT_ENTRY_SIZE_THRESHOLD;
       
        private int currentSize = 0;
        private int maxSize = 0;
       
       
     
       
       
        void checkTimeouts() {
            Date currentDate = new Date();
           
            for (CacheEntry cacheEntry : getEntries()) {
                try {
                    if (cacheEntry.isExpired(currentDate)) {
                        removeEntry(cacheEntry.getRequest());
                    }
                } catch (IOException ioe) {
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.fine("could not vaildate/remove cache entry " + cacheEntry.getRequest().getRequestUrl().toString() + " " + ioe.toString());
                    }
                }
            }
        }

       
        public Collection<CacheEntry> getEntries() {
            EntryCache copy = (EntryCache) clone();
            return copy.values();
        }
       
       
       
        public void setMaxSize(int sizeBytes) {
            maxSize = sizeBytes;
        }
       
        public int getMaxSize() {
            return maxSize;
        }

        public synchronized int getCurrentSize() {
            return currentSize;
        }

        protected boolean removeEldestEntry(java.util.Map.Entry<String, CacheEntry> eldest) {
            if (currentSize > maxSize) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("removing eldest entry (current size " + currentSize + " is larger than max size " + maxSize + ")");
                }
               
                currentSize -= eldest.getValue().getSize();
                return true;
            }
           
            return false;
        }
       
       
       
        private String computeFingerprint(IHttpRequest request) throws IOException {

            StringBuilder sb = new StringBuilder(request.getMethod() + SEPARATOR + request.getRequestUrl().toString());

            List<String> headers = new ArrayList<String>(request.getHeaderNameSet());
            Collections.sort(headers);

            for (String header : headers) {
                if (header.equalsIgnoreCase("User-Agent") ||
                    header.equalsIgnoreCase("Cache-Control") ||
                    header.equalsIgnoreCase("Connection") ||
                    header.equalsIgnoreCase("Transfer-Encoding") ||
                    header.equalsIgnoreCase("Keep-Alive") ||
                    header.equalsIgnoreCase("Proxy-Authenticate") ||
                    header.equalsIgnoreCase("Proxy-Authorization") ||
                    header.equalsIgnoreCase("Upgrade") ||
                    header.equalsIgnoreCase("Trailers") ||
                    header.equalsIgnoreCase("TE"))  {
                    continue;
                }
               
               
                sb.append(header + SEPARATOR);
               
                for (String value : request.getHeaderList(header)) {
                    sb.append(value + SEPARATOR);
                }
            }
            if (request.hasBody()) {
                sb.append(request.getNonBlockingBody().toString());
            }
           
            return sb.toString();
        }
       
       
       
        public CacheEntry getEntry(IHttpRequest request) throws IOException {
            return super.get(computeFingerprint(request));
        }
       
       
        int getMaxSizeCacheEntry() {
            return (maxSize / entrySizeThreshold);
        }
       

        public synchronized CacheEntry putEntry(CacheEntry entry) throws IOException {
            int size = entry.getSize();
           
            // very large entries will not be cached
            if (size > getMaxSizeCacheEntry()) {
                if (LOG.isLoggable(Level.FINE)) {
                    LOG.fine("entry " + entry.getRequest().getRequestUrl().toString() + " will not be cached (is to large: " + entry.getSize() + "bytes)");
                }
                return null;
            }
           
            CacheEntry removed = super.put(computeFingerprint(entry.getRequest()), entry);
           
            currentSize += size;
            if (removed != null) {
                currentSize -= removed.getSize();
           }
           
            return removed;
        }
       
       
       
        public synchronized CacheEntry removeEntry(IHttpRequest request) throws IOException {
           
            String fingerprint = computeFingerprint(request);
           
            CacheEntry removed = super.remove(fingerprint);
            if (removed != null) {
                currentSize -= removed.getSize();
            }
           
            return removed;
        }
  
       
        public void close() throws IOException {
            clear();
        }
    };
          
   
    private static Date computeExperieDate(String expire, long networkLatency) {
        if ((HttpUtils.parseHttpDateString(expire).getTime() - networkLatency) > 0) {
            return new Date(HttpUtils.parseHttpDateString(expire).getTime() - networkLatency);
        } else {
            return new Date(0);
        }
    }

 
    interface IValidationHandler {
       
        void onRevalidated(boolean isNotModified, CacheEntry ce);
       
        void onException(IOException ioe);
    }
 
 
 
  class CacheEntry {
       
      private final IHttpRequest request;
        private final IHttpResponse response;
        private final int size;
       
        private final Date cacheDate;
        private final Date defaultExpireDate = new Date(System.currentTimeMillis() + validationBasedCacheMaxTimeMillis);
       
       
        public CacheEntry(IHttpRequest request, IHttpResponse response) throws IOException {
            this(request, response, new Date());
        }
       
        public CacheEntry(IHttpRequest request, IHttpResponse response, Date cacheDate) throws IOException {
            this.request = request;
            this.response = response;
            this.cacheDate = cacheDate;
           
            int i = request.getRequestHeader().toString().length() +
                    response.getResponseHeader().toString().length();
           
            if (request.hasBody()) {
                i += request.getNonBlockingBody().available();
            }
            if (response.hasBody()) {
                i += response.getNonBlockingBody().available();
            }
           
            size = i;
        }
       
       
        final IHttpRequest getRequest() {
            return request;
        }
       
   
        final IHttpResponse getResponse() {
            return response;
        }
            
       
        final int getSize() {
            return size;
        }
   
        Date getCacheDate() {
            return cacheDate;
        }
       
        Date getExpireDate() {
            return defaultExpireDate;
        }
       
        boolean mustRevalidate(Date currentDate) {
            return true;
        }
       
        boolean supportsRevalidate() {
            return ((response.getHeader("Etag") != null) || (response.getHeader("Last-Modified") != null));
        }

        final boolean isAfter(Date date) {
            if (date.after(cacheDate)) {
                return true;
            }
           
            return false;
        }
       

        final boolean isExpired(Date currentDate) {
            if (currentDate.after(getExpireDate())) {
                return true;
            }
           
            return false;
        }
       
       
        void revalidate(final IValidationHandler hdl) throws IOException {

           
            IHttpRequest requestCopy = HttpUtils.copy(request);
           
            if (response.getHeader("Etag") != null) {
                requestCopy.setHeader("IF-None-Match", response.getHeader("Etag"));
            } else {
                requestCopy.setHeader("If-Modified-Since", response.getHeader("Last-Modified"));
            }           
            requestCopy.setAttribute(CacheHandler.SKIP_CACHE_HANDLING, "true");

           
            IHttpResponseHandler respHdl = new IHttpResponseHandler() {
               
                @Execution(Execution.NONTHREADED)
                @InvokeOn(InvokeOn.MESSAGE_RECEIVED)
                public void onResponse(IHttpResponse response) throws IOException {
                                       
                    if (response.getStatus() == 304) {
                        CacheEntry entry = updateCacheEntryOnRevaildation(request, response.getResponseHeader());
                        register(entry);
                       
                        hdl.onRevalidated(true, CacheEntry.this);
                       
                    } else {
                       
                        if (isCacheableSuccess(response.getStatus())) {
                            CacheEntry entry = updateCacheEntry(request, response);
                            register(entry);
                        } else {
                            deregister(request);
                        }
                       
                        hdl.onRevalidated(false, CacheEntry.this);
                    }
                }
               
               
                public void onException(IOException ioe) throws IOException {
                    if (LOG.isLoggable(Level.FINE)) {
                        LOG.fine("got exception by revalidating "+ ioe.toString());
                    }
                    deregister(request);
                   
                    hdl.onException(ioe);
                }
            };

            if (LOG.isLoggable(Level.FINE)) {
                LOG.fine("revalidating request " + requestCopy.getRequestUrl().toString());
            }
            httpClient.send(requestCopy, respHdl);
        }
       
       
        final CacheEntry updateCacheEntryOnRevaildation(IHttpRequest request, IHttpResponseHeader responseHeader) throws IOException {
     
            /* RFC 2616:
             * If a cache uses a received 304 response to update a cache entry, the
             * cache MUST update the entry to reflect any new field values given in
             * the response.
             */
           
            for (String headername : responseHeader.getHeaderNameSet()) {
                response.removeHeader(headername);
               
                for (String headervalue : responseHeader.getHeaderList(headername)) {
                    response.addHeader(headername, headervalue);
                }
            }
           
            return updateCacheEntry(request, response);
        }

        CacheEntry updateCacheEntry(IHttpRequest request, IHttpResponse response) throws IOException {
            return new CacheEntry(request, response, cacheDate);
        }
       
       
        final IHttpResponse newResponse() throws IOException {
            IHttpResponse response = HttpUtils.copy(getResponse());
            enhanceCachedResponse(response);
            return response;
        }
       
       
        void enhanceCachedResponse(IHttpResponse response) {
           
        }
    }
 
 
 
 
  private abstract class ExpiredBasedCacheEntry extends CacheEntry {
      
      private final boolean isShared;
        private final long networktLatency;
     
        public ExpiredBasedCacheEntry(IHttpRequest request, IHttpResponse response, long networkLatency, boolean isShared) throws IOException {
            super(request, response);
           
            this.networktLatency = networkLatency;
            this.isShared = isShared;
        }
       
    
       
        final CacheEntry updateCacheEntry(IHttpResponse response) throws IOException {
            return newCacheEntry(getRequest(), networktLatency, response);
        }

       
        @Override
        void enhanceCachedResponse(IHttpResponse response) {
            String cacheControl = response.getHeader("Cache-Control");
            if (cacheControl != null) {
                StringBuilder sb = new StringBuilder();
                for (String entry : cacheControl.split(",")) {
                    entry = entry.trim();
                   
                    if (isShared) {
                        if (entry.toLowerCase().startsWith("max-age=")) {
                            entry = "max-age=" + ((getExpireDate().getTime() - System.currentTimeMillis()) / 1000);
                        }
                       
                    } else {
                        if (entry.toLowerCase().startsWith("s-maxage=")) {
                            entry = "s-maxage=" + ((getExpireDate().getTime() - System.currentTimeMillis()) / 1000);
                        }
                    }

                   
                    sb.append(entry + ", ");
                }
                if (sb.length() > 0) {
                    sb.setLength(sb.length() - 2);
                }
                response.setHeader("Cache-Control", sb.toString());
            }
        }
    }
   
 
 
   
  private final class ExpiresCacheEntry extends ExpiredBasedCacheEntry {
        
      private final Date expireDate;
     
     
      public ExpiresCacheEntry(IHttpRequest request, IHttpResponse response, long networkLatency, String expire, boolean isShared) throws IOException {
            this(request, response, networkLatency, computeExperieDate(expire, networkLatency), isShared);
        }
     
     
      public ExpiresCacheEntry(IHttpRequest request, IHttpResponse response, long networkLatency, Date expireDate, boolean isShared) throws IOException {
          super(request, response, networkLatency, isShared);
         
          this.expireDate = expireDate;
        }
     
     
        Date getExpireDate() {
            return expireDate;
        }
     
  
      @Override
      boolean mustRevalidate(Date currentDate) {
          return false;
      }
  }
 
    
    private final class CacheControlCacheEntry extends ExpiredBasedCacheEntry {
    
        private boolean mustRevalidate;
        private Date expireDate;

       
        public CacheControlCacheEntry(IHttpRequest request, IHttpResponse response, long networkLatency, String cacheControl, boolean isShared) throws IOException {
            super(request, response, networkLatency, isShared);


            for (String directive : cacheControl.split(",")) {
                directive = directive.trim();
                if (directive.equalsIgnoreCase("no-cache") || directive.equalsIgnoreCase("no-store")) {
                    expireDate = new Date(0);
                    return;
                }
               
                if (isShared && directive.equalsIgnoreCase("private")) {
                    expireDate = new Date(0);
                    return;
                }
               
                if (directive.equalsIgnoreCase("must-revalidate")) {
                    mustRevalidate = true;
                }

                if (isShared && (directive.equalsIgnoreCase("proxy-revalidate"))) {
                    mustRevalidate = true;
                }
               
                if (directive.toLowerCase().startsWith("max-age=")) {
                    long maxAgeMillis = Long.parseLong(directive.substring("max-age=".length(), directive.length())) * 1000L;
                   
                    if ((maxAgeMillis - networkLatency) > 0) {
                        expireDate = new Date(System.currentTimeMillis() + (maxAgeMillis - networkLatency));
                    } else {
                        expireDate = new Date(0);
                    }
                }
            }
        }
       
       
        Date getExpireDate() {
            return expireDate;
        }

       
            
        @Override
        boolean mustRevalidate(Date currentDate) {
            return mustRevalidate || currentDate.after(getExpireDate());
        }
    }
}
TOP

Related Classes of org.xlightweb.client.HttpCache

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
ml" title="Examples of org.eclipse.core.runtime.content.IContentDescription">org.eclipse.core.runtime.content.IContentDescription
  • org.eclipse.core.runtime.NullProgressMonitor
  • org.gatein.pc.portlet.aspects.cache.StrongContentRef
  • TOP
    Copyright © 2018 www.massapi.com. All rights reserved.
    All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.