diff --git a/.gitattributes b/.gitattributes index 1e21b19d79..cd443a131f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -21,3 +21,4 @@ *.html text diff=html *.css text *.js text +*.serialized binary diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java new file mode 100644 index 0000000000..d5b77fd235 --- /dev/null +++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java @@ -0,0 +1,409 @@ +/* + * ==================================================================== + * 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.hc.client5.http.impl.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer; +import org.apache.hc.client5.http.cache.HttpCacheStorageEntry; +import org.apache.hc.client5.http.cache.Resource; +import org.apache.hc.client5.http.cache.ResourceIOException; +import org.apache.hc.core5.annotation.Experimental; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpVersion; +import org.apache.hc.core5.http.ProtocolVersion; +import org.apache.hc.core5.http.impl.io.AbstractMessageParser; +import org.apache.hc.core5.http.impl.io.AbstractMessageWriter; +import org.apache.hc.core5.http.impl.io.DefaultHttpResponseParser; +import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl; +import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl; +import org.apache.hc.core5.http.io.SessionInputBuffer; +import org.apache.hc.core5.http.io.SessionOutputBuffer; +import org.apache.hc.core5.http.message.BasicHttpRequest; +import org.apache.hc.core5.http.message.BasicLineFormatter; +import org.apache.hc.core5.http.message.StatusLine; +import org.apache.hc.core5.util.CharArrayBuffer; + +/** + * Cache serializer and deserializer that uses an HTTP-like format. + * + * Existing libraries for reading and writing HTTP are used, and metadata is encoded into HTTP + * pseudo-headers for storage. + */ +@Experimental +public class HttpByteArrayCacheEntrySerializer implements HttpCacheEntrySerializer { + public static final HttpByteArrayCacheEntrySerializer INSTANCE = new HttpByteArrayCacheEntrySerializer(); + + private static final String SC_CACHE_ENTRY_PREFIX = "hc-"; + + private static final String SC_HEADER_NAME_STORAGE_KEY = SC_CACHE_ENTRY_PREFIX + "sk"; + private static final String SC_HEADER_NAME_RESPONSE_DATE = SC_CACHE_ENTRY_PREFIX + "resp-date"; + private static final String SC_HEADER_NAME_REQUEST_DATE = SC_CACHE_ENTRY_PREFIX + "req-date"; + private static final String SC_HEADER_NAME_NO_CONTENT = SC_CACHE_ENTRY_PREFIX + "no-content"; + private static final String SC_HEADER_NAME_VARIANT_MAP_KEY = SC_CACHE_ENTRY_PREFIX + "varmap-key"; + private static final String SC_HEADER_NAME_VARIANT_MAP_VALUE = SC_CACHE_ENTRY_PREFIX + "varmap-val"; + + private static final String SC_CACHE_ENTRY_PRESERVE_PREFIX = SC_CACHE_ENTRY_PREFIX + "esc-"; + + private static final int BUFFER_SIZE = 8192; + + public HttpByteArrayCacheEntrySerializer() { + } + + @Override + public byte[] serialize(final HttpCacheStorageEntry httpCacheEntry) throws ResourceIOException { + if (httpCacheEntry.getKey() == null) { + throw new IllegalStateException("Cannot serialize cache object with null storage key"); + } + // content doesn't need null-check because it's validated in the HttpCacheStorageEntry constructor + + // Fake HTTP request, required by response generator + // Use request method from httpCacheEntry, but as far as I can tell it will only ever return "GET". + final HttpRequest httpRequest = new BasicHttpRequest(httpCacheEntry.getContent().getRequestMethod(), "/"); + + final CacheValidityPolicy cacheValidityPolicy = new NoAgeCacheValidityPolicy(); + final CachedHttpResponseGenerator cachedHttpResponseGenerator = new CachedHttpResponseGenerator(cacheValidityPolicy); + + final SimpleHttpResponse httpResponse = cachedHttpResponseGenerator.generateResponse(httpRequest, httpCacheEntry.getContent()); + + try(final ByteArrayOutputStream out = new ByteArrayOutputStream()) { + escapeHeaders(httpResponse); + addMetadataPseudoHeaders(httpResponse, httpCacheEntry); + + final byte[] bodyBytes = httpResponse.getBodyBytes(); + final int resourceLength; + + if (bodyBytes == null) { + // This means no content, for example a 204 response + httpResponse.addHeader(SC_HEADER_NAME_NO_CONTENT, Boolean.TRUE.toString()); + resourceLength = 0; + } else { + resourceLength = bodyBytes.length; + } + + // Use the default, ASCII-only encoder for HTTP protocol and header values. + // It's the only thing that's widely used, and it's not worth it to support anything else. + final SessionOutputBufferImpl outputBuffer = new SessionOutputBufferImpl(BUFFER_SIZE); + final AbstractMessageWriter httpResponseWriter = makeHttpResponseWriter(outputBuffer); + httpResponseWriter.write(httpResponse, outputBuffer, out); + outputBuffer.flush(out); + final byte[] headerBytes = out.toByteArray(); + + final byte[] bytes = new byte[headerBytes.length + resourceLength]; + System.arraycopy(headerBytes, 0, bytes, 0, headerBytes.length); + if (resourceLength > 0) { + System.arraycopy(bodyBytes, 0, bytes, headerBytes.length, resourceLength); + } + return bytes; + } catch(final IOException|HttpException e) { + throw new ResourceIOException("Exception while serializing cache entry", e); + } + } + + @Override + public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws ResourceIOException { + try (final InputStream in = makeByteArrayInputStream(serializedObject); + final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(serializedObject.length) // this is bigger than necessary but will save us from reallocating + ) { + final SessionInputBufferImpl inputBuffer = new SessionInputBufferImpl(BUFFER_SIZE); + final AbstractMessageParser responseParser = makeHttpResponseParser(); + final ClassicHttpResponse response = responseParser.parse(inputBuffer, in); + + // Extract metadata pseudo-headers + final String storageKey = getCachePseudoHeaderAndRemove(response, SC_HEADER_NAME_STORAGE_KEY); + final Date requestDate = getCachePseudoHeaderDateAndRemove(response, SC_HEADER_NAME_REQUEST_DATE); + final Date responseDate = getCachePseudoHeaderDateAndRemove(response, SC_HEADER_NAME_RESPONSE_DATE); + final boolean noBody = getCachePseudoHeaderBooleanAndRemove(response, SC_HEADER_NAME_NO_CONTENT); + final Map variantMap = getVariantMapPseudoHeadersAndRemove(response); + unescapeHeaders(response); + + final Resource resource; + if (noBody) { + // This means no content, for example a 204 response + resource = null; + } else { + copyBytes(inputBuffer, in, bytesOut); + resource = new HeapResource(bytesOut.toByteArray()); + } + + final HttpCacheEntry httpCacheEntry = new HttpCacheEntry( + requestDate, + responseDate, + response.getCode(), + response.getHeaders(), + resource, + variantMap + ); + + return new HttpCacheStorageEntry(storageKey, httpCacheEntry); + } catch (final IOException|HttpException e) { + throw new ResourceIOException("Error deserializing cache entry", e); + } + } + + /** + * Helper method to make a new HTTP response writer. + *

+ * Useful to override for testing. + */ + protected AbstractMessageWriter makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) { + return new SimpleHttpResponseWriter(); + } + + /** + * Helper method to make a new ByteArrayInputStream. + *

+ * Useful to override for testing. + */ + protected InputStream makeByteArrayInputStream(final byte[] bytes) { + return new ByteArrayInputStream(bytes); + } + + /** + * Helper method to make a new HTTP Response parser. + *

+ * Useful to override for testing. + */ + protected AbstractMessageParser makeHttpResponseParser() { + return new DefaultHttpResponseParser(); + } + + /** + * Modify the given response to escape any header names that start with the prefix we use for our own pseudo-headers, + * prefixing them with an escape sequence we can use to recover them later. + * + * @param httpResponse HTTP response object to escape headers in + * @see #unescapeHeaders(HttpResponse) for the corresponding un-escaper. + */ + private static void escapeHeaders(final HttpResponse httpResponse) { + final Header[] headers = httpResponse.getHeaders(); + for (final Header header : headers) { + if (header.getName().startsWith(SC_CACHE_ENTRY_PREFIX)) { + httpResponse.removeHeader(header); + httpResponse.addHeader(SC_CACHE_ENTRY_PRESERVE_PREFIX + header.getName(), header.getValue()); + } + } + } + + /** + * Modify the given response to remove escaping from any header names we escaped before saving. + * + * @param httpResponse HTTP response object to un-escape headers in + * @see #unescapeHeaders(HttpResponse) for the corresponding escaper + */ + private void unescapeHeaders(final HttpResponse httpResponse) { + final Header[] headers = httpResponse.getHeaders(); + for (final Header header : headers) { + if (header.getName().startsWith(SC_CACHE_ENTRY_PRESERVE_PREFIX)) { + httpResponse.removeHeader(header); + httpResponse.addHeader(header.getName().substring(SC_CACHE_ENTRY_PRESERVE_PREFIX.length()), header.getValue()); + } + } + } + + /** + * Modify the given response to add our own cache metadata as pseudo-headers. + * + * @param httpResponse HTTP response object to add pseudo-headers to + */ + private void addMetadataPseudoHeaders(final HttpResponse httpResponse, final HttpCacheStorageEntry httpCacheEntry) { + httpResponse.addHeader(SC_HEADER_NAME_STORAGE_KEY, httpCacheEntry.getKey()); + httpResponse.addHeader(SC_HEADER_NAME_RESPONSE_DATE, Long.toString(httpCacheEntry.getContent().getResponseDate().getTime())); + httpResponse.addHeader(SC_HEADER_NAME_REQUEST_DATE, Long.toString(httpCacheEntry.getContent().getRequestDate().getTime())); + + // Encode these so map entries are stored in a pair of headers, one for key and one for value. + // Header keys look like: {Accept-Encoding=gzip} + // And header values like: {Accept-Encoding=gzip}https://example.com:1234/foo + for (final Map.Entry entry : httpCacheEntry.getContent().getVariantMap().entrySet()) { + // Headers are ordered + httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_KEY, entry.getKey()); + httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_VALUE, entry.getValue()); + } + } + + /** + * Get the string value for a single metadata pseudo-header, and remove it from the response object. + * + * @param response Response object to get and remove the pseudo-header from + * @param name Name of metadata pseudo-header + * @return Value for metadata pseudo-header + * @throws ResourceIOException if the given pseudo-header is not found + */ + private static String getCachePseudoHeaderAndRemove(final HttpResponse response, final String name) throws ResourceIOException { + final String headerValue = getOptionalCachePseudoHeaderAndRemove(response, name); + if (headerValue == null) { + throw new ResourceIOException("Expected cache header '" + name + "' not found"); + } + return headerValue; + } + + /** + * Get the string value for a single metadata pseudo-header if it exists, and remove it from the response object. + * + * @param response Response object to get and remove the pseudo-header from + * @param name Name of metadata pseudo-header + * @return Value for metadata pseudo-header, or null if it does not exist + */ + private static String getOptionalCachePseudoHeaderAndRemove(final HttpResponse response, final String name) { + final Header header = response.getFirstHeader(name); + if (header == null) { + return null; + } + response.removeHeader(header); + return header.getValue(); + } + + /** + * Get the date value for a single metadata pseudo-header, and remove it from the response object. + * + * @param response Response object to get and remove the pseudo-header from + * @param name Name of metadata pseudo-header + * @return Value for metadata pseudo-header + * @throws ResourceIOException if the given pseudo-header is not found, or contains invalid data + */ + private static Date getCachePseudoHeaderDateAndRemove(final HttpResponse response, final String name) throws ResourceIOException{ + final String value = getCachePseudoHeaderAndRemove(response, name); + response.removeHeaders(name); + try { + final long timestamp = Long.parseLong(value); + return new Date(timestamp); + } catch (final NumberFormatException e) { + throw new ResourceIOException("Invalid value for header '" + name + "'", e); + } + } + + /** + * Get the boolean value for a single metadata pseudo-header, and remove it from the response object. + * + * @param response Response object to get and remove the pseudo-header from + * @param name Name of metadata pseudo-header + * @return Value for metadata pseudo-header + */ + private static boolean getCachePseudoHeaderBooleanAndRemove(final ClassicHttpResponse response, final String name) { + // parseBoolean does not throw any exceptions, so no try/catch required. + return Boolean.parseBoolean(getOptionalCachePseudoHeaderAndRemove(response, name)); + } + + + /** + * Get the variant map metadata pseudo-header, and remove it from the response object. + * + * @param response Response object to get and remove the pseudo-header from + * @return Extracted variant map + * @throws ResourceIOException if the given pseudo-header is not found, or contains invalid data + */ + private static Map getVariantMapPseudoHeadersAndRemove(final HttpResponse response) throws ResourceIOException { + final Header[] headers = response.getHeaders(); + final Map variantMap = new HashMap<>(0); + String lastKey = null; + for (final Header header : headers) { + if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_KEY)) { + lastKey = header.getValue(); + response.removeHeader(header); + } else if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_VALUE)) { + if (lastKey == null) { + throw new ResourceIOException("Found mismatched variant map key/value headers"); + } + variantMap.put(lastKey, header.getValue()); + lastKey = null; + response.removeHeader(header); + } + } + + if (lastKey != null) { + throw new ResourceIOException("Found mismatched variant map key/value headers"); + } + + return variantMap; + } + + /** + * Copy bytes from the given source buffer and input stream to the given output stream until end-of-file is reached. + * + * @param srcBuf Buffered input source + * @param src Unbuffered input source + * @param dest Output destination + * @throws IOException if an I/O error occurs + */ + private static void copyBytes(final SessionInputBuffer srcBuf, final InputStream src, final OutputStream dest) throws IOException { + final byte[] buf = new byte[BUFFER_SIZE]; + int lastBytesRead; + while ((lastBytesRead = srcBuf.read(buf, src)) != -1) { + dest.write(buf, 0, lastBytesRead); + } + } + + /** + * Writer for SimpleHttpResponse. + * + * Copied from DefaultHttpResponseWriter, but wrapping a SimpleHttpResponse instead of a ClassicHttpResponse + */ + // Seems like the DefaultHttpResponseWriter should be able to do this, but it doesn't seem to be able to + private class SimpleHttpResponseWriter extends AbstractMessageWriter { + + public SimpleHttpResponseWriter() { + super(BasicLineFormatter.INSTANCE); + } + + @Override + protected void writeHeadLine( + final SimpleHttpResponse message, final CharArrayBuffer lineBuf) { + final ProtocolVersion transportVersion = message.getVersion(); + BasicLineFormatter.INSTANCE.formatStatusLine(lineBuf, new StatusLine( + transportVersion != null ? transportVersion : HttpVersion.HTTP_1_1, + message.getCode(), + message.getReasonPhrase())); + } + } + + /** + * Cache validity policy that always returns an age of 0. + * + * This prevents the Age header from being written to the cache (it does not make sense to cache it), + * and is the only thing the policy is used for in this case. + */ + private class NoAgeCacheValidityPolicy extends CacheValidityPolicy { + @Override + public long getCurrentAgeSecs(final HttpCacheEntry entry, final Date now) { + return 0L; + } + } +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java new file mode 100644 index 0000000000..2b71e84234 --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java @@ -0,0 +1,342 @@ +/* + * ==================================================================== + * 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.hc.client5.http.impl.cache; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Date; +import java.util.Map; + +import org.apache.hc.client5.http.cache.HttpCacheEntry; +import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer; +import org.apache.hc.client5.http.cache.HttpCacheStorageEntry; +import org.apache.hc.client5.http.cache.Resource; +import org.apache.hc.client5.http.cache.ResourceIOException; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.message.BasicHeader; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +class HttpByteArrayCacheEntrySerializerTestUtils { + private final static String TEST_RESOURCE_DIR = "src/test/resources/"; + static final String TEST_STORAGE_KEY = "xyzzy"; + + /** + * Template for incrementally building a new HttpCacheStorageEntry test object, starting from defaults. + */ + static class HttpCacheStorageEntryTestTemplate { + Resource resource; + Date requestDate; + Date responseDate; + int responseCode; + Header[] responseHeaders; + Map variantMap; + String storageKey; + + /** + * Return a new HttpCacheStorageEntryTestTemplate instance with all default values. + * + * @return new HttpCacheStorageEntryTestTemplate instance + */ + static HttpCacheStorageEntryTestTemplate makeDefault() { + return new HttpCacheStorageEntryTestTemplate(DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE); + } + + /** + * Convert this template to a HttpCacheStorageEntry object. + * @return HttpCacheStorageEntry object + */ + HttpCacheStorageEntry toEntry() { + return new HttpCacheStorageEntry(storageKey, + new HttpCacheEntry( + requestDate, + responseDate, + responseCode, + responseHeaders, + resource, + variantMap)); + } + + /** + * Create a new template with all null values. + */ + private HttpCacheStorageEntryTestTemplate() { + } + + /** + * Create a new template values copied from the given template + * + * @param src Template to copy values from + */ + private HttpCacheStorageEntryTestTemplate(final HttpCacheStorageEntryTestTemplate src) { + this.resource = src.resource; + this.requestDate = src.requestDate; + this.responseDate = src.responseDate; + this.responseCode = src.responseCode; + this.responseHeaders = src.responseHeaders; + this.variantMap = src.variantMap; + this.storageKey = src.storageKey; + } + } + + /** + * Template with all default values. + * + * Used by HttpCacheStorageEntryTestTemplate#makeDefault() + */ + private static final HttpCacheStorageEntryTestTemplate DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE = new HttpCacheStorageEntryTestTemplate(); + static { + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.resource = new HeapResource("Hello World".getBytes(StandardCharsets.UTF_8)); + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.requestDate = new Date(165214800000L); + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseDate = new Date(2611108800000L); + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseCode = 200; + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseHeaders = new Header[]{ + new BasicHeader("Content-type", "text/html"), + new BasicHeader("Cache-control", "public, max-age=31536000"), + }; + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.variantMap = Collections.emptyMap(); + DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.storageKey = TEST_STORAGE_KEY; + } + + /** + * Test serializing and deserializing the given object with the given factory. + *

+ * Compares fields to ensure the deserialized object is equivalent to the original object. + * + * @param serializer Factory for creating serializers + * @param httpCacheStorageEntry Original object to serialize and test against + * @throws Exception if anything goes wrong + */ + static void testWithCache(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry) throws Exception { + final byte[] testBytes = serializer.serialize(httpCacheStorageEntry); + verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, testBytes); + } + + /** + * Verify that the given bytes deserialize to the given storage key and an equivalent cache entry. + * + * @param serializer Deserializer + * @param httpCacheStorageEntry Cache entry to verify + * @param testBytes Bytes to deserialize + * @throws Exception if anything goes wrong + */ + static void verifyHttpCacheEntryFromBytes(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final byte[] testBytes) throws Exception { + final HttpCacheStorageEntry testEntry = httpCacheStorageEntryFromBytes(serializer, testBytes); + + assertCacheEntriesEqual(httpCacheStorageEntry, testEntry); + } + + /** + * Verify that the given test file deserializes to a cache entry equivalent to the one given. + * + * @param serializer Deserializer + * @param httpCacheStorageEntry Cache entry to verify + * @param testFileName Name of test file to deserialize + * @param reserializeFiles If true, test files will be regenerated and saved to disk + * @throws Exception if anything goes wrong + */ + static void verifyHttpCacheEntryFromTestFile(final HttpCacheEntrySerializer serializer, + final HttpCacheStorageEntry httpCacheStorageEntry, + final String testFileName, + final boolean reserializeFiles) throws Exception { + if (reserializeFiles) { + final File toFile = makeTestFileObject(testFileName); + saveEntryToFile(serializer, httpCacheStorageEntry, toFile); + } + + final byte[] bytes = readTestFileBytes(testFileName); + + verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, bytes); + } + + /** + * Get the bytes of the given test file. + * + * @param testFileName Name of test file to get bytes from + * @return Bytes from the given test file + * @throws Exception if anything goes wrong + */ + static byte[] readTestFileBytes(final String testFileName) throws Exception { + final File testFile = makeTestFileObject(testFileName); + try(final FileInputStream testStream = new FileInputStream(testFile)) { + return readFullyStrict(testStream, testFile.length()); + } + } + + /** + * Create a new cache object from the given bytes. + * + * @param serializer Deserializer + * @param testBytes Bytes to deserialize + * @return Deserialized object + */ + static HttpCacheStorageEntry httpCacheStorageEntryFromBytes(final HttpCacheEntrySerializer serializer, final byte[] testBytes) throws ResourceIOException { + return serializer.deserialize(testBytes); + } + + /** + * Assert that the given objects are equivalent + * + * @param expected Expected cache entry object + * @param actual Actual cache entry object + * @throws Exception if anything goes wrong + */ + static void assertCacheEntriesEqual(final HttpCacheStorageEntry expected, final HttpCacheStorageEntry actual) throws Exception { + assertEquals(expected.getKey(), actual.getKey()); + + final HttpCacheEntry expectedContent = expected.getContent(); + final HttpCacheEntry actualContent = actual.getContent(); + + assertEquals(expectedContent.getRequestDate(), actualContent.getRequestDate()); + assertEquals(expectedContent.getResponseDate(), actualContent.getResponseDate()); + assertEquals(expectedContent.getStatus(), actualContent.getStatus()); + + assertArrayEquals(expectedContent.getVariantMap().keySet().toArray(), actualContent.getVariantMap().keySet().toArray()); + for (final String key : expectedContent.getVariantMap().keySet()) { + assertEquals("Expected same variantMap values for key '" + key + "'", + expectedContent.getVariantMap().get(key), actualContent.getVariantMap().get(key)); + } + + // Verify that the same headers are present on the expected and actual content. + for(final Header expectedHeader: expectedContent.getHeaders()) { + final Header actualHeader = actualContent.getFirstHeader(expectedHeader.getName()); + + if (actualHeader == null) { + if (expectedHeader.getName().equalsIgnoreCase("content-length")) { + // This header is added by the cache implementation, and can be safely ignored + } else { + fail("Expected header " + expectedHeader.getName() + " was not found"); + } + } else { + assertEquals(expectedHeader.getName(), actualHeader.getName()); + assertEquals(expectedHeader.getValue(), actualHeader.getValue()); + } + } + + if (expectedContent.getResource() == null) { + assertNull("Expected null resource", actualContent.getResource()); + } else { + final byte[] expectedBytes = readFullyStrict( + expectedContent.getResource().getInputStream(), + (int) expectedContent.getResource().length() + ); + final byte[] actualBytes = readFullyStrict( + actualContent.getResource().getInputStream(), + (int) actualContent.getResource().length() + ); + assertArrayEquals(expectedBytes, actualBytes); + } + } + + /** + * Get a File object for the given test file. + * + * @param testFileName Name of test file + * @return File for this test file + */ + static File makeTestFileObject(final String testFileName) { + return new File(TEST_RESOURCE_DIR + testFileName); + } + + /** + * Save the given cache entry serialized to the given file. + * + * @param serializer Serializer + * @param httpCacheStorageEntry Cache entry to serialize and save + * @param outFile Output file to write to + * @throws Exception if anything goes wrong + */ + static void saveEntryToFile(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final File outFile) throws Exception { + final byte[] bytes = serializer.serialize(httpCacheStorageEntry); + + OutputStream out = null; + try { + out = new FileOutputStream(outFile); + out.write(bytes); + } finally { + if (out != null) { + out.close(); + } + } + } + + /** + * Copy bytes from the given input stream to the given destination buffer until the buffer is full, + * or end-of-file is reached, and return the number of bytes read. + * + * @param src Input stream to read from + * @param dest Output buffer to write to + * @return Number of bytes read + * @throws IOException if an I/O error occurs + */ + private static int readFully(final InputStream src, final byte[] dest) throws IOException { + final int destPos = 0; + final int length = dest.length; + int totalBytesRead = 0; + int lastBytesRead; + + while (totalBytesRead < length && (lastBytesRead = src.read(dest, destPos + totalBytesRead, length - totalBytesRead)) != -1) { + totalBytesRead += lastBytesRead; + } + return totalBytesRead; + } + + /** + * Copy bytes from the given input stream to a new buffer until the given length is reached, + * and returns the new buffer. If end-of-file is reached first, an IOException is thrown + * + * @param src Input stream to read from + * @param length Maximum bytes to read + * @return All bytes from file + * @throws IOException if an I/O error occurs or end-of-file is reached before the requested + * number of bytes have been read + */ + static byte[] readFullyStrict(final InputStream src, final long length) throws IOException { + if (length > Integer.MAX_VALUE) { + throw new IllegalArgumentException(String.format("Length %d is too large to fit in an array", length)); + } + final int intLength = (int) length; + final byte[] dest = new byte[intLength]; + final int bytesRead = readFully(src, dest); + + if (bytesRead == intLength) { + return dest; + } else { + throw new IOException(String.format("Expected to read %d bytes but only got %d", intLength, bytesRead)); + } + } +} diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java new file mode 100644 index 0000000000..4c333e7a0e --- /dev/null +++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java @@ -0,0 +1,397 @@ +/* + * ==================================================================== + * 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.hc.client5.http.impl.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashMap; +import java.util.Map; + +import org.apache.hc.client5.http.async.methods.SimpleHttpResponse; +import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer; +import org.apache.hc.client5.http.cache.HttpCacheStorageEntry; +import org.apache.hc.client5.http.cache.ResourceIOException; +import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.impl.io.AbstractMessageParser; +import org.apache.hc.core5.http.impl.io.AbstractMessageWriter; +import org.apache.hc.core5.http.io.SessionInputBuffer; +import org.apache.hc.core5.http.io.SessionOutputBuffer; +import org.apache.hc.core5.http.message.BasicHeader; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.makeTestFileObject; +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.httpCacheStorageEntryFromBytes; +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.readTestFileBytes; +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.testWithCache; +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.verifyHttpCacheEntryFromTestFile; +import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.HttpCacheStorageEntryTestTemplate; + +public class TestHttpByteArrayCacheEntrySerializer { + private static final String SERIALIAZED_EXTENSION = ".httpbytes.serialized"; + + private static final String FILE_TEST_SERIALIZED_NAME = "ApacheLogo" + SERIALIAZED_EXTENSION; + private static final String SIMPLE_OBJECT_SERIALIZED_NAME = "simpleObject" + SERIALIAZED_EXTENSION; + private static final String VARIANTMAP_TEST_SERIALIZED_NAME = "variantMap" + SERIALIAZED_EXTENSION; + private static final String ESCAPED_HEADER_TEST_SERIALIZED_NAME = "escapedHeader" + SERIALIAZED_EXTENSION; + private static final String NO_BODY_TEST_SERIALIZED_NAME = "noBody" + SERIALIAZED_EXTENSION; + private static final String MISSING_HEADER_TEST_SERIALIZED_NAME = "missingHeader" + SERIALIAZED_EXTENSION; + private static final String INVALID_HEADER_TEST_SERIALIZED_NAME = "invalidHeader" + SERIALIAZED_EXTENSION; + private static final String VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME = "variantMapMissingKey" + SERIALIAZED_EXTENSION; + private static final String VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME = "variantMapMissingValue" + SERIALIAZED_EXTENSION; + + private static final String TEST_CONTENT_FILE_NAME = "ApacheLogo.png"; + + private HttpCacheEntrySerializer serializer; + + // Manually set this to true to re-generate all of the serialized files + private final boolean reserializeFiles = false; + + @Before + public void before() { + serializer = HttpByteArrayCacheEntrySerializer.INSTANCE; + } + + /** + * Serialize and deserialize a simple object with a tiny body. + * + * @throws Exception if anything goes wrong + */ + @Test + public void simpleObjectTest() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Serialize and deserialize a larger object with a binary file for a body. + * + * @throws Exception if anything goes wrong + */ + @Test + public void fileObjectTest() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME)); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Serialize and deserialize a cache entry with no headers. + * + * @throws Exception if anything goes wrong + */ + @Test + public void noHeadersTest() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.responseHeaders = new Header[0]; + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Serialize and deserialize a cache entry with an empty body. + * + * @throws Exception if anything goes wrong + */ + @Test + public void emptyBodyTest() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.resource = new HeapResource(new byte[0]); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Serialize and deserialize a cache entry with no body. + * + * @throws Exception if anything goes wrong + */ + @Test + public void noBodyTest() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.resource = null; + cacheObjectValues.responseCode = 204; + + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Serialize and deserialize a cache entry with a variant map. + * + * @throws Exception if anything goes wrong + */ + @Test + public void testSimpleVariantMap() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + final Map variantMap = new HashMap<>(); + variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo"); + variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo"); + cacheObjectValues.variantMap = variantMap; + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Ensures that if the server uses our reserved header names we don't mix them up with our own pseudo-headers. + * + * @throws Exception if anything goes wrong + */ + @Test + public void testEscapedHeaders() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.responseHeaders = new Header[] { + new BasicHeader("hc-test-1", "hc-test-1-value"), + new BasicHeader("hc-sk", "hc-sk-value"), + new BasicHeader("hc-resp-date", "hc-resp-date-value"), + new BasicHeader("hc-req-date-date", "hc-req-date-value"), + new BasicHeader("hc-varmap-key", "hc-varmap-key-value"), + new BasicHeader("hc-varmap-val", "hc-varmap-val-value"), + }; + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + testWithCache(serializer, testEntry); + } + + /** + * Attempt to store a cache entry with a null storage key. + * + * @throws Exception is expected + */ + @Test(expected = IllegalStateException.class) + public void testNullStorageKey() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.storageKey = null; + + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + serializer.serialize(testEntry); + } + + /** + * Deserialize a simple object, from a previously saved file. + * + * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test. + * + * @throws Exception if anything goes wrong + */ + @Test + public void simpleTestFromPreviouslySerialized() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + verifyHttpCacheEntryFromTestFile(serializer, testEntry, SIMPLE_OBJECT_SERIALIZED_NAME, reserializeFiles); + } + + /** + * Deserialize a larger object with a binary body, from a previously saved file. + * + * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test. + * + * @throws Exception if anything goes wrong + */ + @Test + public void fileTestFromPreviouslySerialized() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME)); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + verifyHttpCacheEntryFromTestFile(serializer, testEntry, FILE_TEST_SERIALIZED_NAME, reserializeFiles); + } + + /** + * Deserialize a cache entry with a variant map, from a previously saved file. + * + * @throws Exception if anything goes wrong + */ + @Test + public void variantMapTestFromPreviouslySerialized() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + final Map variantMap = new HashMap<>(); + variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo"); + variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo"); + cacheObjectValues.variantMap = variantMap; + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + verifyHttpCacheEntryFromTestFile(serializer, testEntry, VARIANTMAP_TEST_SERIALIZED_NAME, reserializeFiles); + } + + /** + * Deserialize a cache entry with headers that use our pseudo-header prefix and need escaping. + * + * @throws Exception if anything goes wrong + */ + @Test + public void escapedHeaderTestFromPreviouslySerialized() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.responseHeaders = new Header[] { + new BasicHeader("hc-test-1", "hc-test-1-value"), + new BasicHeader("hc-sk", "hc-sk-value"), + new BasicHeader("hc-resp-date", "hc-resp-date-value"), + new BasicHeader("hc-req-date-date", "hc-req-date-value"), + new BasicHeader("hc-varmap-key", "hc-varmap-key-value"), + new BasicHeader("hc-varmap-val", "hc-varmap-val-value"), + }; + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + verifyHttpCacheEntryFromTestFile(serializer, testEntry, ESCAPED_HEADER_TEST_SERIALIZED_NAME, reserializeFiles); + } + + /** + * Deserialize a cache entry with no body, from a previously saved file. + * + * @throws Exception if anything goes wrong + */ + @Test + public void noBodyTestFromPreviouslySerialized() throws Exception { + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + cacheObjectValues.resource = null; + cacheObjectValues.responseCode = 204; + + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + verifyHttpCacheEntryFromTestFile(serializer, testEntry, NO_BODY_TEST_SERIALIZED_NAME, reserializeFiles); + } + + /** + * Deserialize a cache entry in a bad format, expecting an exception. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testInvalidCacheEntry() throws Exception { + // This file is a JPEG not a cache entry, so should fail to deserialize + final byte[] bytes = readTestFileBytes(TEST_CONTENT_FILE_NAME); + httpCacheStorageEntryFromBytes(serializer, bytes); + } + + /** + * Deserialize a cache entry with a missing header, from a previously saved file. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testMissingHeaderCacheEntry() throws Exception { + // This file hand-edited to be missing a necessary header + final byte[] bytes = readTestFileBytes(MISSING_HEADER_TEST_SERIALIZED_NAME); + httpCacheStorageEntryFromBytes(serializer, bytes); + } + + /** + * Deserialize a cache entry with an invalid header value, from a previously saved file. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testInvalidHeaderCacheEntry() throws Exception { + // This file hand-edited to have an invalid header + final byte[] bytes = readTestFileBytes(INVALID_HEADER_TEST_SERIALIZED_NAME); + httpCacheStorageEntryFromBytes(serializer, bytes); + } + + /** + * Deserialize a cache entry with a missing variant map key, from a previously saved file. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testVariantMapMissingKeyCacheEntry() throws Exception { + // This file hand-edited to be missing a VariantCache key + final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME); + httpCacheStorageEntryFromBytes(serializer, bytes); + } + + /** + * Deserialize a cache entry with a missing variant map value, from a previously saved file. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testVariantMapMissingValueCacheEntry() throws Exception { + // This file hand-edited to be missing a VariantCache value + final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME); + httpCacheStorageEntryFromBytes(serializer, bytes); + } + + /** + * Test an HttpException being thrown while serializing. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testSerializeWithHTTPException() throws Exception { + final AbstractMessageWriter throwyHttpWriter = Mockito.mock(AbstractMessageWriter.class); + Mockito. + doThrow(new HttpException("Test Exception")). + when(throwyHttpWriter). + write(Mockito.any(SimpleHttpResponse.class), Mockito.any(SessionOutputBuffer.class), Mockito.any(OutputStream.class)); + + final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault(); + final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry(); + + final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() { + protected AbstractMessageWriter makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) { + return throwyHttpWriter; + } + }; + testSerializer.serialize(testEntry); + } + + /** + * Test an IOException being thrown while deserializing. + * + * @throws Exception is expected + */ + @Test(expected = ResourceIOException.class) + public void testDeserializeWithIOException() throws Exception { + final AbstractMessageParser throwyParser = Mockito.mock(AbstractMessageParser.class); + Mockito. + doThrow(new IOException("Test Exception")). + when(throwyParser). + parse(Mockito.any(SessionInputBuffer.class), Mockito.any(InputStream.class)); + + final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() { + @Override + protected AbstractMessageParser makeHttpResponseParser() { + return throwyParser; + } + }; + testSerializer.deserialize(new byte[0]); + } +} diff --git a/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized b/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized new file mode 100644 index 0000000000..1f6ccd6d7e Binary files /dev/null and b/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized differ diff --git a/httpclient5-cache/src/test/resources/ApacheLogo.png b/httpclient5-cache/src/test/resources/ApacheLogo.png new file mode 100644 index 0000000000..c6daa67bab Binary files /dev/null and b/httpclient5-cache/src/test/resources/ApacheLogo.png differ diff --git a/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized new file mode 100644 index 0000000000..ca7d018357 --- /dev/null +++ b/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized @@ -0,0 +1,13 @@ +HTTP/1.1 200 OK +Content-Length: 11 +hc-esc-hc-test-1: hc-test-1-value +hc-esc-hc-sk: hc-sk-value +hc-esc-hc-resp-date: hc-resp-date-value +hc-esc-hc-req-date-date: hc-req-date-value +hc-esc-hc-varmap-key: hc-varmap-key-value +hc-esc-hc-varmap-val: hc-varmap-val-value +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 + +Hello World \ No newline at end of file diff --git a/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized new file mode 100644 index 0000000000..80f310240c --- /dev/null +++ b/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized @@ -0,0 +1,8 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +hc-sk: xyzzy +hc-resp-date: badbadbad +hc-req-date: 165214800000 + +Hello World diff --git a/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized new file mode 100644 index 0000000000..c73e8ca247 --- /dev/null +++ b/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized @@ -0,0 +1,7 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 + +Hello World diff --git a/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized b/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized new file mode 100644 index 0000000000..f2ec4c402e --- /dev/null +++ b/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized @@ -0,0 +1,8 @@ +HTTP/1.1 204 No Content +Content-type: text/html +Cache-control: public, max-age=31536000 +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 +hc-no-content: true + diff --git a/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized b/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized new file mode 100644 index 0000000000..c214ddca07 --- /dev/null +++ b/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +Content-Length: 11 +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 + +Hello World \ No newline at end of file diff --git a/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized new file mode 100644 index 0000000000..a383bf2e48 --- /dev/null +++ b/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized @@ -0,0 +1,13 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +Content-Length: 11 +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 +hc-varmap-key: {Accept-Encoding=gzip} +hc-varmap-val: {Accept-Encoding=gzip}https://example.com:1234/foo +hc-varmap-key: {Accept-Encoding=compress} +hc-varmap-val: {Accept-Encoding=compress}https://example.com:1234/foo + +Hello World \ No newline at end of file diff --git a/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized new file mode 100644 index 0000000000..62f79ff50b --- /dev/null +++ b/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 +hc-varmap-val: {Accept-Encoding=compress}https://example.com:1234/foo + +Hello World diff --git a/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized new file mode 100644 index 0000000000..12858e1ab2 --- /dev/null +++ b/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized @@ -0,0 +1,9 @@ +HTTP/1.1 200 OK +Content-type: text/html +Cache-control: public, max-age=31536000 +hc-sk: xyzzy +hc-resp-date: 2611108800000 +hc-req-date: 165214800000 +hc-varmap-key: {Accept-Encoding=gzip} + +Hello World diff --git a/pom.xml b/pom.xml index ab17af6fe5..cc3e1c0384 100644 --- a/pom.xml +++ b/pom.xml @@ -303,6 +303,7 @@ src/docbkx/resources/** src/test/resources/*.truststore + src/test/resources/*.serialized .checkstyle .externalToolBuilders/** maven-eclipse.xml