Return-Path: Delivered-To: apmail-hc-commits-archive@www.apache.org Received: (qmail 99829 invoked from network); 6 Sep 2010 19:57:17 -0000 Received: from unknown (HELO mail.apache.org) (140.211.11.3) by 140.211.11.9 with SMTP; 6 Sep 2010 19:57:17 -0000 Received: (qmail 10906 invoked by uid 500); 6 Sep 2010 19:57:16 -0000 Delivered-To: apmail-hc-commits-archive@hc.apache.org Received: (qmail 10864 invoked by uid 500); 6 Sep 2010 19:57:16 -0000 Mailing-List: contact commits-help@hc.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: "HttpComponents Project" Delivered-To: mailing list commits@hc.apache.org Received: (qmail 10845 invoked by uid 99); 6 Sep 2010 19:57:16 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 06 Sep 2010 19:57:16 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Mon, 06 Sep 2010 19:57:13 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id 475F823889E9; Mon, 6 Sep 2010 19:56:53 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: 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/ Date: Mon, 06 Sep 2010 19:56:53 -0000 To: commits@hc.apache.org From: olegk@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20100906195653.475F823889E9@eris.apache.org> 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 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
newWarningHeaders = new ArrayList
(); + 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 + * . + * + */ +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 WarnValue objects + */ + public static WarningValue[] getWarningValues(Header h) { + List out = new ArrayList(); + 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 = + */ + private boolean isChar(char c) { + int i = (int)c; + return (i >= 0 && i <= 127); + } + + /* + * CTL = + */ + 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* + */ + 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 = > + */ + 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 int + */ + 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: + *
+     *   Warning: 110 fred "Response is stale"
+     * 
+ * then this method will return "\"Response is stale\"" + * (surrounding quotes included). + * @return {@link String} + */ + public String getWarnText() { return warnText; } + + /** Returns the date and time when this warning was added, or + * null if a warning date was not supplied in the + * header. + * @return {@link Date} + */ + public Date getWarnDate() { return warnDate; } + + /** Formats a WarningValue as a {@link String} + * suitable for including in a header. For example, you can: + *
+     *   WarningValue wv = ...;
+     *   HttpResponse resp = ...;
+     *   resp.addHeader("Warning", wv.toString());
+     * 
+ * @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 cap = new Capture(); @@ -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 + * . + * + */ +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