HTTPCORE-615: Implement HTTP-based cache serializer-deserializer. (#192)
HTTPCORE-615: Implement HTTP-based cache serializer-deserializer.
This commit is contained in:
parent
40173ca071
commit
f765a81b31
|
@ -21,3 +21,4 @@
|
|||
*.html text diff=html
|
||||
*.css text
|
||||
*.js text
|
||||
*.serialized binary
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]);
|
||||
}
|
||||
}
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
1
pom.xml
1
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>
|
||||
|
|
Loading…
Reference in New Issue