You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2019/12/30 09:58:06 UTC
[httpcomponents-client] branch master updated: HTTPCORE-615:
Implement HTTP-based cache serializer-deserializer. (#192)
This is an automated email from the ASF dual-hosted git repository.
olegk pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/httpcomponents-client.git
The following commit(s) were added to refs/heads/master by this push:
new f765a81 HTTPCORE-615: Implement HTTP-based cache serializer-deserializer. (#192)
f765a81 is described below
commit f765a81b31d9cbb25c2653f5bb881505ab2bc2ae
Author: Scott Gifford <sg...@suspectclass.com>
AuthorDate: Mon Dec 30 04:57:58 2019 -0500
HTTPCORE-615: Implement HTTP-based cache serializer-deserializer. (#192)
HTTPCORE-615: Implement HTTP-based cache serializer-deserializer.
---
.gitattributes | 1 +
.../cache/HttpByteArrayCacheEntrySerializer.java | 409 +++++++++++++++++++++
...HttpByteArrayCacheEntrySerializerTestUtils.java | 342 +++++++++++++++++
.../TestHttpByteArrayCacheEntrySerializer.java | 397 ++++++++++++++++++++
.../test/resources/ApacheLogo.httpbytes.serialized | Bin 0 -> 35161 bytes
.../src/test/resources/ApacheLogo.png | Bin 0 -> 34983 bytes
.../resources/escapedHeader.httpbytes.serialized | 13 +
.../resources/invalidHeader.httpbytes.serialized | 8 +
.../resources/missingHeader.httpbytes.serialized | 7 +
.../src/test/resources/noBody.httpbytes.serialized | 8 +
.../resources/simpleObject.httpbytes.serialized | 9 +
.../test/resources/variantMap.httpbytes.serialized | 13 +
.../variantMapMissingKey.httpbytes.serialized | 9 +
.../variantMapMissingValue.httpbytes.serialized | 9 +
pom.xml | 1 +
15 files changed, 1226 insertions(+)
diff --git a/.gitattributes b/.gitattributes
index 1e21b19..cd443a1 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 0000000..d5b77fd
--- /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
+ * <http://www.apache.org/>.
+ *
+ */
+
+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<byte[]> {
+ 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<SimpleHttpResponse> 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<ClassicHttpResponse> 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<String, String> 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.
+ * <p>
+ * Useful to override for testing.
+ */
+ protected AbstractMessageWriter<SimpleHttpResponse> makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
+ return new SimpleHttpResponseWriter();
+ }
+
+ /**
+ * Helper method to make a new ByteArrayInputStream.
+ * <p>
+ * Useful to override for testing.
+ */
+ protected InputStream makeByteArrayInputStream(final byte[] bytes) {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ /**
+ * Helper method to make a new HTTP Response parser.
+ * <p>
+ * Useful to override for testing.
+ */
+ protected AbstractMessageParser<ClassicHttpResponse> 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<String, String> 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<String, String> getVariantMapPseudoHeadersAndRemove(final HttpResponse response) throws ResourceIOException {
+ final Header[] headers = response.getHeaders();
+ final Map<String, String> 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<SimpleHttpResponse> {
+
+ 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 0000000..2b71e84
--- /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
+ * <http://www.apache.org/>.
+ *
+ */
+
+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<String, String> 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.
+ * <p>
+ * 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<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> 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<byte[]> 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 0000000..4c333e7
--- /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
+ * <http://www.apache.org/>.
+ *
+ */
+
+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<byte[]> 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<String, String> 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<String, String> 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<SimpleHttpResponse> 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<SimpleHttpResponse> 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<ClassicHttpResponse> 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<ClassicHttpResponse> 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 0000000..1f6ccd6
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 0000000..c6daa67
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 0000000..ca7d018
--- /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 0000000..80f3102
--- /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 0000000..c73e8ca
--- /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 0000000..f2ec4c4
--- /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 0000000..c214ddc
--- /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 0000000..a383bf2
--- /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 0000000..62f79ff
--- /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 0000000..12858e1
--- /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 ab17af6..cc3e1c0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -303,6 +303,7 @@
<excludes>
<exclude>src/docbkx/resources/**</exclude>
<exclude>src/test/resources/*.truststore</exclude>
+ <exclude>src/test/resources/*.serialized</exclude>
<exclude>.checkstyle</exclude>
<exclude>.externalToolBuilders/**</exclude>
<exclude>maven-eclipse.xml</exclude>