You are viewing a plain text version of this content. The canonical link for it is here.
Posted to solr-commits@lucene.apache.org by ho...@apache.org on 2008/02/21 23:44:20 UTC

svn commit: r630037 - in /lucene/solr/trunk: ./ client/java/solrj/src/org/apache/solr/client/solrj/embedded/ example/solr/conf/ src/java/org/apache/solr/core/ src/java/org/apache/solr/search/ src/test/org/apache/solr/servlet/ src/test/test-files/solr/c...

Author: hossman
Date: Thu Feb 21 14:44:19 2008
New Revision: 630037

URL: http://svn.apache.org/viewvc?rev=630037&view=rev
Log:
SOLR-127: HTTP Caching awareness

Added:
    lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java   (with props)
    lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java   (with props)
    lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java   (with props)
    lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml   (with props)
    lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/
    lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java   (with props)
    lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java   (with props)
Modified:
    lucene/solr/trunk/CHANGES.txt
    lucene/solr/trunk/client/java/solrj/src/org/apache/solr/client/solrj/embedded/JettySolrRunner.java
    lucene/solr/trunk/example/solr/conf/solrconfig.xml
    lucene/solr/trunk/src/java/org/apache/solr/core/SolrConfig.java
    lucene/solr/trunk/src/java/org/apache/solr/search/SolrIndexSearcher.java
    lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig.xml
    lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java
    lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrRequestParsers.java

Modified: lucene/solr/trunk/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/CHANGES.txt?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/CHANGES.txt (original)
+++ lucene/solr/trunk/CHANGES.txt Thu Feb 21 14:44:19 2008
@@ -191,6 +191,12 @@
     (ryan)
 
 38. SOLR-478: Added ability to get back unique key information from the LukeRequestHandler. (gsingers)    
+
+39. SOLR-127: HTTP Caching awareness.  Solr now recognizes HTTP Request
+    headers related to HTTP Caching (see RFC 2616 sec13) and will respond
+    with "304 Not Modified" when appropriate.  New options have been added
+    to solrconfig.xml to influence this behavior.
+    (Thomas Peuss via hossman)
     
 Changes in runtime behavior
 

Modified: lucene/solr/trunk/client/java/solrj/src/org/apache/solr/client/solrj/embedded/JettySolrRunner.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/client/java/solrj/src/org/apache/solr/client/solrj/embedded/JettySolrRunner.java?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/client/java/solrj/src/org/apache/solr/client/solrj/embedded/JettySolrRunner.java (original)
+++ lucene/solr/trunk/client/java/solrj/src/org/apache/solr/client/solrj/embedded/JettySolrRunner.java Thu Feb 21 14:44:19 2008
@@ -45,6 +45,12 @@
   {
     this.init( context, port );
   }
+
+  public JettySolrRunner( String context, int port, String solrConfigFilename )
+  {
+    this.init( context, port );
+    dispatchFilter.setInitParameter("solrconfig-filename", solrConfigFilename);
+  }
   
 //  public JettySolrRunner( String context, String home, String dataDir, int port, boolean log )
 //  {
@@ -88,6 +94,7 @@
   {
     if( server.isRunning() ) {
       server.stop();
+      server.join();
     }
   }
   

Modified: lucene/solr/trunk/example/solr/conf/solrconfig.xml
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/example/solr/conf/solrconfig.xml?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/example/solr/conf/solrconfig.xml (original)
+++ lucene/solr/trunk/example/solr/conf/solrconfig.xml Thu Feb 21 14:44:19 2008
@@ -264,6 +264,42 @@
   <requestDispatcher handleSelect="true" >
     <!--Make sure your system has some authentication before enabling remote streaming!  -->
     <requestParsers enableRemoteStreaming="false" multipartUploadLimitInKB="2048" />
+        
+    <!-- Set HTTP caching related parameters (for proxy caches and clients).
+          
+         To get the behaviour of Solr 1.2 (ie: no caching related headers)
+         use the never304="true" option and do not specify a value for
+         <cacheControl>
+    -->
+    <!-- <httpCaching never304="true"> -->
+    <httpCaching lastModifiedFrom="openTime"
+                 etagSeed="Solr">
+       <!-- lastModFrom="openTime" is the default, the Last-Modified value
+            (and validation against If-Modified-Since requests) will all be
+            relative to when the current Searcher was opened.
+            You can change it to lastModFrom="dirLastMod" if you want the
+            value to exactly corrispond to when the physical index was last
+            modified.
+               
+            etagSeed="..." is an option you can change to force the ETag
+            header (and validation against If-None-Match requests) to be
+            differnet even if the index has not changed (ie: when making
+            significant changes to your config file)
+
+            lastModifiedFrom and etagSeed are both ignored if you use the
+            never304="true" option.
+       -->
+       <!-- If you include a <cacheControl> directive, it will be used to
+            generate a Cache-Control header, as well as an Expires header
+            if the value contains "max-age="
+               
+            By default, no Cache-Control header is generated.
+
+            You can use the <cacheControl> option even if you have set
+            never304="true"
+       -->
+       <!-- <cacheControl>max-age=30, public</cacheControl> -->
+    </httpCaching>
   </requestDispatcher>
   
       

Modified: lucene/solr/trunk/src/java/org/apache/solr/core/SolrConfig.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/java/org/apache/solr/core/SolrConfig.java?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/src/java/org/apache/solr/core/SolrConfig.java (original)
+++ lucene/solr/trunk/src/java/org/apache/solr/core/SolrConfig.java Thu Feb 21 14:44:19 2008
@@ -33,6 +33,10 @@
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.StringTokenizer;
+import java.util.logging.Logger;
+import java.util.logging.Level;
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
 import java.io.IOException;
 import java.io.InputStream;
 
@@ -117,6 +121,8 @@
     hashDocSetMaxSize= getInt("//HashDocSet/@maxSize",3000);
     
     pingQueryParams = readPingQueryParams(this);
+
+    httpCachingConfig = new HttpCachingConfig(this);
     Config.log.info("Loaded SolrConfig: " + file);
     
     // TODO -- at solr 2.0. this should go away
@@ -146,6 +152,11 @@
   public final SolrIndexConfig defaultIndexConfig;
   public final SolrIndexConfig mainIndexConfig;
   
+  private final HttpCachingConfig httpCachingConfig;
+  public HttpCachingConfig getHttpCachingConfig() {
+    return httpCachingConfig;
+  }
+  
   // ping query request parameters
   @Deprecated
   private final NamedList pingQueryParams;
@@ -174,5 +185,77 @@
   @Deprecated
   public SolrQueryRequest getPingQueryRequest(SolrCore core) {
     return new LocalSolrQueryRequest(core, pingQueryParams);
+  }
+
+
+  public static class HttpCachingConfig {
+
+    /** config xpath prefix for getting HTTP Caching options */
+    private final static String CACHE_PRE
+      = "requestDispatcher/httpCaching/";
+    
+    /** For extracting Expires "ttl" from <cacheControl> config */
+    private final static Pattern MAX_AGE
+      = Pattern.compile("\\bmax-age=(\\d+)");
+    
+    public static enum LastModFrom {
+      OPENTIME, DIRLASTMOD, BOGUS;
+
+      /** Input must not be null */
+      public static LastModFrom parse(final String s) {
+        try {
+          return valueOf(s.toUpperCase());
+        } catch (Exception e) {
+          log.log(Level.WARNING,
+                  "Unrecognized value for lastModFrom: " + s, e);
+          return BOGUS;
+        }
+      }
+    }
+    
+    private final boolean never304;
+    private final String etagSeed;
+    private final String cacheControlHeader;
+    private final Integer maxAge;
+    private final LastModFrom lastModFrom;
+    
+    private HttpCachingConfig(SolrConfig conf) {
+
+      never304 = conf.getBool(CACHE_PRE+"@never304", false);
+      
+      etagSeed = conf.get(CACHE_PRE+"@etagSeed", "Solr");
+      
+
+      lastModFrom = LastModFrom.parse(conf.get(CACHE_PRE+"@lastModFrom",
+                                               "openTime"));
+      
+      cacheControlHeader = conf.get(CACHE_PRE+"cacheControl",null);
+
+      Integer tmp = null; // maxAge
+      if (null != cacheControlHeader) {
+        try { 
+          final Matcher ttlMatcher = MAX_AGE.matcher(cacheControlHeader);
+          final String ttlStr = ttlMatcher.find() ? ttlMatcher.group(1) : null;
+          tmp = (null != ttlStr && !"".equals(ttlStr))
+            ? Integer.valueOf(ttlStr)
+            : null;
+        } catch (Exception e) {
+          log.log(Level.WARNING,
+                  "Ignoring exception while attempting to " +
+                  "extract max-age from cacheControl config: " +
+                  cacheControlHeader, e);
+        }
+      }
+      maxAge = tmp;
+
+    }
+    
+    public boolean isNever304() { return never304; }
+    public String getEtagSeed() { return etagSeed; }
+    /** null if no Cache-Control header */
+    public String getCacheControlHeader() { return cacheControlHeader; }
+    /** null if no max age limitation */
+    public Integer getMaxAge() { return maxAge; }
+    public LastModFrom getLastModFrom() { return lastModFrom; }
   }
 }

Modified: lucene/solr/trunk/src/java/org/apache/solr/search/SolrIndexSearcher.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/java/org/apache/solr/search/SolrIndexSearcher.java?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/src/java/org/apache/solr/search/SolrIndexSearcher.java (original)
+++ lucene/solr/trunk/src/java/org/apache/solr/search/SolrIndexSearcher.java Thu Feb 21 14:44:19 2008
@@ -1395,7 +1395,6 @@
     }
   }
 
-
   /**
    * return the named generic cache
    */
@@ -1419,6 +1418,9 @@
     return cache==null ? null : cache.put(key,val);
   }
 
+  public long getOpenTime() {
+    return openTime;
+  }
 
   /////////////////////////////////////////////////////////////////////
   // SolrInfoMBean stuff: Statistics and Module Info

Added: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java (added)
+++ lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java Thu Feb 21 14:44:19 2008
@@ -0,0 +1,180 @@
+/**
+ * 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;
+
+import java.util.Date;
+
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.util.DateUtil;
+
+/**
+ * A test case for the several HTTP cache headers emitted by Solr
+ */
+public class CacheHeaderTest extends CacheHeaderTestBase {
+  @Override public String getSolrConfigFilename() { return "solrconfig.xml";  }
+  
+  protected void doLastModified(String method) throws Exception {
+    // We do a first request to get the last modified
+    // This must result in a 200 OK response
+    HttpMethodBase get = getSelectMethod(method);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+
+    assertEquals("Got no response code 200 in initial request", 200, get
+        .getStatusCode());
+
+    Header head = get.getResponseHeader("Last-Modified");
+    assertNotNull("We got no Last-Modified header", head);
+
+    Date lastModified = DateUtil.parseDate(head.getValue());
+
+    // If-Modified-Since tests
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Modified-Since", DateUtil.formatDate(new Date()));
+
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("Expected 304 NotModified response with current date", 304,
+        get.getStatusCode());
+
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Modified-Since", DateUtil.formatDate(new Date(
+        lastModified.getTime() - 10000)));
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("Expected 200 OK response with If-Modified-Since in the past",
+        200, get.getStatusCode());
+
+    // If-Unmodified-Since tests
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Unmodified-Since", DateUtil.formatDate(new Date(
+        lastModified.getTime() - 10000)));
+
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "Expected 412 Precondition failed with If-Unmodified-Since in the past",
+        412, get.getStatusCode());
+
+    get = getSelectMethod(method);
+    get
+        .addRequestHeader("If-Unmodified-Since", DateUtil
+            .formatDate(new Date()));
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "Expected 200 OK response with If-Unmodified-Since and current date",
+        200, get.getStatusCode());
+  }
+
+  // test ETag
+  protected void doETag(String method) throws Exception {
+    HttpMethodBase get = getSelectMethod(method);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+
+    assertEquals("Got no response code 200 in initial request", 200, get
+        .getStatusCode());
+
+    Header head = get.getResponseHeader("ETag");
+    assertNotNull("We got no ETag in the response", head);
+    assertTrue("Not a valid ETag", head.getValue().startsWith("\"")
+        && head.getValue().endsWith("\""));
+
+    String etag = head.getValue();
+
+    // If-None-Match tests
+    // we set a non matching ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-None-Match", "\"xyz123456\"");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "If-None-Match: Got no response code 200 in response to non matching ETag",
+        200, get.getStatusCode());
+
+    // now we set matching ETags
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-None-Match", "\"xyz1223\"");
+    get.addRequestHeader("If-None-Match", "\"1231323423\", \"1211211\",   "
+        + etag);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-None-Match: Got no response 304 to matching ETag", 304,
+        get.getStatusCode());
+
+    // we now set the special star ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-None-Match", "*");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-None-Match: Got no response 304 for star ETag", 304, get
+        .getStatusCode());
+
+    // If-Match tests
+    // we set a non matching ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Match", "\"xyz123456\"");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "If-Match: Got no response code 412 in response to non matching ETag",
+        412, get.getStatusCode());
+
+    // now we set matching ETags
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Match", "\"xyz1223\"");
+    get.addRequestHeader("If-Match", "\"1231323423\", \"1211211\",   " + etag);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-Match: Got no response 200 to matching ETag", 200, get
+        .getStatusCode());
+
+    // now we set the special star ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Match", "*");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-Match: Got no response 200 to star ETag", 200, get
+        .getStatusCode());
+  }
+
+  protected void doCacheControl(String method) throws Exception {
+    if ("POST".equals(method)) {
+      HttpMethodBase m = getSelectMethod(method);
+      getClient().executeMethod(m);
+      checkResponseBody(method, m);
+
+      Header head = m.getResponseHeader("Cache-Control");
+      assertNull("We got a cache-control header in response to POST", head);
+      
+      head=m.getResponseHeader("Expires");
+      assertNull("We got an Expires  header in response to POST", head);
+    } else {
+      HttpMethodBase m = getSelectMethod(method);
+      getClient().executeMethod(m);
+      checkResponseBody(method, m);
+
+      Header head = m.getResponseHeader("Cache-Control");
+      assertNotNull("We got no cache-control header", head);
+      
+      head=m.getResponseHeader("Expires");
+      assertNotNull("We got no Expires header in response",head);
+    }
+  }
+}

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTest.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Added: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java (added)
+++ lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java Thu Feb 21 14:44:19 2008
@@ -0,0 +1,148 @@
+/**
+ * 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;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.HeadMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.solr.client.solrj.SolrExampleTestBase;
+import org.apache.solr.client.solrj.SolrServer;
+import org.apache.solr.client.solrj.embedded.JettySolrRunner;
+import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer;
+
+public abstract class CacheHeaderTestBase extends SolrExampleTestBase {
+  @Override public String getSolrHome() {  return "solr/"; }
+  
+  abstract public String getSolrConfigFilename();
+  
+  public String getSolrConfigFile() { return getSolrHome()+"conf/"+getSolrConfigFilename(); }
+  
+  CommonsHttpSolrServer server;
+
+  JettySolrRunner jetty;
+
+  static final int port = 8985; // not 8983
+
+  static final String context = "/example";
+
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+    
+    jetty = new JettySolrRunner(context, port, getSolrConfigFilename());
+    jetty.start();
+
+    server = this.createNewSolrServer();
+  }
+
+  @Override
+  public void tearDown() throws Exception {
+    super.tearDown();
+    jetty.stop(); // stop the server
+  }
+  
+  @Override
+  protected SolrServer getSolrServer() {
+    return server;
+  }
+
+  @Override
+  protected CommonsHttpSolrServer createNewSolrServer() {
+    try {
+      // setup the server...
+      String url = "http://localhost:" + port + context;
+      CommonsHttpSolrServer s = new CommonsHttpSolrServer(url);
+      s.setConnectionTimeout(5);
+      s.setDefaultMaxConnectionsPerHost(100);
+      s.setMaxTotalConnections(100);
+      return s;
+    } catch (Exception ex) {
+      throw new RuntimeException(ex);
+    }
+  }
+
+  protected HttpMethodBase getSelectMethod(String method) {
+    HttpMethodBase m = null;
+    if ("GET".equals(method)) {
+      m = new GetMethod(server.getBaseURL() + "/select");
+    } else if ("HEAD".equals(method)) {
+      m = new HeadMethod(server.getBaseURL() + "/select");
+    } else if ("POST".equals(method)) {
+      m = new PostMethod(server.getBaseURL() + "/select");
+    }
+    m.setQueryString(new NameValuePair[] { new NameValuePair("q", "solr"),
+          new NameValuePair("qt", "standard") });
+    return m;
+  }
+
+  protected HttpClient getClient() {
+    return server.getHttpClient();
+  }
+
+  protected void checkResponseBody(String method, HttpMethodBase resp)
+      throws Exception {
+    String responseBody = resp.getResponseBodyAsString();
+    if ("GET".equals(method)) {
+      switch (resp.getStatusCode()) {
+        case 200:
+          assertTrue("Response body was empty for method " + method,
+              responseBody != null && responseBody.length() > 0);
+          break;
+        case 304:
+          assertTrue("Response body was not empty for method " + method,
+              responseBody == null || responseBody.length() == 0);
+          break;
+        case 412:
+          assertTrue("Response body was not empty for method " + method,
+              responseBody == null || responseBody.length() == 0);
+          break;
+        default:
+          System.err.println(responseBody);
+          assertEquals("Unknown request response", 0, resp.getStatusCode());
+      }
+    }
+    if ("HEAD".equals(method)) {
+      assertTrue("Response body was not empty for method " + method,
+          responseBody == null || responseBody.length() == 0);
+    }
+  }
+
+  // The tests
+  public void testLastModified() throws Exception {
+    doLastModified("GET");
+    doLastModified("HEAD");
+  }
+
+  public void testEtag() throws Exception {
+    doETag("GET");
+    doETag("HEAD");
+  }
+
+  public void testCacheControl() throws Exception {
+    doCacheControl("GET");
+    doCacheControl("HEAD");
+    doCacheControl("POST");
+  }
+
+  protected abstract void doCacheControl(String method) throws Exception;
+  protected abstract void doETag(String method) throws Exception;
+  protected abstract void doLastModified(String method) throws Exception;
+  
+}

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/CacheHeaderTestBase.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Added: lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java (added)
+++ lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java Thu Feb 21 14:44:19 2008
@@ -0,0 +1,158 @@
+/**
+ * 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;
+
+import java.util.Date;
+
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.util.DateUtil;
+
+/**
+ * A test case for the several HTTP cache headers emitted by Solr
+ */
+public class NoCacheHeaderTest extends CacheHeaderTestBase {
+  @Override public String getSolrConfigFilename() { return "solrconfig-nocache.xml";  }
+
+  // The tests
+  public void testLastModified() throws Exception {
+    doLastModified("GET");
+    doLastModified("HEAD");
+  }
+
+  public void testEtag() throws Exception {
+    doETag("GET");
+    doETag("HEAD");
+  }
+
+  public void testCacheControl() throws Exception {
+    doCacheControl("GET");
+    doCacheControl("HEAD");
+    doCacheControl("POST");
+  }
+  
+  protected void doLastModified(String method) throws Exception {
+    // We do a first request to get the last modified
+    // This must result in a 200 OK response
+    HttpMethodBase get = getSelectMethod(method);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+
+    assertEquals("Got no response code 200 in initial request", 200, get
+        .getStatusCode());
+
+    Header head = get.getResponseHeader("Last-Modified");
+    assertNull("We got a Last-Modified header", head);
+
+    // If-Modified-Since tests
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Modified-Since", DateUtil.formatDate(new Date()));
+
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("Expected 200 with If-Modified-Since header. We should never get a 304 here", 200,
+        get.getStatusCode());
+
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Modified-Since", DateUtil.formatDate(new Date(System.currentTimeMillis()-10000)));
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("Expected 200 with If-Modified-Since header. We should never get a 304 here",
+        200, get.getStatusCode());
+
+    // If-Unmodified-Since tests
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Unmodified-Since", DateUtil.formatDate(new Date(System.currentTimeMillis()-10000)));
+
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "Expected 200 with If-Unmodified-Since header. We should never get a 304 here",
+        200, get.getStatusCode());
+
+    get = getSelectMethod(method);
+    get
+        .addRequestHeader("If-Unmodified-Since", DateUtil
+            .formatDate(new Date()));
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "Expected 200 with If-Unmodified-Since header. We should never get a 304 here",
+        200, get.getStatusCode());
+  }
+
+  // test ETag
+  protected void doETag(String method) throws Exception {
+    HttpMethodBase get = getSelectMethod(method);
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+
+    assertEquals("Got no response code 200 in initial request", 200, get
+        .getStatusCode());
+
+    Header head = get.getResponseHeader("ETag");
+    assertNull("We got an ETag in the response", head);
+
+    // If-None-Match tests
+    // we set a non matching ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-None-Match", "\"xyz123456\"");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "If-None-Match: Got no response code 200 in response to non matching ETag",
+        200, get.getStatusCode());
+
+    // we now set the special star ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-None-Match", "*");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-None-Match: Got no response 200 for star ETag", 200, get
+        .getStatusCode());
+
+    // If-Match tests
+    // we set a non matching ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Match", "\"xyz123456\"");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals(
+        "If-Match: Got no response code 200 in response to non matching ETag",
+        200, get.getStatusCode());
+
+    // now we set the special star ETag
+    get = getSelectMethod(method);
+    get.addRequestHeader("If-Match", "*");
+    getClient().executeMethod(get);
+    checkResponseBody(method, get);
+    assertEquals("If-Match: Got no response 200 to star ETag", 200, get
+        .getStatusCode());
+  }
+
+  protected void doCacheControl(String method) throws Exception {
+      HttpMethodBase m = getSelectMethod(method);
+      getClient().executeMethod(m);
+      checkResponseBody(method, m);
+
+      Header head = m.getResponseHeader("Cache-Control");
+      assertNull("We got a cache-control header in response", head);
+      
+      head = m.getResponseHeader("Expires");
+      assertNull("We got an Expires header in response", head);
+  }
+}
\ No newline at end of file

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/test/org/apache/solr/servlet/NoCacheHeaderTest.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Added: lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml (added)
+++ lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml Thu Feb 21 14:44:19 2008
@@ -0,0 +1,324 @@
+<?xml version="1.0" ?>
+
+<!--
+ 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.
+-->
+
+<!-- $Id$
+     $Source$
+     $Name$
+  -->
+
+<config>
+
+  <!-- Used to specify an alternate directory to hold all index data.
+       It defaults to "index" if not present, and should probably
+       not be changed if replication is in use. -->
+  <!--
+  <indexDir>index</indexDir>
+  -->
+
+  <indexDefaults>
+   <!-- Values here affect all index writers and act as a default
+   unless overridden. -->
+    <useCompoundFile>false</useCompoundFile>
+    <mergeFactor>10</mergeFactor>
+    <maxBufferedDocs>1000</maxBufferedDocs>
+    <maxMergeDocs>2147483647</maxMergeDocs>
+    <maxFieldLength>10000</maxFieldLength>
+
+    <!-- these are global... can't currently override per index -->
+    <writeLockTimeout>1000</writeLockTimeout>
+    <commitLockTimeout>10000</commitLockTimeout>
+
+    <lockType>single</lockType>
+  </indexDefaults>
+
+  <mainIndex>
+    <!-- lucene options specific to the main on-disk lucene index -->
+    <useCompoundFile>false</useCompoundFile>
+    <mergeFactor>10</mergeFactor>
+    <maxBufferedDocs>1000</maxBufferedDocs>
+    <maxMergeDocs>2147483647</maxMergeDocs>
+    <maxFieldLength>10000</maxFieldLength>
+
+    <unlockOnStartup>true</unlockOnStartup>
+  </mainIndex>
+
+  <updateHandler class="solr.DirectUpdateHandler2">
+
+    <!-- autocommit pending docs if certain criteria are met 
+    <autoCommit> 
+      <maxDocs>10000</maxDocs>
+      <maxTime>3600000</maxTime> 
+    </autoCommit>
+    -->
+    <!-- represents a lower bound on the frequency that commits may
+    occur (in seconds). NOTE: not yet implemented
+    
+    <commitIntervalLowerBound>0</commitIntervalLowerBound>
+    -->
+
+    <!-- The RunExecutableListener executes an external command.
+         exe - the name of the executable to run
+         dir - dir to use as the current working directory. default="."
+         wait - the calling thread waits until the executable returns. default="true"
+         args - the arguments to pass to the program.  default=nothing
+         env - environment variables to set.  default=nothing
+      -->
+    <!-- A postCommit event is fired after every commit
+    <listener event="postCommit" class="solr.RunExecutableListener">
+      <str name="exe">/var/opt/resin3/__PORT__/scripts/solr/snapshooter</str>
+      <str name="dir">/var/opt/resin3/__PORT__</str>
+      <bool name="wait">true</bool>
+      <arr name="args"> <str>arg1</str> <str>arg2</str> </arr>
+      <arr name="env"> <str>MYVAR=val1</str> </arr>
+    </listener>
+    -->
+
+
+  </updateHandler>
+
+
+  <query>
+    <!-- Maximum number of clauses in a boolean query... can affect
+        range or wildcard queries that expand to big boolean
+        queries.  An exception is thrown if exceeded.
+    -->
+    <maxBooleanClauses>1024</maxBooleanClauses>
+
+    
+    <!-- Cache specification for Filters or DocSets - unordered set of *all* documents
+         that match a particular query.
+      -->
+    <filterCache
+      class="solr.search.LRUCache"
+      size="512"
+      initialSize="512"
+      autowarmCount="256"/>
+
+    <queryResultCache
+      class="solr.search.LRUCache"
+      size="512"
+      initialSize="512"
+      autowarmCount="1024"/>
+
+    <documentCache
+      class="solr.search.LRUCache"
+      size="512"
+      initialSize="512"
+      autowarmCount="0"/>
+
+    <!-- If true, stored fields that are not requested will be loaded lazily.
+    -->
+    <enableLazyFieldLoading>true</enableLazyFieldLoading>
+
+    <!--
+
+    <cache name="myUserCache"
+      class="solr.search.LRUCache"
+      size="4096"
+      initialSize="1024"
+      autowarmCount="1024"
+      regenerator="MyRegenerator"
+      />
+    -->
+
+
+    <useFilterForSortedQuery>true</useFilterForSortedQuery>
+
+    <queryResultWindowSize>10</queryResultWindowSize>
+
+    <!-- set maxSize artificially low to exercise both types of sets -->
+    <HashDocSet maxSize="3" loadFactor="0.75"/>
+
+
+    <!-- boolToFilterOptimizer converts boolean clauses with zero boost
+         into cached filters if the number of docs selected by the clause exceeds
+         the threshold (represented as a fraction of the total index)
+    -->
+    <boolTofilterOptimizer enabled="false" cacheSize="32" threshold=".05"/>
+
+
+    <!-- a newSearcher event is fired whenever a new searcher is being prepared
+         and there is a current searcher handling requests (aka registered). -->
+    <!-- QuerySenderListener takes an array of NamedList and executes a
+         local query request for each NamedList in sequence. -->
+    <!--
+    <listener event="newSearcher" class="solr.QuerySenderListener">
+      <arr name="queries">
+        <lst> <str name="q">solr</str> <str name="start">0</str> <str name="rows">10</str> </lst>
+        <lst> <str name="q">rocks</str> <str name="start">0</str> <str name="rows">10</str> </lst>
+      </arr>
+    </listener>
+    -->
+
+    <!-- a firstSearcher event is fired whenever a new searcher is being
+         prepared but there is no current registered searcher to handle
+         requests or to gain prewarming data from. -->
+    <!--
+    <listener event="firstSearcher" class="solr.QuerySenderListener">
+      <arr name="queries">
+        <lst> <str name="q">fast_warm</str> <str name="start">0</str> <str name="rows">10</str> </lst>
+      </arr>
+    </listener>
+    -->
+
+
+  </query>
+
+
+  <!-- An alternate set representation that uses an integer hash to store filters (sets of docids).
+       If the set cardinality <= maxSize elements, then HashDocSet will be used instead of the bitset
+       based HashBitset. -->
+
+  <!-- requestHandler plugins... incoming queries will be dispatched to the
+     correct handler based on the qt (query type) param matching the
+     name of registered handlers.
+      The "standard" request handler is the default and will be used if qt
+     is not specified in the request.
+  -->
+  <requestHandler name="standard" class="solr.StandardRequestHandler"/>
+  <requestHandler name="dismaxOldStyleDefaults"
+                  class="solr.DisMaxRequestHandler" >
+     <!-- for historic reasons, DisMaxRequestHandler will use all of
+          it's init params as "defaults" if there is no "defaults" list
+          specified
+     -->
+     <float name="tie">0.01</float>
+     <str name="qf">
+        text^0.5 features_t^1.0 subject^1.4 title_stemmed^2.0
+     </str>
+     <str name="pf">
+        text^0.2 features_t^1.1 subject^1.4 title_stemmed^2.0 title^1.5
+     </str>
+     <str name="bf">
+        ord(weight)^0.5 recip(rord(iind),1,1000,1000)^0.3
+     </str>
+     <str name="mm">
+        3&lt;-1 5&lt;-2 6&lt;90%
+     </str>
+     <int name="ps">100</int>
+  </requestHandler>
+  <requestHandler name="dismax" class="solr.DisMaxRequestHandler" >
+    <lst name="defaults">
+     <str name="q.alt">*:*</str>
+     <float name="tie">0.01</float>
+     <str name="qf">
+        text^0.5 features_t^1.0 subject^1.4 title_stemmed^2.0
+     </str>
+     <str name="pf">
+        text^0.2 features_t^1.1 subject^1.4 title_stemmed^2.0 title^1.5
+     </str>
+     <str name="bf">
+        ord(weight)^0.5 recip(rord(iind),1,1000,1000)^0.3
+     </str>
+     <str name="mm">
+        3&lt;-1 5&lt;-2 6&lt;90%
+     </str>
+     <int name="ps">100</int>
+    </lst>
+  </requestHandler>
+  <requestHandler name="old" class="solr.tst.OldRequestHandler" >
+    <int name="myparam">1000</int>
+    <float name="ratio">1.4142135</float>
+    <arr name="myarr"><int>1</int><int>2</int></arr>
+    <str>foo</str>
+  </requestHandler>
+  <requestHandler name="oldagain" class="solr.tst.OldRequestHandler" >
+    <lst name="lst1"> <str name="op">sqrt</str> <int name="val">2</int> </lst>
+    <lst name="lst2"> <str name="op">log</str> <float name="val">10</float> </lst>
+  </requestHandler>
+
+  <requestHandler name="test" class="solr.tst.TestRequestHandler" />
+
+  <!-- test query parameter defaults --> 
+  <requestHandler name="defaults" class="solr.StandardRequestHandler">
+    <lst name="defaults">
+      <int name="rows">4</int>
+      <bool name="hl">true</bool>
+      <str name="hl.fl">text,name,subject,title,whitetok</str>
+    </lst>
+  </requestHandler>
+  
+  <!-- test query parameter defaults --> 
+  <requestHandler name="lazy" class="solr.StandardRequestHandler" startup="lazy">
+    <lst name="defaults">
+      <int name="rows">4</int>
+      <bool name="hl">true</bool>
+      <str name="hl.fl">text,name,subject,title,whitetok</str>
+    </lst>
+  </requestHandler>
+
+  <requestHandler name="/update"     class="solr.XmlUpdateRequestHandler"          />
+  <requestHandler name="/update/csv" class="solr.CSVRequestHandler" startup="lazy" />
+
+  <!-- test elevation -->
+  <searchComponent name="elevate" class="org.apache.solr.handler.component.QueryElevationComponent" >
+    <str name="queryFieldType">string</str>
+    <str name="config-file">elevate.xml</str>
+  </searchComponent>
+ 
+  <requestHandler name="/elevate" class="org.apache.solr.handler.component.SearchHandler">
+    <lst name="defaults">
+      <str name="echoParams">explicit</str>
+    </lst>
+    <arr name="last-components">
+      <str>elevate</str>
+    </arr>
+  </requestHandler>
+  
+
+  <highlighting>
+   <!-- Configure the standard fragmenter -->
+   <fragmenter name="gap" class="org.apache.solr.highlight.GapFragmenter" default="true">
+    <lst name="defaults">
+     <int name="hl.fragsize">100</int>
+    </lst>
+   </fragmenter>
+   
+   <fragmenter name="regex" class="org.apache.solr.highlight.RegexFragmenter">
+    <lst name="defaults">
+     <int name="hl.fragsize">70</int>
+    </lst>
+   </fragmenter>
+   
+   <!-- Configure the standard formatter -->
+   <formatter name="html" class="org.apache.solr.highlight.HtmlFormatter" default="true">
+    <lst name="defaults">
+     <str name="hl.simple.pre"><![CDATA[<em>]]></str>
+     <str name="hl.simple.post"><![CDATA[</em>]]></str>
+    </lst>
+   </formatter>
+  </highlighting>
+
+
+  <!-- enable streaming for testing... -->
+  <requestDispatcher handleSelect="true" >
+    <requestParsers enableRemoteStreaming="true" multipartUploadLimitInKB="2048" />
+    <httpCaching never304="true" /> 
+  </requestDispatcher>
+
+  <admin>
+    <defaultQuery>solr</defaultQuery>
+    <gettableFiles>solrconfig.xml scheam.xml admin-extra.html</gettableFiles>
+  </admin>
+
+  <!-- test getting system property -->
+  <propTest attr1="${solr.test.sys.prop1}-$${literal}"
+            attr2="${non.existent.sys.prop:default-from-config}">prefix-${solr.test.sys.prop2}-suffix</propTest>
+
+</config>

Propchange: lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig-nocache.xml
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Modified: lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig.xml
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig.xml?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig.xml (original)
+++ lucene/solr/trunk/src/test/test-files/solr/conf/solrconfig.xml Thu Feb 21 14:44:19 2008
@@ -309,6 +309,9 @@
   <!-- enable streaming for testing... -->
   <requestDispatcher handleSelect="true" >
     <requestParsers enableRemoteStreaming="true" multipartUploadLimitInKB="2048" />
+    <httpCaching lastModifiedFrom="openTime" etagSeed="Solr" never304="false">
+      <cacheControl>max-age=30, public</cacheControl>
+    </httpCaching>
   </requestDispatcher>
 
   <admin>

Modified: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java (original)
+++ lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrDispatchFilter.java Thu Feb 21 14:44:19 2008
@@ -44,6 +44,8 @@
 import org.apache.solr.request.SolrQueryRequest;
 import org.apache.solr.request.SolrQueryResponse;
 import org.apache.solr.request.SolrRequestHandler;
+import org.apache.solr.servlet.cache.HttpCacheHeaderUtil;
+import org.apache.solr.servlet.cache.Method;
 
 /**
  * This filter looks at the incoming URL maps them to handlers defined in solrconfig.xml
@@ -51,13 +53,14 @@
 public class SolrDispatchFilter implements Filter 
 {
   final Logger log = Logger.getLogger(SolrDispatchFilter.class.getName());
-    
+  
   protected SolrCore singlecore;
   protected MultiCore multicore;
   protected SolrRequestParsers parsers;
   protected boolean handleSelect = false;
   protected String pathPrefix = null; // strip this from the beginning of a path
   protected String abortErrorMessage = null;
+  protected String solrConfigFilename = null;
   
   public void init(FilterConfig config) throws ServletException 
   {
@@ -67,6 +70,7 @@
     try {
       // web.xml configuration
       this.pathPrefix = config.getInitParameter( "path-prefix" );
+      this.solrConfigFilename = config.getInitParameter("solrconfig-filename");
       
       // Find a valid solr core
       SolrCore core = null;
@@ -87,7 +91,11 @@
         core = multicore.getDefaultCore();
       }
       else {
-        singlecore = new SolrCore( null, null, new SolrConfig(), null );
+        if (this.solrConfigFilename==null) {
+          singlecore = new SolrCore( null, null, new SolrConfig(), null );
+        } else {
+          singlecore = new SolrCore( null, null, new SolrConfig(this.solrConfigFilename), null);
+        }
         core = singlecore;
       }
       
@@ -168,6 +176,7 @@
     if( request instanceof HttpServletRequest) {
       SolrQueryRequest solrReq = null;
       HttpServletRequest req = (HttpServletRequest)request;
+      HttpServletResponse resp = (HttpServletResponse)response;
       try {
         String path = req.getServletPath();    
         if( req.getPathInfo() != null ) {
@@ -233,7 +242,29 @@
           if( solrReq == null ) {
             solrReq = parsers.parse( core, path, req );
           }
+          
+          final SolrConfig conf = core.getSolrConfig();
+          final Method reqMethod = Method.getMethod(req.getMethod());
+
+          if (Method.POST != reqMethod) {
+            HttpCacheHeaderUtil.setCacheControlHeader(conf, resp);
+          }
+            
+          // unless we have been explicitly told not to, do cache validation
+          if (!conf.getHttpCachingConfig().isNever304()) {
+            // if we've confirmed cache validation, return immediately
+            if (HttpCacheHeaderUtil.doCacheHeaderValidation(solrReq,
+                                                            req,resp)) {
+              return;
+            }
+          }
+          
           SolrQueryResponse solrRsp = new SolrQueryResponse();
+          /* even for HEAD requests, we need to execute the handler to
+           * ensure we don't get an error (and to make sure the correct 
+           * QueryResponseWriter is selectedand we get the correct
+           * Content-Type)
+           */
           this.execute( req, handler, solrReq, solrRsp );
           if( solrRsp.getException() != null ) {
             sendError( (HttpServletResponse)response, solrRsp.getException() );
@@ -243,6 +274,11 @@
           // Now write it out
           QueryResponseWriter responseWriter = core.getQueryResponseWriter(solrReq);
           response.setContentType(responseWriter.getContentType(solrReq, solrRsp));
+          if (Method.HEAD == Method.getMethod(req.getMethod())) {
+            // nothing to write out, waited this long just to get ContentType
+            return; 
+          }
+          
           PrintWriter out = response.getWriter();
           responseWriter.write(out, solrReq, solrRsp);
           return;
@@ -303,7 +339,7 @@
       }
     }
     res.sendError( code, ex.getMessage() + trace );
-  }
+  }    
 
   //---------------------------------------------------------------------
   //---------------------------------------------------------------------

Modified: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrRequestParsers.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrRequestParsers.java?rev=630037&r1=630036&r2=630037&view=diff
==============================================================================
--- lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrRequestParsers.java (original)
+++ lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/SolrRequestParsers.java Thu Feb 21 14:44:19 2008
@@ -348,7 +348,7 @@
       final HttpServletRequest req, ArrayList<ContentStream> streams ) throws Exception
   {
     String method = req.getMethod().toUpperCase();
-    if( "GET".equals( method ) ) {
+    if( "GET".equals( method ) || "HEAD".equals( method )) {
       return new ServletSolrParams(req);
     }
     if( "POST".equals( method ) ) {
@@ -367,7 +367,7 @@
       }
       return raw.parseParamsAndFillStreams(req, streams);
     }
-    throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Unsuported method: "+method );
+    throw new SolrException( SolrException.ErrorCode.BAD_REQUEST, "Unsupported method: "+method );
   }
 }
 

Added: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java (added)
+++ lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java Thu Feb 21 14:44:19 2008
@@ -0,0 +1,298 @@
+/**
+ * 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.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.request.SolrRequestHandler;
+
+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;
+        
+        etagCache = "\""
+          + new String(Base64.encodeBase64((Long.toHexString
+                                            (Long.reverse(indexVersionCache))
+                                            + etagSeed).getBytes()))
+          + "\"";
+      }
+      
+      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.
+   */
+  public static void setCacheControlHeader(final SolrConfig conf,
+                                           final HttpServletResponse resp) {
+
+    final String cc = conf.getHttpCachingConfig().getCacheControlHeader();
+    if (null != cc) {
+      resp.setHeader("Cache-Control", cc);
+    }
+    Integer maxAge = conf.getHttpCachingConfig().getMaxAge();
+    if (null != maxAge) {
+      resp.setDateHeader("Expires", System.currentTimeMillis()
+                         + (maxAge * 1000));
+    }
+
+    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 HttpServletResponse resp)
+    throws IOException {
+
+    final Method reqMethod=Method.getMethod(req.getMethod());
+    
+    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;
+  }
+}

Propchange: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/HttpCacheHeaderUtil.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL

Added: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java
URL: http://svn.apache.org/viewvc/lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java?rev=630037&view=auto
==============================================================================
--- lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java (added)
+++ lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java Thu Feb 21 14:44:19 2008
@@ -0,0 +1,41 @@
+/**
+ * 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;
+
+public enum Method {
+  GET("GET"), POST("POST"), HEAD("HEAD"), OTHER("");
+
+  private final String method;
+
+  Method(String method) {
+    this.method = method.intern();
+  }
+
+  public static Method getMethod(String method) {
+    method = method.toUpperCase().intern();
+
+    for (Method m : Method.values()) {
+      // we can use == because we interned the String objects
+      if (m.method==method) {
+        return m;
+      }
+    }
+
+    return OTHER;
+  }
+}

Propchange: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: lucene/solr/trunk/src/webapp/src/org/apache/solr/servlet/cache/Method.java
------------------------------------------------------------------------------
    svn:keywords = Date Author Id Revision HeadURL