### Eclipse Workspace Patch 1.0 #P httpcomponents-client Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java (revision 0) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestWarningValue.java (revision 0) @@ -0,0 +1,205 @@ +package org.apache.http.impl.client.cache; + +import static org.junit.Assert.*; + +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.Test; + + +public class TestWarningValue { + + @Test + public void testParseSingleWarnValue() { + Header h = new BasicHeader("Warning","110 fred \"stale\""); + WarningValue[] result = WarningValue.getWarningValues(h); + assertEquals(1, result.length); + WarningValue wv = result[0]; + assertEquals(110, wv.getWarnCode()); + assertEquals("fred", wv.getWarnAgent()); + assertEquals("\"stale\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + } + + @Test + public void testParseMultipleWarnValues() { + Header h = new BasicHeader("Warning","110 fred \"stale\", 111 wilma \"other\""); + WarningValue[] result = WarningValue.getWarningValues(h); + assertEquals(2, result.length); + WarningValue wv = result[0]; + assertEquals(110, wv.getWarnCode()); + assertEquals("fred", wv.getWarnAgent()); + assertEquals("\"stale\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + wv = result[1]; + assertEquals(111, wv.getWarnCode()); + assertEquals("wilma", wv.getWarnAgent()); + assertEquals("\"other\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + } + + @Test + public void testMidHeaderParseErrorRecovery() { + Header h = new BasicHeader("Warning","110 fred \"stale\", bogus, 111 wilma \"other\""); + WarningValue[] result = WarningValue.getWarningValues(h); + assertEquals(2, result.length); + WarningValue wv = result[0]; + assertEquals(110, wv.getWarnCode()); + assertEquals("fred", wv.getWarnAgent()); + assertEquals("\"stale\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + wv = result[1]; + assertEquals(111, wv.getWarnCode()); + assertEquals("wilma", wv.getWarnAgent()); + assertEquals("\"other\"", wv.getWarnText()); + 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); + assertEquals(2, result.length); + WarningValue wv = result[0]; + assertEquals(110, wv.getWarnCode()); + assertEquals("fred", wv.getWarnAgent()); + assertEquals("\"stale\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + wv = result[1]; + assertEquals(111, wv.getWarnCode()); + assertEquals("wilma", wv.getWarnAgent()); + assertEquals("\"other\"", wv.getWarnText()); + 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); + assertEquals(2, result.length); + WarningValue wv = result[0]; + assertEquals(110, wv.getWarnCode()); + assertEquals("fred", wv.getWarnAgent()); + assertEquals("\"stale\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + wv = result[1]; + assertEquals(111, wv.getWarnCode()); + assertEquals("wilma", wv.getWarnAgent()); + assertEquals("\"other\"", wv.getWarnText()); + assertNull(wv.getWarnDate()); + } + + @Test + public void testConstructSingleWarnValue() { + WarningValue impl = new WarningValue("110 fred \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithIPv4Address() { + WarningValue impl = new WarningValue("110 192.168.1.1 \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("192.168.1.1", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithHostname() { + WarningValue impl = new WarningValue("110 foo.example.com \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("foo.example.com", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithHostnameAndPort() { + WarningValue impl = new WarningValue("110 foo.example.com:8080 \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("foo.example.com:8080", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithIPv4AddressAndPort() { + WarningValue impl = new WarningValue("110 192.168.1.1:8080 \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("192.168.1.1:8080", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithPseudonym() { + WarningValue impl = new WarningValue("110 ca$hm0ney \"stale\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("ca$hm0ney", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithTextWithSpaces() { + WarningValue impl = new WarningValue("110 fred \"stale stuff\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale stuff\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithTextWithCommas() { + WarningValue impl = new WarningValue("110 fred \"stale, stuff\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale, stuff\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithTextWithEscapedQuotes() { + WarningValue impl = new WarningValue("110 fred \"stale\\\" stuff\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale\\\" stuff\"", impl.getWarnText()); + assertNull(impl.getWarnDate()); + } + + @Test + public void testConstructWarnValueWithAscTimeWarnDate() throws Exception { + WarningValue impl = new WarningValue("110 fred \"stale\" \"Sun Nov 6 08:49:37 1994\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + Date target = DateUtils.parseDate("Sun Nov 6 08:49:37 1994"); + 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\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + Date target = DateUtils.parseDate("Sunday, 06-Nov-94 08:49:37 GMT"); + 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\""); + assertEquals(110, impl.getWarnCode()); + assertEquals("fred", impl.getWarnAgent()); + assertEquals("\"stale\"", impl.getWarnText()); + Date target = DateUtils.parseDate("Sun, 06 Nov 1994 08:49:37 GMT"); + assertEquals(target, impl.getWarnDate()); + } +} Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java (revision 0) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/WarningValue.java (revision 0) @@ -0,0 +1,333 @@ +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. + * @author jmoore + * + */ +public 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); + } + } +} Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (revision 992312) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestProtocolRequirements.java (working copy) @@ -2291,6 +2291,12 @@ * 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 @@ * 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 @@ * 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() @@ -5474,84 +5491,84 @@ */ @Test public void testProperlyFormattedViaHeaderIsAddedToRequests() throws Exception { - Capture cap = new Capture(); - request.removeHeaders("Via"); - EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), - EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) - .andReturn(originResponse); - - replayMocks(); - impl.execute(host, request); - verifyMocks(); - - HttpRequest captured = cap.getValue(); - String via = captured.getFirstHeader("Via").getValue(); - assertValidViaHeader(via); + Capture cap = new Capture(); + request.removeHeaders("Via"); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) + .andReturn(originResponse); + + replayMocks(); + impl.execute(host, request); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + String via = captured.getFirstHeader("Via").getValue(); + assertValidViaHeader(via); } - + @Test public void testProperlyFormattedViaHeaderIsAddedToResponses() throws Exception { - originResponse.removeHeaders("Via"); - backendExpectsAnyRequest().andReturn(originResponse); - replayMocks(); - HttpResponse result = impl.execute(host, request); - verifyMocks(); - assertValidViaHeader(result.getFirstHeader("Via").getValue()); + originResponse.removeHeaders("Via"); + backendExpectsAnyRequest().andReturn(originResponse); + replayMocks(); + HttpResponse result = impl.execute(host, request); + verifyMocks(); + assertValidViaHeader(result.getFirstHeader("Via").getValue()); } - - + + private void assertValidViaHeader(String via) { - // Via = "Via" ":" 1#( received-protocol received-by [ comment ] ) - // received-protocol = [ protocol-name "/" ] protocol-version - // protocol-name = token - // protocol-version = token - // received-by = ( host [ ":" port ] ) | pseudonym - // pseudonym = token - - String[] parts = via.split("\\s+"); - Assert.assertTrue(parts.length >= 2); - - // received protocol - String receivedProtocol = parts[0]; - String[] protocolParts = receivedProtocol.split("/"); - Assert.assertTrue(protocolParts.length >= 1); - Assert.assertTrue(protocolParts.length <= 2); - - final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+"; - for(String protocolPart : protocolParts) { - Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart)); - } - - // received-by - if (!Pattern.matches(tokenRegexp, parts[1])) { - // host : port - new HttpHost(parts[1]); - } - - // comment - if (parts.length > 2) { - StringBuilder buf = new StringBuilder(parts[2]); - for(int i=3; i= 2); + + // received protocol + String receivedProtocol = parts[0]; + String[] protocolParts = receivedProtocol.split("/"); + Assert.assertTrue(protocolParts.length >= 1); + Assert.assertTrue(protocolParts.length <= 2); + + final String tokenRegexp = "[^\\p{Cntrl}()<>@,;:\\\\\"/\\[\\]?={} \\t]+"; + for(String protocolPart : protocolParts) { + Assert.assertTrue(Pattern.matches(tokenRegexp, protocolPart)); + } + + // received-by + if (!Pattern.matches(tokenRegexp, parts[1])) { + // host : port + new HttpHost(parts[1]); + } + + // comment + if (parts.length > 2) { + StringBuilder buf = new StringBuilder(parts[2]); + for(int i=3; i cap = new Capture(); - EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), - EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) - .andReturn(originResponse); - - replayMocks(); - impl.execute(host, request); - verifyMocks(); - - HttpRequest captured = cap.getValue(); - String via = captured.getFirstHeader("Via").getValue(); - String protocol = via.split("\\s+")[0]; - String[] protoParts = protocol.split("/"); - if (protoParts.length > 1) { - Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); - } - Assert.assertEquals("1.0",protoParts[protoParts.length-1]); + throws Exception { + request = new BasicHttpRequest("GET", "/", HttpVersion.HTTP_1_0); + request.removeHeaders("Via"); + Capture cap = new Capture(); + EasyMock.expect(mockBackend.execute(EasyMock.isA(HttpHost.class), + EasyMock.capture(cap), (HttpContext)EasyMock.isNull())) + .andReturn(originResponse); + + replayMocks(); + impl.execute(host, request); + verifyMocks(); + + HttpRequest captured = cap.getValue(); + String via = captured.getFirstHeader("Via").getValue(); + String protocol = via.split("\\s+")[0]; + String[] protoParts = protocol.split("/"); + if (protoParts.length > 1) { + Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); + } + Assert.assertEquals("1.0",protoParts[protoParts.length-1]); } - + @Test public void testViaHeaderOnResponseProperlyRecordsOriginProtocol() - throws Exception { + throws Exception { + + originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content"); + + backendExpectsAnyRequest().andReturn(originResponse); + + replayMocks(); + HttpResponse result = impl.execute(host, request); + verifyMocks(); + + String via = result.getFirstHeader("Via").getValue(); + String protocol = via.split("\\s+")[0]; + String[] protoParts = protocol.split("/"); + Assert.assertTrue(protoParts.length >= 1); + Assert.assertTrue(protoParts.length <= 2); + if (protoParts.length > 1) { + Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); + } + Assert.assertEquals("1.0", protoParts[protoParts.length - 1]); + } - originResponse = new BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_NO_CONTENT, "No Content"); + /* "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); - backendExpectsAnyRequest().andReturn(originResponse); + replayMocks(); + HttpResponse result = impl.execute(host, request); + verifyMocks(); - replayMocks(); - HttpResponse result = impl.execute(host, request); - verifyMocks(); + Header[] warningHeaders = result.getHeaders("Warning"); + Assert.assertTrue(warningHeaders == null || warningHeaders.length == 0); + } - String via = result.getFirstHeader("Via").getValue(); - String protocol = via.split("\\s+")[0]; - String[] protoParts = protocol.split("/"); - Assert.assertTrue(protoParts.length >= 1); - Assert.assertTrue(protoParts.length <= 2); - if (protoParts.length > 1) { - Assert.assertTrue("http".equalsIgnoreCase(protoParts[0])); - } - Assert.assertEquals("1.0", protoParts[protoParts.length - 1]); - } } \ No newline at end of file Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java (revision 992312) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/CacheEntryUpdater.java (working copy) @@ -107,9 +107,8 @@ } 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()]); } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java (revision 992312) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ResponseProtocolCompliance.java (working copy) @@ -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.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,9 +82,41 @@ ensure200ForOPTIONSRequestWithNoBodyHasContentLengthZero(request, response); ensure206ContainsDateHeader(response); + + warningsWithNonMatchingWarnDatesAreRemoved(response); } - private void authenticationRequiredDidNotHaveAProxyAuthenticationHeader(HttpRequest request, + 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, HttpResponse response) throws ClientProtocolException { if (response.getStatusLine().getStatusCode() != HttpStatus.SC_PROXY_AUTHENTICATION_REQUIRED) return;