You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2010/09/06 21:56:53 UTC

svn commit: r993139 - in /httpcomponents/httpclient/trunk/httpclient-cache/src: main/java/org/apache/http/impl/client/cache/ test/java/org/apache/http/impl/client/cache/

Author: olegk
Date: Mon Sep  6 19:56:52 2010
New Revision: 993139

URL: http://svn.apache.org/viewvc?rev=993139&view=rev
Log:
HTTPCLIENT-986: cache module does not completely handle upstream Warning headers correctly
Contributed by Jonathan Moore <jonathan_moore at comcast.com>

Added:
    httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java   (with props)
    httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java   (with props)
Modified:
    httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java
    httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java
    httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java?rev=993139&r1=993138&r2=993139&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java (original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java Mon Sep  6 19:56:52 2010
@@ -107,9 +107,8 @@ class CacheEntryUpdater {
         }
 
         removeCacheHeadersThatMatchResponse(cacheEntryHeaderList, response);
-
-        cacheEntryHeaderList.addAll(Arrays.asList(response.getAllHeaders()));
         removeCacheEntry1xxWarnings(cacheEntryHeaderList, entry);
+        cacheEntryHeaderList.addAll(Arrays.asList(response.getAllHeaders()));
 
         return cacheEntryHeaderList.toArray(new Header[cacheEntryHeaderList.size()]);
     }

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java?rev=993139&r1=993138&r2=993139&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java (original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java Mon Sep  6 19:56:52 2010
@@ -26,8 +26,11 @@
  */
 package org.apache.http.impl.client.cache;
 
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 
+import org.apache.http.Header;
 import org.apache.http.HttpEntityEnclosingRequest;
 import org.apache.http.HttpRequest;
 import org.apache.http.HttpResponse;
@@ -38,7 +41,9 @@ import org.apache.http.annotation.Immuta
 import org.apache.http.client.ClientProtocolException;
 import org.apache.http.client.cache.HeaderConstants;
 import org.apache.http.impl.client.RequestWrapper;
+import org.apache.http.impl.cookie.DateParseException;
 import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.message.BasicHeader;
 import org.apache.http.protocol.HTTP;
 
 /**
@@ -77,6 +82,38 @@ class ResponseProtocolCompliance {
         ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(request, response);
 
         ensure206ContainsDateHeader(response);
+
+        warningsWithNonMatchingWarnDatesAreRemoved(response);
+    }
+
+    private void warningsWithNonMatchingWarnDatesAreRemoved(
+            HttpResponse response) {
+        Date responseDate = null;
+        try {
+            responseDate = DateUtils.parseDate(response.getFirstHeader("Date").getValue());
+        } catch (DateParseException e) {
+        }
+        if (responseDate == null) return;
+        Header[] warningHeaders = response.getHeaders("Warning");
+        if (warningHeaders == null || warningHeaders.length == 0) return;
+        List<Header> newWarningHeaders = new ArrayList<Header>();
+        boolean modified = false;
+        for(Header h : warningHeaders) {
+            for(WarningValue wv : WarningValue.getWarningValues(h)) {
+                Date warnDate = wv.getWarnDate();
+                if (warnDate == null || warnDate.equals(responseDate)) {
+                    newWarningHeaders.add(new BasicHeader("Warning",wv.toString()));
+                } else {
+                    modified = true;
+                }
+            }
+        }
+        if (modified) {
+            response.removeHeaders("Warning");
+            for(Header h : newWarningHeaders) {
+                response.addHeader(h);
+            }
+        }
     }
 
     private void authenticationRequiredDidNotHaveAProxyAuthenticationHeader(HttpRequest request,

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java?rev=993139&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java Mon Sep  6 19:56:52 2010
@@ -0,0 +1,358 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.impl.client.cache;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.http.Header;
+import org.apache.http.impl.cookie.DateParseException;
+import org.apache.http.impl.cookie.DateUtils;
+
+/** This class provides for parsing and understanding Warning headers. As
+ * the Warning header can be multi-valued, but the values can contain
+ * separators like commas inside quoted strings, we cannot use the regular
+ * {@link Header#getElements()} call to access the values.
+ */
+class WarningValue {
+
+    private int offs;
+    private int init_offs;
+    private String src;
+    private int warnCode;
+    private String warnAgent;
+    private String warnText;
+    private Date warnDate;
+
+    WarningValue(String s) {
+        this(s, 0);
+    }
+
+    WarningValue(String s, int offs) {
+        this.offs = this.init_offs = offs;
+        this.src = s;
+        consumeWarnValue();
+    }
+
+    /** Returns an array of the parseable warning values contained
+     * in the given header value, which is assumed to be a
+     * Warning header. Improperly formatted warning values will be
+     * skipped, in keeping with the philosophy of "ignore what you
+     * cannot understand."
+     * @param h Warning {@link Header} to parse
+     * @return array of <code>WarnValue</code> objects
+     */
+    public static WarningValue[] getWarningValues(Header h) {
+        List<WarningValue> out = new ArrayList<WarningValue>();
+        String src = h.getValue();
+        int offs = 0;
+        while(offs < src.length()) {
+            try {
+                WarningValue wv = new WarningValue(src, offs);
+                out.add(wv);
+                offs = wv.offs;
+            } catch (IllegalArgumentException e) {
+                final int nextComma = src.indexOf(',', offs);
+                if (nextComma == -1) break;
+                offs = nextComma + 1;
+            }
+        }
+        WarningValue[] wvs = {};
+        return out.toArray(wvs);
+    }
+
+    /*
+     * LWS            = [CRLF] 1*( SP | HT )
+     * CRLF           = CR LF
+     */
+    protected void consumeLinearWhitespace() {
+        while(offs < src.length()) {
+            switch(src.charAt(offs)) {
+            case '\r':
+                if (offs+2 >= src.length()
+                    || src.charAt(offs+1) != '\n'
+                    || (src.charAt(offs+2) != ' '
+                        && src.charAt(offs+2) != '\t')) {
+                    return;
+                }
+                offs += 2;
+                break;
+            case ' ':
+            case '\t':
+                break;
+            default:
+                return;
+            }
+            offs++;
+        }
+    }
+
+    /*
+     * CHAR           = <any US-ASCII character (octets 0 - 127)>
+     */
+    private boolean isChar(char c) {
+        int i = (int)c;
+        return (i >= 0 && i <= 127);
+    }
+
+    /*
+     * CTL            = <any US-ASCII control character
+                        (octets 0 - 31) and DEL (127)>
+     */
+    private boolean isControl(char c) {
+        int i = (int)c;
+        return (i == 127 || (i >=0 && i <= 31));
+    }
+
+    /*
+     * separators     = "(" | ")" | "<" | ">" | "@"
+     *                | "," | ";" | ":" | "\" | <">
+     *                | "/" | "[" | "]" | "?" | "="
+     *                | "{" | "}" | SP | HT
+     */
+    private boolean isSeparator(char c) {
+        return (c == '(' || c == ')' || c == '<' || c == '>'
+                || c == '@' || c == ',' || c == ';' || c == ':'
+                || c == '\\' || c == '\"' || c == '/'
+                || c == '[' || c == ']' || c == '?' || c == '='
+                || c == '{' || c == '}' || c == ' ' || c == '\t');
+    }
+
+    /*
+     * token          = 1*<any CHAR except CTLs or separators>
+     */
+    protected void consumeToken() {
+        if (!isTokenChar(src.charAt(offs))) parseError();
+        while(offs < src.length()) {
+            if (!isTokenChar(src.charAt(offs))) break;
+            offs++;
+        }
+    }
+
+    private boolean isTokenChar(char c) {
+        return (isChar(c) && !isControl(c) && !isSeparator(c));
+    }
+
+    private static final String TOPLABEL = "\\p{Alpha}([\\p{Alnum}-]*\\p{Alnum})?";
+    private static final String DOMAINLABEL = "\\p{Alnum}([\\p{Alnum}-]*\\p{Alnum})?";
+    private static final String HOSTNAME = "(" + DOMAINLABEL + "\\.)*" + TOPLABEL + "\\.?";
+    private static final String IPV4ADDRESS = "\\d+\\.\\d+\\.\\d+\\.\\d+";
+    private static final String HOST = "(" + HOSTNAME + ")|(" + IPV4ADDRESS + ")";
+    private static final String PORT = "\\d*";
+    private static final String HOSTPORT = "(" + HOST + ")(\\:" + PORT + ")?";
+    private static final Pattern HOSTPORT_PATTERN = Pattern.compile(HOSTPORT);
+
+    protected void consumeHostPort() {
+        Matcher m = HOSTPORT_PATTERN.matcher(src.substring(offs));
+        if (!m.find()) parseError();
+        if (m.start() != 0) parseError();
+        offs += m.end();
+    }
+
+
+    /*
+     * warn-agent = ( host [ ":" port ] ) | pseudonym
+     * pseudonym         = token
+     */
+    protected void consumeWarnAgent() {
+        int curr_offs = offs;
+        try {
+            consumeHostPort();
+            warnAgent = src.substring(curr_offs, offs);
+            consumeCharacter(' ');
+            return;
+        } catch (IllegalArgumentException e) {
+            offs = curr_offs;
+        }
+        consumeToken();
+        warnAgent = src.substring(curr_offs, offs);
+        consumeCharacter(' ');
+    }
+
+    /*
+     * quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
+     * qdtext         = <any TEXT except <">>
+     */
+    protected void consumeQuotedString() {
+        if (src.charAt(offs) != '\"') parseError();
+        offs++;
+        boolean foundEnd = false;
+        while(offs < src.length() && !foundEnd) {
+            char c = src.charAt(offs);
+            if (offs + 1 < src.length() && c == '\\'
+                && isChar(src.charAt(offs+1))) {
+                offs += 2;	// consume quoted-pair
+            } else if (c == '\"') {
+                foundEnd = true;
+                offs++;
+            } else if (c != '\"' && !isControl(c)) {
+                offs++;
+            } else {
+                parseError();
+            }
+        }
+        if (!foundEnd) parseError();
+    }
+
+    /*
+     * warn-text  = quoted-string
+     */
+    protected void consumeWarnText() {
+        int curr = offs;
+        consumeQuotedString();
+        warnText = src.substring(curr, offs);
+    }
+
+    private static final String MONTH = "Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec";
+    private static final String WEEKDAY = "Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday";
+    private static final String WKDAY = "Mon|Tue|Wed|Thu|Fri|Sat|Sun";
+    private static final String TIME = "\\d{2}:\\d{2}:\\d{2}";
+    private static final String DATE3 = "(" + MONTH + ") ( |\\d)\\d";
+    private static final String DATE2 = "\\d{2}-(" + MONTH + ")-\\d{2}";
+    private static final String DATE1 = "\\d{2} (" + MONTH + ") \\d{4}";
+    private static final String ASCTIME_DATE = "(" + WKDAY + ") (" + DATE3 + ") (" + TIME + ") \\d{4}";
+    private static final String RFC850_DATE = "(" + WEEKDAY + "), (" + DATE2 + ") (" + TIME + ") GMT";
+    private static final String RFC1123_DATE = "(" + WKDAY + "), (" + DATE1 + ") (" + TIME + ") GMT";
+    private static final String HTTP_DATE = "(" + RFC1123_DATE + ")|(" + RFC850_DATE + ")|(" + ASCTIME_DATE + ")";
+    private static final String WARN_DATE = "\"(" + HTTP_DATE + ")\"";
+    private static final Pattern WARN_DATE_PATTERN = Pattern.compile(WARN_DATE);
+
+    /*
+     * warn-date  = <"> HTTP-date <">
+     */
+    protected void consumeWarnDate() {
+        int curr = offs;
+        Matcher m = WARN_DATE_PATTERN.matcher(src.substring(offs));
+        if (!m.lookingAt()) parseError();
+        offs += m.end();
+        try {
+            warnDate = DateUtils.parseDate(src.substring(curr+1,offs-1));
+        } catch (DateParseException e) {
+            throw new IllegalStateException("couldn't parse a parseable date");
+        }
+    }
+
+    /*
+     * warning-value = warn-code SP warn-agent SP warn-text [SP warn-date]
+     */
+    protected void consumeWarnValue() {
+        consumeLinearWhitespace();
+        consumeWarnCode();
+        consumeWarnAgent();
+        consumeWarnText();
+        if (offs + 1 < src.length() && src.charAt(offs) == ' ' && src.charAt(offs+1) == '\"') {
+            consumeCharacter(' ');
+            consumeWarnDate();
+        }
+        consumeLinearWhitespace();
+        if (offs != src.length()) {
+            consumeCharacter(',');
+        }
+    }
+
+    protected void consumeCharacter(char c) {
+        if (offs + 1 > src.length()
+            || c != src.charAt(offs)) {
+            parseError();
+        }
+        offs++;
+    }
+
+    /*
+     * warn-code  = 3DIGIT
+     */
+    protected void consumeWarnCode() {
+        if (offs + 4 > src.length()
+            || !Character.isDigit(src.charAt(offs))
+            || !Character.isDigit(src.charAt(offs + 1))
+            || !Character.isDigit(src.charAt(offs + 2))
+            || src.charAt(offs + 3) != ' ') {
+            parseError();
+        }
+        warnCode = Integer.parseInt(src.substring(offs,offs+3));
+        offs += 4;
+    }
+
+    private void parseError() {
+        String s = src.substring(init_offs);
+        throw new IllegalArgumentException("Bad warn code \"" + s + "\"");
+    }
+
+    /** Returns the 3-digit code associated with this warning.
+     * @return <code>int</code>
+     */
+    public int getWarnCode() { return warnCode; }
+
+    /** Returns the "warn-agent" string associated with this warning,
+     * which is either the name or pseudonym of the server that added
+     * this particular Warning header.
+     * @return {@link String}
+     */
+    public String getWarnAgent() { return warnAgent; }
+
+    /** Returns the human-readable warning text for this warning. Note
+     * that the original quoted-string is returned here, including
+     * escaping for any contained characters. In other words, if the
+     * header was:
+     * <pre>
+     *   Warning: 110 fred "Response is stale"
+     * </pre>
+     * then this method will return <code>"\"Response is stale\""</code>
+     * (surrounding quotes included).
+     * @return {@link String}
+     */
+    public String getWarnText() { return warnText; }
+
+    /** Returns the date and time when this warning was added, or
+     * <code>null</code> if a warning date was not supplied in the
+     * header.
+     * @return {@link Date}
+     */
+    public Date getWarnDate() { return warnDate; }
+
+    /** Formats a <code>WarningValue</code> as a {@link String}
+     * suitable for including in a header. For example, you can:
+     * <pre>
+     *   WarningValue wv = ...;
+     *   HttpResponse resp = ...;
+     *   resp.addHeader("Warning", wv.toString());
+     * </pre>
+     * @return {@link String}
+     */
+    public String toString() {
+        if (warnDate != null) {
+            return String.format("%d %s %s \"%s\"", warnCode,
+                    warnAgent, warnText, DateUtils.formatDate(warnDate));
+        } else {
+            return String.format("%d %s %s", warnCode, warnAgent, warnText);
+        }
+    }
+
+}

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java
------------------------------------------------------------------------------
    svn:keywords = Date Revision

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain

Modified: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java?rev=993139&r1=993138&r2=993139&view=diff
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (original)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java Mon Sep  6 19:56:52 2010
@@ -2291,6 +2291,12 @@ public class TestProtocolRequirements ex
      * there was a communication failure."
      *
      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.1.1
+     *
+     * "111 Revalidation failed MUST be included if a cache returns a stale
+     * response because an attempt to revalidate the response failed, due to an
+     * inability to reach the server."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
      */
     @Test
     public void testMustServeAppropriateErrorOrWarningIfNoOriginCommunicationPossible()
@@ -2538,6 +2544,12 @@ public class TestProtocolRequirements ex
      * been added."
      *
      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.2.4
+     *
+     * "113 Heuristic expiration MUST be included if the cache heuristically
+     * chose a freshness lifetime greater than 24 hours and the response's age
+     * is greater than 24 hours."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
      */
     @Test
     public void testHeuristicCacheOlderThan24HoursHasWarningAttached() throws Exception {
@@ -4705,6 +4717,11 @@ public class TestProtocolRequirements ex
      * is stale).
      *
      * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9.3
+     *
+     * "110 Response is stale MUST be included whenever the returned
+     * response is stale."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
      */
     @Test
     public void testWarning110IsAddedToStaleResponses()
@@ -5478,7 +5495,7 @@ public class TestProtocolRequirements ex
         request.removeHeaders("Via");
         EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class),
                 EasyMock.capture(cap), (HttpContext)EasyMock.isNull()))
-            .andReturn(originResponse);
+                .andReturn(originResponse);
 
         replayMocks();
         impl.execute(host, request);
@@ -5564,7 +5581,7 @@ public class TestProtocolRequirements ex
      */
     @Test
     public void testViaHeaderOnRequestProperlyRecordsClientProtocol()
-            throws Exception {
+    throws Exception {
         request = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0);
         request.removeHeaders("Via");
         Capture<HttpRequest> cap = new Capture<HttpRequest>();
@@ -5588,7 +5605,7 @@ public class TestProtocolRequirements ex
 
     @Test
     public void testViaHeaderOnResponseProperlyRecordsOriginProtocol()
-        throws Exception {
+    throws Exception {
 
         originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content");
 
@@ -5608,4 +5625,184 @@ public class TestProtocolRequirements ex
         }
         Assert.assertEquals("1.0", protoParts[protoParts.length - 1]);
     }
+
+    /* "A cache MUST NOT delete any Warning header that it received with
+     * a message."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
+     */
+    @Test
+    public void testRetainsWarningHeadersReceivedFromUpstream()
+        throws Exception {
+        originResponse.removeHeaders("Warning");
+        final String warning = "199 fred \"misc\"";
+        originResponse.addHeader("Warning", warning);
+        backendExpectsAnyRequest().andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+        Assert.assertEquals(warning,
+                result.getFirstHeader("Warning").getValue());
+    }
+
+    /* "However, if a cache successfully validates a cache entry, it
+     * SHOULD remove any Warning headers previously attached to that
+     * entry except as specified for specific Warning codes. It MUST
+     * then add any Warning headers received in the validating response."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
+     */
+    @Test
+    public void testUpdatesWarningHeadersOnValidation()
+        throws Exception {
+        HttpRequest req1 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
+        HttpRequest req2 = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_1);
+
+        Date now = new Date();
+        Date twentySecondsAgo = new Date(now.getTime() - 20 * 1000L);
+        HttpResponse resp1 = make200Response();
+        resp1.setHeader("Date", DateUtils.formatDate(twentySecondsAgo));
+        resp1.setHeader("Cache-Control","public,max-age=5");
+        resp1.setHeader("ETag", "\"etag1\"");
+        final String oldWarning = "113 wilma \"stale\"";
+        resp1.setHeader("Warning", oldWarning);
+
+        Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+        HttpResponse resp2 = new BasicHttpResponse(HttpVersion.HTTP_1_1, HttpStatus.SC_NOT_MODIFIED, "Not Modified");
+        resp2.setHeader("Date", DateUtils.formatDate(tenSecondsAgo));
+        resp2.setHeader("ETag", "\"etag1\"");
+        final String newWarning = "113 betty \"stale too\"";
+        resp2.setHeader("Warning", newWarning);
+
+        backendExpectsAnyRequest().andReturn(resp1);
+        backendExpectsAnyRequest().andReturn(resp2);
+
+        replayMocks();
+        impl.execute(host, req1);
+        HttpResponse result = impl.execute(host, req2);
+        verifyMocks();
+
+        boolean oldWarningFound = false;
+        boolean newWarningFound = false;
+        for(Header h : result.getHeaders("Warning")) {
+            for(String warnValue : h.getValue().split("\\s*,\\s*")) {
+                if (oldWarning.equals(warnValue)) {
+                    oldWarningFound = true;
+                } else if (newWarning.equals(warnValue)) {
+                    newWarningFound = true;
+                }
+            }
+        }
+        Assert.assertFalse(oldWarningFound);
+        Assert.assertTrue(newWarningFound);
+    }
+
+    /* "If an implementation sends a message with one or more Warning
+     * headers whose version is HTTP/1.0 or lower, then the sender MUST
+     * include in each warning-value a warn-date that matches the date
+     * in the response."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
+     */
+    @Test
+    public void testWarnDatesAreAddedToWarningsOnLowerProtocolVersions()
+        throws Exception {
+        final String dateHdr = DateUtils.formatDate(new Date());
+        final String origWarning = "110 fred \"stale\"";
+        originResponse.setStatusLine(HttpVersion.HTTP_1_0, HttpStatus.SC_OK);
+        originResponse.addHeader("Warning", origWarning);
+        originResponse.setHeader("Date", dateHdr);
+        backendExpectsAnyRequest().andReturn(originResponse);
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+        // note that currently the implementation acts as an HTTP/1.1 proxy,
+        // which means that all the responses from the caching module should
+        // be HTTP/1.1, so we won't actually be testing anything here until
+        // that changes.
+        if (HttpVersion.HTTP_1_0.greaterEquals(result.getProtocolVersion())) {
+            Assert.assertEquals(dateHdr, result.getFirstHeader("Date").getValue());
+            boolean warningFound = false;
+            String targetWarning = origWarning + " \"" + dateHdr + "\"";
+            for(Header h : result.getHeaders("Warning")) {
+                for(String warning : h.getValue().split("\\s*,\\s*")) {
+                    if (targetWarning.equals(warning)) {
+                        warningFound = true;
+                        break;
+                    }
+                }
+            }
+            Assert.assertTrue(warningFound);
+        }
+    }
+
+    /* "If an implementation receives a message with a warning-value that
+     * includes a warn-date, and that warn-date is different from the Date
+     * value in the response, then that warning-value MUST be deleted from
+     * the message before storing, forwarding, or using it. (This prevents
+     * bad consequences of naive caching of Warning header fields.) If all
+     * of the warning-values are deleted for this reason, the Warning
+     * header MUST be deleted as well."
+     *
+     * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.46
+     */
+    @Test
+    public void testStripsBadlyDatedWarningsFromForwardedResponses()
+        throws Exception {
+        Date now = new Date();
+        Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+        originResponse.setHeader("Date", DateUtils.formatDate(now));
+        originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
+                + DateUtils.formatDate(tenSecondsAgo) + "\"");
+        originResponse.setHeader("Cache-Control","no-cache,no-store");
+        backendExpectsAnyRequest().andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+
+        for(Header h : result.getHeaders("Warning")) {
+            Assert.assertFalse(h.getValue().contains("wilma"));
+        }
+    }
+
+    @Test
+    public void testStripsBadlyDatedWarningsFromStoredResponses()
+        throws Exception {
+        Date now = new Date();
+        Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+        originResponse.setHeader("Date", DateUtils.formatDate(now));
+        originResponse.addHeader("Warning", "110 fred \"stale\", 110 wilma \"stale\" \""
+                + DateUtils.formatDate(tenSecondsAgo) + "\"");
+        originResponse.setHeader("Cache-Control","public,max-age=3600");
+        backendExpectsAnyRequest().andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+
+        for(Header h : result.getHeaders("Warning")) {
+            Assert.assertFalse(h.getValue().contains("wilma"));
+        }
+    }
+
+    @Test
+    public void testRemovesWarningHeaderIfAllWarnValuesAreBadlyDated()
+    throws Exception {
+        Date now = new Date();
+        Date tenSecondsAgo = new Date(now.getTime() - 10 * 1000L);
+        originResponse.setHeader("Date", DateUtils.formatDate(now));
+        originResponse.addHeader("Warning", "110 wilma \"stale\" \""
+                + DateUtils.formatDate(tenSecondsAgo) + "\"");
+        backendExpectsAnyRequest().andReturn(originResponse);
+
+        replayMocks();
+        HttpResponse result = impl.execute(host, request);
+        verifyMocks();
+
+        Header[] warningHeaders = result.getHeaders("Warning");
+        Assert.assertTrue(warningHeaders == null || warningHeaders.length == 0);
+    }
+
 }
\ No newline at end of file

Added: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java
URL: http://svn.apache.org/viewvc/httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java?rev=993139&view=auto
==============================================================================
--- httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java (added)
+++ httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java Mon Sep  6 19:56:52 2010
@@ -0,0 +1,230 @@
+/*
+ * ====================================================================
+ * 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.http.impl.client.cache;
+
+import java.util.Date;
+
+import org.apache.http.Header;
+import org.apache.http.impl.cookie.DateUtils;
+import org.apache.http.message.BasicHeader;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class TestWarningValue {
+
+    @Test
+    public void testParseSingleWarnValue() {
+        Header h = new BasicHeader("Warning","110 fred \"stale\"");
+        WarningValue[] result = WarningValue.getWarningValues(h);
+        Assert.assertEquals(1, result.length);
+        WarningValue wv = result[0];
+        Assert.assertEquals(110, wv.getWarnCode());
+        Assert.assertEquals("fred", wv.getWarnAgent());
+        Assert.assertEquals("\"stale\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+    }
+
+    @Test
+    public void testParseMultipleWarnValues() {
+        Header h = new BasicHeader("Warning","110 fred \"stale\", 111 wilma \"other\"");
+        WarningValue[] result = WarningValue.getWarningValues(h);
+        Assert.assertEquals(2, result.length);
+        WarningValue wv = result[0];
+        Assert.assertEquals(110, wv.getWarnCode());
+        Assert.assertEquals("fred", wv.getWarnAgent());
+        Assert.assertEquals("\"stale\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+        wv = result[1];
+        Assert.assertEquals(111, wv.getWarnCode());
+        Assert.assertEquals("wilma", wv.getWarnAgent());
+        Assert.assertEquals("\"other\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+    }
+
+    @Test
+    public void testMidHeaderParseErrorRecovery() {
+        Header h = new BasicHeader("Warning","110 fred \"stale\", bogus, 111 wilma \"other\"");
+        WarningValue[] result = WarningValue.getWarningValues(h);
+        Assert.assertEquals(2, result.length);
+        WarningValue wv = result[0];
+        Assert.assertEquals(110, wv.getWarnCode());
+        Assert.assertEquals("fred", wv.getWarnAgent());
+        Assert.assertEquals("\"stale\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+        wv = result[1];
+        Assert.assertEquals(111, wv.getWarnCode());
+        Assert.assertEquals("wilma", wv.getWarnAgent());
+        Assert.assertEquals("\"other\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+    }
+
+    @Test
+    public void testTrickyCommaMidHeaderParseErrorRecovery() {
+        Header h = new BasicHeader("Warning","110 fred \"stale\", \"bogus, dude\", 111 wilma \"other\"");
+        WarningValue[] result = WarningValue.getWarningValues(h);
+        Assert.assertEquals(2, result.length);
+        WarningValue wv = result[0];
+        Assert.assertEquals(110, wv.getWarnCode());
+        Assert.assertEquals("fred", wv.getWarnAgent());
+        Assert.assertEquals("\"stale\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+        wv = result[1];
+        Assert.assertEquals(111, wv.getWarnCode());
+        Assert.assertEquals("wilma", wv.getWarnAgent());
+        Assert.assertEquals("\"other\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+    }
+
+    @Test
+    public void testParseErrorRecoveryAtEndOfHeader() {
+        Header h = new BasicHeader("Warning","110 fred \"stale\", 111 wilma \"other\", \"bogus, dude\"");
+        WarningValue[] result = WarningValue.getWarningValues(h);
+        Assert.assertEquals(2, result.length);
+        WarningValue wv = result[0];
+        Assert.assertEquals(110, wv.getWarnCode());
+        Assert.assertEquals("fred", wv.getWarnAgent());
+        Assert.assertEquals("\"stale\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+        wv = result[1];
+        Assert.assertEquals(111, wv.getWarnCode());
+        Assert.assertEquals("wilma", wv.getWarnAgent());
+        Assert.assertEquals("\"other\"", wv.getWarnText());
+        Assert.assertNull(wv.getWarnDate());
+    }
+
+    @Test
+    public void testConstructSingleWarnValue() {
+        WarningValue impl = new WarningValue("110 fred \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithIPv4Address() {
+        WarningValue impl = new WarningValue("110 192.168.1.1 \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("192.168.1.1", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithHostname() {
+        WarningValue impl = new WarningValue("110 foo.example.com \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("foo.example.com", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithHostnameAndPort() {
+        WarningValue impl = new WarningValue("110 foo.example.com:8080 \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("foo.example.com:8080", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithIPv4AddressAndPort() {
+        WarningValue impl = new WarningValue("110 192.168.1.1:8080 \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("192.168.1.1:8080", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithPseudonym() {
+        WarningValue impl = new WarningValue("110 ca$hm0ney \"stale\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("ca$hm0ney", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithTextWithSpaces() {
+        WarningValue impl = new WarningValue("110 fred \"stale stuff\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale stuff\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithTextWithCommas() {
+        WarningValue impl = new WarningValue("110 fred \"stale, stuff\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale, stuff\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithTextWithEscapedQuotes() {
+        WarningValue impl = new WarningValue("110 fred \"stale\\\" stuff\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\\\" stuff\"", impl.getWarnText());
+        Assert.assertNull(impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithAscTimeWarnDate() throws Exception {
+        WarningValue impl = new WarningValue("110 fred \"stale\" \"Sun Nov  6 08:49:37 1994\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Date target = DateUtils.parseDate("Sun Nov  6 08:49:37 1994");
+        Assert.assertEquals(target, impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithRFC850WarnDate() throws Exception {
+        WarningValue impl = new WarningValue("110 fred \"stale\" \"Sunday, 06-Nov-94 08:49:37 GMT\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Date target = DateUtils.parseDate("Sunday, 06-Nov-94 08:49:37 GMT");
+        Assert.assertEquals(target, impl.getWarnDate());
+    }
+
+    @Test
+    public void testConstructWarnValueWithRFC1123WarnDate() throws Exception {
+        WarningValue impl = new WarningValue("110 fred \"stale\" \"Sun, 06 Nov 1994 08:49:37 GMT\"");
+        Assert.assertEquals(110, impl.getWarnCode());
+        Assert.assertEquals("fred", impl.getWarnAgent());
+        Assert.assertEquals("\"stale\"", impl.getWarnText());
+        Date target = DateUtils.parseDate("Sun, 06 Nov 1994 08:49:37 GMT");
+        Assert.assertEquals(target, impl.getWarnDate());
+    }
+
+}

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java
------------------------------------------------------------------------------
    svn:keywords = Date Revision

Propchange: httpcomponents/httpclient/trunk/httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain