From f765a81b31d9cbb25c2653f5bb881505ab2bc2ae Mon Sep 17 00:00:00 2001 From: Scott Gifford Date: Mon, 30 Dec 2019 04:57:58 -0500 Subject: [PATCH] HTTPCORE-615: Implement HTTP-based cache serializer-deserializer. (#192) HTTPCORE-615: Implement HTTP-based cache serializer-deserializer. --- .gitattributes | 1 + .../HttpByteArrayCacheEntrySerializer.java | 409 ++++++++++++++++++ ...yteArrayCacheEntrySerializerTestUtils.java | 342 +++++++++++++++ ...TestHttpByteArrayCacheEntrySerializer.java | 397 +++++++++++++++++ .../resources/ApacheLogo.httpbytes.serialized | Bin 0 -> 35161 bytes .../src/test/resources/ApacheLogo.png | Bin 0 -> 34983 bytes .../escapedHeader.httpbytes.serialized | 13 + .../invalidHeader.httpbytes.serialized | 8 + .../missingHeader.httpbytes.serialized | 7 + .../resources/noBody.httpbytes.serialized | 8 + .../simpleObject.httpbytes.serialized | 9 + .../resources/variantMap.httpbytes.serialized | 13 + .../variantMapMissingKey.httpbytes.serialized | 9 + ...ariantMapMissingValue.httpbytes.serialized | 9 + pom.xml | 1 + 15 files changed, 1226 insertions(+) create mode 100644 httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializer.java create mode 100644 httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java create mode 100644 httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java create mode 100644 httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/ApacheLogo.png create mode 100644 httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/noBody.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized create mode 100644 httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized diff --git a/.gitattributes b/.gitattributes index 1e21b19d7..cd443a131 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 000000000..d5b77fd23 --- /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 000000000..2b71e8423 --- /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 000000000..4c333e7a0 --- /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 0000000000000000000000000000000000000000..1f6ccd6d7ea3993c9c131be9729401c52c285db0 GIT binary patch literal 35161 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 Hu0mjfE;||C literal 0 HcmV?d00001 diff --git a/httpclient5-cache/src/test/resources/ApacheLogo.png b/httpclient5-cache/src/test/resources/ApacheLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..c6daa67babc6960a452eb70918c1a67b939d29fe GIT binary patch literal 34983 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 Hu0mjf(+B3t literal 0 HcmV?d00001 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 000000000..ca7d01835 --- /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 000000000..80f310240 --- /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 000000000..c73e8ca24 --- /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 000000000..f2ec4c402 --- /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 000000000..c214ddca0 --- /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 000000000..a383bf2e4 --- /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 000000000..62f79ff50 --- /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 000000000..12858e1ab --- /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 ab17af6fe..cc3e1c038 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