content: /**
   * Licensed to the Apache Software Foundation (ASF) under one or more
   * contributor license agreements.  See the NOTICE file distributed with
   * this work for additional information regarding copyright ownership.
   * The ASF licenses this file to You under the Apache License, Version 2.0
   * (the "License"); you may not use this file except in compliance with
   * the License.  You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package org.apache.solr.servlet.cache;
  
  import java.io.IOException;
  import java.io.UnsupportedEncodingException;
  import java.util.Collections;
  import java.util.Map;
  import java.util.WeakHashMap;
  import java.util.List;
  
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  
  import org.apache.lucene.index.IndexReader;
  
  import org.apache.solr.core.SolrCore;
  import org.apache.solr.core.SolrConfig;
  import org.apache.solr.core.SolrConfig.HttpCachingConfig.LastModFrom;
  import org.apache.solr.common.SolrException;
  import org.apache.solr.common.SolrException.ErrorCode;
  import org.apache.solr.search.SolrIndexSearcher;
  import org.apache.solr.request.SolrQueryRequest;
  import org.apache.solr.response.SolrQueryResponse;
  
  import org.apache.commons.codec.binary.Base64;
  
  public final class HttpCacheHeaderUtil {
    
    public static void sendNotModified(HttpServletResponse res)
      throws IOException {
      res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
    }
  
    public static void sendPreconditionFailed(HttpServletResponse res)
      throws IOException {
      res.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED);
    }
    
    /**
     * Weak Ref based cache for keeping track of core specific etagSeed
     * and the last computed etag.
     *
     * @see #calcEtag
     */
    private static Map<SolrCore, EtagCacheVal> etagCoreCache
      = new WeakHashMap<SolrCore, EtagCacheVal>();
  
    /** @see #etagCoreCache */
    private static class EtagCacheVal {
      private final String etagSeed;
      
      private String etagCache = null;
      private long indexVersionCache=-1;
      
      public EtagCacheVal(final String etagSeed) {
        this.etagSeed = etagSeed;
      }
  
      public String calcEtag(final long currentIndexVersion) {
        if (currentIndexVersion != indexVersionCache) {
          indexVersionCache=currentIndexVersion;
          
          try {
            etagCache = "\""
             + new String(Base64.encodeBase64((Long.toHexString
                                               (Long.reverse(indexVersionCache))
                                               + etagSeed).getBytes()), "US-ASCII")
             + "\"";
          } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e); // may not happen
          }
        }
        
        return etagCache;
      }
    }
    
    /**
     * Calculates a tag for the ETag header.
     *
     * @param solrReq
     * @return a tag
     */
    public static String calcEtag(final SolrQueryRequest solrReq) {
      final SolrCore core = solrReq.getCore();
      final long currentIndexVersion
        = solrReq.getSearcher().getReader().getVersion();
  
      EtagCacheVal etagCache = etagCoreCache.get(core);
      if (null == etagCache) {
        final String etagSeed
          = core.getSolrConfig().getHttpCachingConfig().getEtagSeed();
        etagCache = new EtagCacheVal(etagSeed);
        etagCoreCache.put(core, etagCache);
      }
      
      return etagCache.calcEtag(currentIndexVersion);
      
    }
  
    /**
     * Checks if one of the tags in the list equals the given etag.
     * 
     * @param headerList
     *            the ETag header related header elements
     * @param etag
     *            the ETag to compare with
     * @return true if the etag is found in one of the header elements - false
     *         otherwise
     */
    public static boolean isMatchingEtag(final List<String> headerList,
        final String etag) {
      for (String header : headerList) {
        final String[] headerEtags = header.split(",");
        for (String s : headerEtags) {
          s = s.trim();
          if (s.equals(etag) || "*".equals(s)) {
            return true;
          }
        }
      }
  
      return false;
    }
  
    /**
     * Calculate the appropriate last-modified time for Solr relative the current request.
     * 
     * @param solrReq
     * @return the timestamp to use as a last modified time.
     */
    public static long calcLastModified(final SolrQueryRequest solrReq) {
      final SolrCore core = solrReq.getCore();
      final SolrIndexSearcher searcher = solrReq.getSearcher();
      
      final LastModFrom lastModFrom
        = core.getSolrConfig().getHttpCachingConfig().getLastModFrom();
  
      long lastMod;
      try {
        // assume default, change if needed (getOpenTime() should be fast)
        lastMod =
          LastModFrom.DIRLASTMOD == lastModFrom
          ? IndexReader.lastModified(searcher.getReader().directory())
          : searcher.getOpenTime();
      } catch (IOException e) {
        // we're pretty freaking screwed if this happens
        throw new SolrException(ErrorCode.SERVER_ERROR, e);
      }
      // Get the time where the searcher has been opened
      // We get rid of the milliseconds because the HTTP header has only
      // second granularity
      return lastMod - (lastMod % 1000L);
    }
  
    /**
     * Set the Cache-Control HTTP header (and Expires if needed)
     * based on the SolrConfig.
     * @param conf The config of the SolrCore handling this request
     * @param resp The servlet response object to modify
     * @param method The request method (GET, POST, ...) used by this request
     */
    public static void setCacheControlHeader(final SolrConfig conf,
                                             final HttpServletResponse resp, final Method method) {
      // We do not emit HTTP header for POST and OTHER request types
      if (Method.POST==method || Method.OTHER==method) {
        return;
      }
      final String cc = conf.getHttpCachingConfig().getCacheControlHeader();
      if (null != cc) {
        resp.setHeader("Cache-Control", cc);
      }
      Long maxAge = conf.getHttpCachingConfig().getMaxAge();
      if (null != maxAge) {
        resp.setDateHeader("Expires", System.currentTimeMillis()
                           + (maxAge * 1000L));
      }
  
      return;
    }
  
    /**
     * Sets HTTP Response cache validator headers appropriately and
     * validates the HTTP Request against these using any conditional
     * request headers.
     *
     * If the request contains conditional headers, and those headers
     * indicate a match with the current known state of the system, this
     * method will return "true" indicating that a 304 Status code can be
     * returned, and no further processing is needed.
     *
     * 
     * @return true if the request contains conditional headers, and those
     *         headers indicate a match with the current known state of the
     *         system -- indicating that a 304 Status code can be returned to
     *         the client, and no further request processing is needed.  
     */
    public static boolean doCacheHeaderValidation(final SolrQueryRequest solrReq,
                                                  final HttpServletRequest req,
                                                  final Method reqMethod,
                                                  final HttpServletResponse resp)
      throws IOException {
      
      if (Method.POST==reqMethod || Method.OTHER==reqMethod) {
        return false;
      }
      
      final long lastMod = HttpCacheHeaderUtil.calcLastModified(solrReq);
      final String etag = HttpCacheHeaderUtil.calcEtag(solrReq);
      
      resp.setDateHeader("Last-Modified", lastMod);
      resp.setHeader("ETag", etag);
  
      if (checkETagValidators(req, resp, reqMethod, etag)) {
        return true;
      }
  
      if (checkLastModValidators(req, resp, lastMod)) {
        return true;
      }
  
      return false;
    }
    
  
    /**
     * Check for etag related conditional headers and set status 
     * 
     * @return true if no request processing is necessary and HTTP response status has been set, false otherwise.
     * @throws IOException
     */
    @SuppressWarnings("unchecked")
    public static boolean checkETagValidators(final HttpServletRequest req,
                                              final HttpServletResponse resp,
                                              final Method reqMethod,
                                              final String etag)
      throws IOException {
      
      // First check If-None-Match because this is the common used header
      // element by HTTP clients
      final List<String> ifNoneMatchList = Collections.list(req
          .getHeaders("If-None-Match"));
      if (ifNoneMatchList.size() > 0 && isMatchingEtag(ifNoneMatchList, etag)) {
        if (reqMethod == Method.GET || reqMethod == Method.HEAD) {
          sendNotModified(resp);
        } else {
          sendPreconditionFailed(resp);
        }
        return true;
      }
  
      // Check for If-Match headers
      final List<String> ifMatchList = Collections.list(req
          .getHeaders("If-Match"));
      if (ifMatchList.size() > 0 && !isMatchingEtag(ifMatchList, etag)) {
        sendPreconditionFailed(resp);
        return true;
      }
  
      return false;
    }
  
    /**
     * Check for modify time related conditional headers and set status 
     * 
     * @return true if no request processing is necessary and HTTP response status has been set, false otherwise.
     * @throws IOException
     */
    public static boolean checkLastModValidators(final HttpServletRequest req,
                                                 final HttpServletResponse resp,
                                                 final long lastMod)
      throws IOException {
  
      try {
        // First check for If-Modified-Since because this is the common
        // used header by HTTP clients
        final long modifiedSince = req.getDateHeader("If-Modified-Since");
        if (modifiedSince != -1L && lastMod <= modifiedSince) {
          // Send a "not-modified"
          sendNotModified(resp);
          return true;
        }
        
        final long unmodifiedSince = req.getDateHeader("If-Unmodified-Since");
        if (unmodifiedSince != -1L && lastMod > unmodifiedSince) {
          // Send a "precondition failed"
          sendPreconditionFailed(resp);
          return true;
        }
      } catch (IllegalArgumentException iae) {
        // one of our date headers was not formated properly, ignore it
        /* NOOP */
      }
      return false;
    }
  
     /**
     * Checks if the downstream request handler wants to avoid HTTP caching of
     * the response.
     * 
     * @param solrRsp The Solr response object
     * @param resp The HTTP servlet response object
     * @param reqMethod The HTTP request type
     */
    public static void checkHttpCachingVeto(final SolrQueryResponse solrRsp,
        HttpServletResponse resp, final Method reqMethod) {
      // For POST we do nothing. They never get cached
      if (Method.POST == reqMethod || Method.OTHER == reqMethod) {
        return;
      }
      // If the request handler has not vetoed and there is no
      // exception silently return
      if (solrRsp.isHttpCaching() && solrRsp.getException() == null) {
        return;
      }
      
      // Otherwise we tell the caches that we don't want to cache the response
      resp.setHeader("Cache-Control", "no-cache, no-store");
  
      // For HTTP/1.0 proxy caches
      resp.setHeader("Pragma", "no-cache");
  
      // This sets the expiry date to a date in the past
      // As long as no time machines get invented this is safe
      resp.setHeader("Expires", "Sat, 01 Jan 2000 01:00:00 GMT");
  
      // We signal "just modified" just in case some broken
      // proxy cache does not follow the above headers
      resp.setDateHeader("Last-Modified", System.currentTimeMillis());
      
      // We override the ETag with something different
      resp.setHeader("ETag", '"'+Long.toHexString(System.currentTimeMillis())+'"');
    } 
  }
  