makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
+ return new SimpleHttpResponseWriter();
+ }
+
+ /**
+ * Helper method to make a new ByteArrayInputStream.
+ *
+ * Useful to override for testing.
+ */
+ protected InputStream makeByteArrayInputStream(final byte[] bytes) {
+ return new ByteArrayInputStream(bytes);
+ }
+
+ /**
+ * Helper method to make a new HTTP Response parser.
+ *
+ * Useful to override for testing.
+ */
+ protected AbstractMessageParser makeHttpResponseParser() {
+ return new DefaultHttpResponseParser();
+ }
+
+ /**
+ * Modify the given response to escape any header names that start with the prefix we use for our own pseudo-headers,
+ * prefixing them with an escape sequence we can use to recover them later.
+ *
+ * @param httpResponse HTTP response object to escape headers in
+ * @see #unescapeHeaders(HttpResponse) for the corresponding un-escaper.
+ */
+ private static void escapeHeaders(final HttpResponse httpResponse) {
+ final Header[] headers = httpResponse.getHeaders();
+ for (final Header header : headers) {
+ if (header.getName().startsWith(SC_CACHE_ENTRY_PREFIX)) {
+ httpResponse.removeHeader(header);
+ httpResponse.addHeader(SC_CACHE_ENTRY_PRESERVE_PREFIX + header.getName(), header.getValue());
+ }
+ }
+ }
+
+ /**
+ * Modify the given response to remove escaping from any header names we escaped before saving.
+ *
+ * @param httpResponse HTTP response object to un-escape headers in
+ * @see #unescapeHeaders(HttpResponse) for the corresponding escaper
+ */
+ private void unescapeHeaders(final HttpResponse httpResponse) {
+ final Header[] headers = httpResponse.getHeaders();
+ for (final Header header : headers) {
+ if (header.getName().startsWith(SC_CACHE_ENTRY_PRESERVE_PREFIX)) {
+ httpResponse.removeHeader(header);
+ httpResponse.addHeader(header.getName().substring(SC_CACHE_ENTRY_PRESERVE_PREFIX.length()), header.getValue());
+ }
+ }
+ }
+
+ /**
+ * Modify the given response to add our own cache metadata as pseudo-headers.
+ *
+ * @param httpResponse HTTP response object to add pseudo-headers to
+ */
+ private void addMetadataPseudoHeaders(final HttpResponse httpResponse, final HttpCacheStorageEntry httpCacheEntry) {
+ httpResponse.addHeader(SC_HEADER_NAME_STORAGE_KEY, httpCacheEntry.getKey());
+ httpResponse.addHeader(SC_HEADER_NAME_RESPONSE_DATE, Long.toString(httpCacheEntry.getContent().getResponseDate().getTime()));
+ httpResponse.addHeader(SC_HEADER_NAME_REQUEST_DATE, Long.toString(httpCacheEntry.getContent().getRequestDate().getTime()));
+
+ // Encode these so map entries are stored in a pair of headers, one for key and one for value.
+ // Header keys look like: {Accept-Encoding=gzip}
+ // And header values like: {Accept-Encoding=gzip}https://example.com:1234/foo
+ for (final Map.Entry entry : httpCacheEntry.getContent().getVariantMap().entrySet()) {
+ // Headers are ordered
+ httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_KEY, entry.getKey());
+ httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_VALUE, entry.getValue());
+ }
+ }
+
+ /**
+ * Get the string value for a single metadata pseudo-header, and remove it from the response object.
+ *
+ * @param response Response object to get and remove the pseudo-header from
+ * @param name Name of metadata pseudo-header
+ * @return Value for metadata pseudo-header
+ * @throws ResourceIOException if the given pseudo-header is not found
+ */
+ private static String getCachePseudoHeaderAndRemove(final HttpResponse response, final String name) throws ResourceIOException {
+ final String headerValue = getOptionalCachePseudoHeaderAndRemove(response, name);
+ if (headerValue == null) {
+ throw new ResourceIOException("Expected cache header '" + name + "' not found");
+ }
+ return headerValue;
+ }
+
+ /**
+ * Get the string value for a single metadata pseudo-header if it exists, and remove it from the response object.
+ *
+ * @param response Response object to get and remove the pseudo-header from
+ * @param name Name of metadata pseudo-header
+ * @return Value for metadata pseudo-header, or null if it does not exist
+ */
+ private static String getOptionalCachePseudoHeaderAndRemove(final HttpResponse response, final String name) {
+ final Header header = response.getFirstHeader(name);
+ if (header == null) {
+ return null;
+ }
+ response.removeHeader(header);
+ return header.getValue();
+ }
+
+ /**
+ * Get the date value for a single metadata pseudo-header, and remove it from the response object.
+ *
+ * @param response Response object to get and remove the pseudo-header from
+ * @param name Name of metadata pseudo-header
+ * @return Value for metadata pseudo-header
+ * @throws ResourceIOException if the given pseudo-header is not found, or contains invalid data
+ */
+ private static Date getCachePseudoHeaderDateAndRemove(final HttpResponse response, final String name) throws ResourceIOException{
+ final String value = getCachePseudoHeaderAndRemove(response, name);
+ response.removeHeaders(name);
+ try {
+ final long timestamp = Long.parseLong(value);
+ return new Date(timestamp);
+ } catch (final NumberFormatException e) {
+ throw new ResourceIOException("Invalid value for header '" + name + "'", e);
+ }
+ }
+
+ /**
+ * Get the boolean value for a single metadata pseudo-header, and remove it from the response object.
+ *
+ * @param response Response object to get and remove the pseudo-header from
+ * @param name Name of metadata pseudo-header
+ * @return Value for metadata pseudo-header
+ */
+ private static boolean getCachePseudoHeaderBooleanAndRemove(final ClassicHttpResponse response, final String name) {
+ // parseBoolean does not throw any exceptions, so no try/catch required.
+ return Boolean.parseBoolean(getOptionalCachePseudoHeaderAndRemove(response, name));
+ }
+
+
+ /**
+ * Get the variant map metadata pseudo-header, and remove it from the response object.
+ *
+ * @param response Response object to get and remove the pseudo-header from
+ * @return Extracted variant map
+ * @throws ResourceIOException if the given pseudo-header is not found, or contains invalid data
+ */
+ private static Map getVariantMapPseudoHeadersAndRemove(final HttpResponse response) throws ResourceIOException {
+ final Header[] headers = response.getHeaders();
+ final Map variantMap = new HashMap<>(0);
+ String lastKey = null;
+ for (final Header header : headers) {
+ if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_KEY)) {
+ lastKey = header.getValue();
+ response.removeHeader(header);
+ } else if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_VALUE)) {
+ if (lastKey == null) {
+ throw new ResourceIOException("Found mismatched variant map key/value headers");
+ }
+ variantMap.put(lastKey, header.getValue());
+ lastKey = null;
+ response.removeHeader(header);
+ }
+ }
+
+ if (lastKey != null) {
+ throw new ResourceIOException("Found mismatched variant map key/value headers");
+ }
+
+ return variantMap;
+ }
+
+ /**
+ * Copy bytes from the given source buffer and input stream to the given output stream until end-of-file is reached.
+ *
+ * @param srcBuf Buffered input source
+ * @param src Unbuffered input source
+ * @param dest Output destination
+ * @throws IOException if an I/O error occurs
+ */
+ private static void copyBytes(final SessionInputBuffer srcBuf, final InputStream src, final OutputStream dest) throws IOException {
+ final byte[] buf = new byte[BUFFER_SIZE];
+ int lastBytesRead;
+ while ((lastBytesRead = srcBuf.read(buf, src)) != -1) {
+ dest.write(buf, 0, lastBytesRead);
+ }
+ }
+
+ /**
+ * Writer for SimpleHttpResponse.
+ *
+ * Copied from DefaultHttpResponseWriter, but wrapping a SimpleHttpResponse instead of a ClassicHttpResponse
+ */
+ // Seems like the DefaultHttpResponseWriter should be able to do this, but it doesn't seem to be able to
+ private class SimpleHttpResponseWriter extends AbstractMessageWriter {
+
+ public SimpleHttpResponseWriter() {
+ super(BasicLineFormatter.INSTANCE);
+ }
+
+ @Override
+ protected void writeHeadLine(
+ final SimpleHttpResponse message, final CharArrayBuffer lineBuf) {
+ final ProtocolVersion transportVersion = message.getVersion();
+ BasicLineFormatter.INSTANCE.formatStatusLine(lineBuf, new StatusLine(
+ transportVersion != null ? transportVersion : HttpVersion.HTTP_1_1,
+ message.getCode(),
+ message.getReasonPhrase()));
+ }
+ }
+
+ /**
+ * Cache validity policy that always returns an age of 0.
+ *
+ * This prevents the Age header from being written to the cache (it does not make sense to cache it),
+ * and is the only thing the policy is used for in this case.
+ */
+ private class NoAgeCacheValidityPolicy extends CacheValidityPolicy {
+ @Override
+ public long getCurrentAgeSecs(final HttpCacheEntry entry, final Date now) {
+ return 0L;
+ }
+ }
+}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java
new file mode 100644
index 0000000000..2b71e84234
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpByteArrayCacheEntrySerializerTestUtils.java
@@ -0,0 +1,342 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.impl.cache;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.Resource;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.message.BasicHeader;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+class HttpByteArrayCacheEntrySerializerTestUtils {
+ private final static String TEST_RESOURCE_DIR = "src/test/resources/";
+ static final String TEST_STORAGE_KEY = "xyzzy";
+
+ /**
+ * Template for incrementally building a new HttpCacheStorageEntry test object, starting from defaults.
+ */
+ static class HttpCacheStorageEntryTestTemplate {
+ Resource resource;
+ Date requestDate;
+ Date responseDate;
+ int responseCode;
+ Header[] responseHeaders;
+ Map variantMap;
+ String storageKey;
+
+ /**
+ * Return a new HttpCacheStorageEntryTestTemplate instance with all default values.
+ *
+ * @return new HttpCacheStorageEntryTestTemplate instance
+ */
+ static HttpCacheStorageEntryTestTemplate makeDefault() {
+ return new HttpCacheStorageEntryTestTemplate(DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE);
+ }
+
+ /**
+ * Convert this template to a HttpCacheStorageEntry object.
+ * @return HttpCacheStorageEntry object
+ */
+ HttpCacheStorageEntry toEntry() {
+ return new HttpCacheStorageEntry(storageKey,
+ new HttpCacheEntry(
+ requestDate,
+ responseDate,
+ responseCode,
+ responseHeaders,
+ resource,
+ variantMap));
+ }
+
+ /**
+ * Create a new template with all null values.
+ */
+ private HttpCacheStorageEntryTestTemplate() {
+ }
+
+ /**
+ * Create a new template values copied from the given template
+ *
+ * @param src Template to copy values from
+ */
+ private HttpCacheStorageEntryTestTemplate(final HttpCacheStorageEntryTestTemplate src) {
+ this.resource = src.resource;
+ this.requestDate = src.requestDate;
+ this.responseDate = src.responseDate;
+ this.responseCode = src.responseCode;
+ this.responseHeaders = src.responseHeaders;
+ this.variantMap = src.variantMap;
+ this.storageKey = src.storageKey;
+ }
+ }
+
+ /**
+ * Template with all default values.
+ *
+ * Used by HttpCacheStorageEntryTestTemplate#makeDefault()
+ */
+ private static final HttpCacheStorageEntryTestTemplate DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE = new HttpCacheStorageEntryTestTemplate();
+ static {
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.resource = new HeapResource("Hello World".getBytes(StandardCharsets.UTF_8));
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.requestDate = new Date(165214800000L);
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseDate = new Date(2611108800000L);
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseCode = 200;
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseHeaders = new Header[]{
+ new BasicHeader("Content-type", "text/html"),
+ new BasicHeader("Cache-control", "public, max-age=31536000"),
+ };
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.variantMap = Collections.emptyMap();
+ DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.storageKey = TEST_STORAGE_KEY;
+ }
+
+ /**
+ * Test serializing and deserializing the given object with the given factory.
+ *
+ * Compares fields to ensure the deserialized object is equivalent to the original object.
+ *
+ * @param serializer Factory for creating serializers
+ * @param httpCacheStorageEntry Original object to serialize and test against
+ * @throws Exception if anything goes wrong
+ */
+ static void testWithCache(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry) throws Exception {
+ final byte[] testBytes = serializer.serialize(httpCacheStorageEntry);
+ verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, testBytes);
+ }
+
+ /**
+ * Verify that the given bytes deserialize to the given storage key and an equivalent cache entry.
+ *
+ * @param serializer Deserializer
+ * @param httpCacheStorageEntry Cache entry to verify
+ * @param testBytes Bytes to deserialize
+ * @throws Exception if anything goes wrong
+ */
+ static void verifyHttpCacheEntryFromBytes(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final byte[] testBytes) throws Exception {
+ final HttpCacheStorageEntry testEntry = httpCacheStorageEntryFromBytes(serializer, testBytes);
+
+ assertCacheEntriesEqual(httpCacheStorageEntry, testEntry);
+ }
+
+ /**
+ * Verify that the given test file deserializes to a cache entry equivalent to the one given.
+ *
+ * @param serializer Deserializer
+ * @param httpCacheStorageEntry Cache entry to verify
+ * @param testFileName Name of test file to deserialize
+ * @param reserializeFiles If true, test files will be regenerated and saved to disk
+ * @throws Exception if anything goes wrong
+ */
+ static void verifyHttpCacheEntryFromTestFile(final HttpCacheEntrySerializer serializer,
+ final HttpCacheStorageEntry httpCacheStorageEntry,
+ final String testFileName,
+ final boolean reserializeFiles) throws Exception {
+ if (reserializeFiles) {
+ final File toFile = makeTestFileObject(testFileName);
+ saveEntryToFile(serializer, httpCacheStorageEntry, toFile);
+ }
+
+ final byte[] bytes = readTestFileBytes(testFileName);
+
+ verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, bytes);
+ }
+
+ /**
+ * Get the bytes of the given test file.
+ *
+ * @param testFileName Name of test file to get bytes from
+ * @return Bytes from the given test file
+ * @throws Exception if anything goes wrong
+ */
+ static byte[] readTestFileBytes(final String testFileName) throws Exception {
+ final File testFile = makeTestFileObject(testFileName);
+ try(final FileInputStream testStream = new FileInputStream(testFile)) {
+ return readFullyStrict(testStream, testFile.length());
+ }
+ }
+
+ /**
+ * Create a new cache object from the given bytes.
+ *
+ * @param serializer Deserializer
+ * @param testBytes Bytes to deserialize
+ * @return Deserialized object
+ */
+ static HttpCacheStorageEntry httpCacheStorageEntryFromBytes(final HttpCacheEntrySerializer serializer, final byte[] testBytes) throws ResourceIOException {
+ return serializer.deserialize(testBytes);
+ }
+
+ /**
+ * Assert that the given objects are equivalent
+ *
+ * @param expected Expected cache entry object
+ * @param actual Actual cache entry object
+ * @throws Exception if anything goes wrong
+ */
+ static void assertCacheEntriesEqual(final HttpCacheStorageEntry expected, final HttpCacheStorageEntry actual) throws Exception {
+ assertEquals(expected.getKey(), actual.getKey());
+
+ final HttpCacheEntry expectedContent = expected.getContent();
+ final HttpCacheEntry actualContent = actual.getContent();
+
+ assertEquals(expectedContent.getRequestDate(), actualContent.getRequestDate());
+ assertEquals(expectedContent.getResponseDate(), actualContent.getResponseDate());
+ assertEquals(expectedContent.getStatus(), actualContent.getStatus());
+
+ assertArrayEquals(expectedContent.getVariantMap().keySet().toArray(), actualContent.getVariantMap().keySet().toArray());
+ for (final String key : expectedContent.getVariantMap().keySet()) {
+ assertEquals("Expected same variantMap values for key '" + key + "'",
+ expectedContent.getVariantMap().get(key), actualContent.getVariantMap().get(key));
+ }
+
+ // Verify that the same headers are present on the expected and actual content.
+ for(final Header expectedHeader: expectedContent.getHeaders()) {
+ final Header actualHeader = actualContent.getFirstHeader(expectedHeader.getName());
+
+ if (actualHeader == null) {
+ if (expectedHeader.getName().equalsIgnoreCase("content-length")) {
+ // This header is added by the cache implementation, and can be safely ignored
+ } else {
+ fail("Expected header " + expectedHeader.getName() + " was not found");
+ }
+ } else {
+ assertEquals(expectedHeader.getName(), actualHeader.getName());
+ assertEquals(expectedHeader.getValue(), actualHeader.getValue());
+ }
+ }
+
+ if (expectedContent.getResource() == null) {
+ assertNull("Expected null resource", actualContent.getResource());
+ } else {
+ final byte[] expectedBytes = readFullyStrict(
+ expectedContent.getResource().getInputStream(),
+ (int) expectedContent.getResource().length()
+ );
+ final byte[] actualBytes = readFullyStrict(
+ actualContent.getResource().getInputStream(),
+ (int) actualContent.getResource().length()
+ );
+ assertArrayEquals(expectedBytes, actualBytes);
+ }
+ }
+
+ /**
+ * Get a File object for the given test file.
+ *
+ * @param testFileName Name of test file
+ * @return File for this test file
+ */
+ static File makeTestFileObject(final String testFileName) {
+ return new File(TEST_RESOURCE_DIR + testFileName);
+ }
+
+ /**
+ * Save the given cache entry serialized to the given file.
+ *
+ * @param serializer Serializer
+ * @param httpCacheStorageEntry Cache entry to serialize and save
+ * @param outFile Output file to write to
+ * @throws Exception if anything goes wrong
+ */
+ static void saveEntryToFile(final HttpCacheEntrySerializer serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final File outFile) throws Exception {
+ final byte[] bytes = serializer.serialize(httpCacheStorageEntry);
+
+ OutputStream out = null;
+ try {
+ out = new FileOutputStream(outFile);
+ out.write(bytes);
+ } finally {
+ if (out != null) {
+ out.close();
+ }
+ }
+ }
+
+ /**
+ * Copy bytes from the given input stream to the given destination buffer until the buffer is full,
+ * or end-of-file is reached, and return the number of bytes read.
+ *
+ * @param src Input stream to read from
+ * @param dest Output buffer to write to
+ * @return Number of bytes read
+ * @throws IOException if an I/O error occurs
+ */
+ private static int readFully(final InputStream src, final byte[] dest) throws IOException {
+ final int destPos = 0;
+ final int length = dest.length;
+ int totalBytesRead = 0;
+ int lastBytesRead;
+
+ while (totalBytesRead < length && (lastBytesRead = src.read(dest, destPos + totalBytesRead, length - totalBytesRead)) != -1) {
+ totalBytesRead += lastBytesRead;
+ }
+ return totalBytesRead;
+ }
+
+ /**
+ * Copy bytes from the given input stream to a new buffer until the given length is reached,
+ * and returns the new buffer. If end-of-file is reached first, an IOException is thrown
+ *
+ * @param src Input stream to read from
+ * @param length Maximum bytes to read
+ * @return All bytes from file
+ * @throws IOException if an I/O error occurs or end-of-file is reached before the requested
+ * number of bytes have been read
+ */
+ static byte[] readFullyStrict(final InputStream src, final long length) throws IOException {
+ if (length > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException(String.format("Length %d is too large to fit in an array", length));
+ }
+ final int intLength = (int) length;
+ final byte[] dest = new byte[intLength];
+ final int bytesRead = readFully(src, dest);
+
+ if (bytesRead == intLength) {
+ return dest;
+ } else {
+ throw new IOException(String.format("Expected to read %d bytes but only got %d", intLength, bytesRead));
+ }
+ }
+}
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java
new file mode 100644
index 0000000000..4c333e7a0e
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpByteArrayCacheEntrySerializer.java
@@ -0,0 +1,397 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+
+package org.apache.hc.client5.http.impl.cache;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.impl.io.AbstractMessageParser;
+import org.apache.hc.core5.http.impl.io.AbstractMessageWriter;
+import org.apache.hc.core5.http.io.SessionInputBuffer;
+import org.apache.hc.core5.http.io.SessionOutputBuffer;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.makeTestFileObject;
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.httpCacheStorageEntryFromBytes;
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.readTestFileBytes;
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.testWithCache;
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.verifyHttpCacheEntryFromTestFile;
+import static org.apache.hc.client5.http.impl.cache.HttpByteArrayCacheEntrySerializerTestUtils.HttpCacheStorageEntryTestTemplate;
+
+public class TestHttpByteArrayCacheEntrySerializer {
+ private static final String SERIALIAZED_EXTENSION = ".httpbytes.serialized";
+
+ private static final String FILE_TEST_SERIALIZED_NAME = "ApacheLogo" + SERIALIAZED_EXTENSION;
+ private static final String SIMPLE_OBJECT_SERIALIZED_NAME = "simpleObject" + SERIALIAZED_EXTENSION;
+ private static final String VARIANTMAP_TEST_SERIALIZED_NAME = "variantMap" + SERIALIAZED_EXTENSION;
+ private static final String ESCAPED_HEADER_TEST_SERIALIZED_NAME = "escapedHeader" + SERIALIAZED_EXTENSION;
+ private static final String NO_BODY_TEST_SERIALIZED_NAME = "noBody" + SERIALIAZED_EXTENSION;
+ private static final String MISSING_HEADER_TEST_SERIALIZED_NAME = "missingHeader" + SERIALIAZED_EXTENSION;
+ private static final String INVALID_HEADER_TEST_SERIALIZED_NAME = "invalidHeader" + SERIALIAZED_EXTENSION;
+ private static final String VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME = "variantMapMissingKey" + SERIALIAZED_EXTENSION;
+ private static final String VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME = "variantMapMissingValue" + SERIALIAZED_EXTENSION;
+
+ private static final String TEST_CONTENT_FILE_NAME = "ApacheLogo.png";
+
+ private HttpCacheEntrySerializer serializer;
+
+ // Manually set this to true to re-generate all of the serialized files
+ private final boolean reserializeFiles = false;
+
+ @Before
+ public void before() {
+ serializer = HttpByteArrayCacheEntrySerializer.INSTANCE;
+ }
+
+ /**
+ * Serialize and deserialize a simple object with a tiny body.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void simpleObjectTest() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Serialize and deserialize a larger object with a binary file for a body.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void fileObjectTest() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME));
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Serialize and deserialize a cache entry with no headers.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void noHeadersTest() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.responseHeaders = new Header[0];
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Serialize and deserialize a cache entry with an empty body.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void emptyBodyTest() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.resource = new HeapResource(new byte[0]);
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Serialize and deserialize a cache entry with no body.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void noBodyTest() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.resource = null;
+ cacheObjectValues.responseCode = 204;
+
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Serialize and deserialize a cache entry with a variant map.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void testSimpleVariantMap() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ final Map variantMap = new HashMap<>();
+ variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo");
+ variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo");
+ cacheObjectValues.variantMap = variantMap;
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Ensures that if the server uses our reserved header names we don't mix them up with our own pseudo-headers.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void testEscapedHeaders() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.responseHeaders = new Header[] {
+ new BasicHeader("hc-test-1", "hc-test-1-value"),
+ new BasicHeader("hc-sk", "hc-sk-value"),
+ new BasicHeader("hc-resp-date", "hc-resp-date-value"),
+ new BasicHeader("hc-req-date-date", "hc-req-date-value"),
+ new BasicHeader("hc-varmap-key", "hc-varmap-key-value"),
+ new BasicHeader("hc-varmap-val", "hc-varmap-val-value"),
+ };
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ testWithCache(serializer, testEntry);
+ }
+
+ /**
+ * Attempt to store a cache entry with a null storage key.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = IllegalStateException.class)
+ public void testNullStorageKey() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.storageKey = null;
+
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+ serializer.serialize(testEntry);
+ }
+
+ /**
+ * Deserialize a simple object, from a previously saved file.
+ *
+ * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void simpleTestFromPreviouslySerialized() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ verifyHttpCacheEntryFromTestFile(serializer, testEntry, SIMPLE_OBJECT_SERIALIZED_NAME, reserializeFiles);
+ }
+
+ /**
+ * Deserialize a larger object with a binary body, from a previously saved file.
+ *
+ * Ensures that if the serialization format changes in an incompatible way, we'll find out in a test.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void fileTestFromPreviouslySerialized() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.resource = new FileResource(makeTestFileObject(TEST_CONTENT_FILE_NAME));
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ verifyHttpCacheEntryFromTestFile(serializer, testEntry, FILE_TEST_SERIALIZED_NAME, reserializeFiles);
+ }
+
+ /**
+ * Deserialize a cache entry with a variant map, from a previously saved file.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void variantMapTestFromPreviouslySerialized() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ final Map variantMap = new HashMap<>();
+ variantMap.put("{Accept-Encoding=gzip}","{Accept-Encoding=gzip}https://example.com:1234/foo");
+ variantMap.put("{Accept-Encoding=compress}","{Accept-Encoding=compress}https://example.com:1234/foo");
+ cacheObjectValues.variantMap = variantMap;
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ verifyHttpCacheEntryFromTestFile(serializer, testEntry, VARIANTMAP_TEST_SERIALIZED_NAME, reserializeFiles);
+ }
+
+ /**
+ * Deserialize a cache entry with headers that use our pseudo-header prefix and need escaping.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void escapedHeaderTestFromPreviouslySerialized() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.responseHeaders = new Header[] {
+ new BasicHeader("hc-test-1", "hc-test-1-value"),
+ new BasicHeader("hc-sk", "hc-sk-value"),
+ new BasicHeader("hc-resp-date", "hc-resp-date-value"),
+ new BasicHeader("hc-req-date-date", "hc-req-date-value"),
+ new BasicHeader("hc-varmap-key", "hc-varmap-key-value"),
+ new BasicHeader("hc-varmap-val", "hc-varmap-val-value"),
+ };
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ verifyHttpCacheEntryFromTestFile(serializer, testEntry, ESCAPED_HEADER_TEST_SERIALIZED_NAME, reserializeFiles);
+ }
+
+ /**
+ * Deserialize a cache entry with no body, from a previously saved file.
+ *
+ * @throws Exception if anything goes wrong
+ */
+ @Test
+ public void noBodyTestFromPreviouslySerialized() throws Exception {
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ cacheObjectValues.resource = null;
+ cacheObjectValues.responseCode = 204;
+
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ verifyHttpCacheEntryFromTestFile(serializer, testEntry, NO_BODY_TEST_SERIALIZED_NAME, reserializeFiles);
+ }
+
+ /**
+ * Deserialize a cache entry in a bad format, expecting an exception.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testInvalidCacheEntry() throws Exception {
+ // This file is a JPEG not a cache entry, so should fail to deserialize
+ final byte[] bytes = readTestFileBytes(TEST_CONTENT_FILE_NAME);
+ httpCacheStorageEntryFromBytes(serializer, bytes);
+ }
+
+ /**
+ * Deserialize a cache entry with a missing header, from a previously saved file.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testMissingHeaderCacheEntry() throws Exception {
+ // This file hand-edited to be missing a necessary header
+ final byte[] bytes = readTestFileBytes(MISSING_HEADER_TEST_SERIALIZED_NAME);
+ httpCacheStorageEntryFromBytes(serializer, bytes);
+ }
+
+ /**
+ * Deserialize a cache entry with an invalid header value, from a previously saved file.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testInvalidHeaderCacheEntry() throws Exception {
+ // This file hand-edited to have an invalid header
+ final byte[] bytes = readTestFileBytes(INVALID_HEADER_TEST_SERIALIZED_NAME);
+ httpCacheStorageEntryFromBytes(serializer, bytes);
+ }
+
+ /**
+ * Deserialize a cache entry with a missing variant map key, from a previously saved file.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testVariantMapMissingKeyCacheEntry() throws Exception {
+ // This file hand-edited to be missing a VariantCache key
+ final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_KEY_TEST_SERIALIZED_NAME);
+ httpCacheStorageEntryFromBytes(serializer, bytes);
+ }
+
+ /**
+ * Deserialize a cache entry with a missing variant map value, from a previously saved file.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testVariantMapMissingValueCacheEntry() throws Exception {
+ // This file hand-edited to be missing a VariantCache value
+ final byte[] bytes = readTestFileBytes(VARIANTMAP_MISSING_VALUE_TEST_SERIALIZED_NAME);
+ httpCacheStorageEntryFromBytes(serializer, bytes);
+ }
+
+ /**
+ * Test an HttpException being thrown while serializing.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testSerializeWithHTTPException() throws Exception {
+ final AbstractMessageWriter throwyHttpWriter = Mockito.mock(AbstractMessageWriter.class);
+ Mockito.
+ doThrow(new HttpException("Test Exception")).
+ when(throwyHttpWriter).
+ write(Mockito.any(SimpleHttpResponse.class), Mockito.any(SessionOutputBuffer.class), Mockito.any(OutputStream.class));
+
+ final HttpCacheStorageEntryTestTemplate cacheObjectValues = HttpCacheStorageEntryTestTemplate.makeDefault();
+ final HttpCacheStorageEntry testEntry = cacheObjectValues.toEntry();
+
+ final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() {
+ protected AbstractMessageWriter makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
+ return throwyHttpWriter;
+ }
+ };
+ testSerializer.serialize(testEntry);
+ }
+
+ /**
+ * Test an IOException being thrown while deserializing.
+ *
+ * @throws Exception is expected
+ */
+ @Test(expected = ResourceIOException.class)
+ public void testDeserializeWithIOException() throws Exception {
+ final AbstractMessageParser throwyParser = Mockito.mock(AbstractMessageParser.class);
+ Mockito.
+ doThrow(new IOException("Test Exception")).
+ when(throwyParser).
+ parse(Mockito.any(SessionInputBuffer.class), Mockito.any(InputStream.class));
+
+ final HttpByteArrayCacheEntrySerializer testSerializer = new HttpByteArrayCacheEntrySerializer() {
+ @Override
+ protected AbstractMessageParser makeHttpResponseParser() {
+ return throwyParser;
+ }
+ };
+ testSerializer.deserialize(new byte[0]);
+ }
+}
diff --git a/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized b/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized
new file mode 100644
index 0000000000..1f6ccd6d7e
Binary files /dev/null and b/httpclient5-cache/src/test/resources/ApacheLogo.httpbytes.serialized differ
diff --git a/httpclient5-cache/src/test/resources/ApacheLogo.png b/httpclient5-cache/src/test/resources/ApacheLogo.png
new file mode 100644
index 0000000000..c6daa67bab
Binary files /dev/null and b/httpclient5-cache/src/test/resources/ApacheLogo.png differ
diff --git a/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized
new file mode 100644
index 0000000000..ca7d018357
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/escapedHeader.httpbytes.serialized
@@ -0,0 +1,13 @@
+HTTP/1.1 200 OK
+Content-Length: 11
+hc-esc-hc-test-1: hc-test-1-value
+hc-esc-hc-sk: hc-sk-value
+hc-esc-hc-resp-date: hc-resp-date-value
+hc-esc-hc-req-date-date: hc-req-date-value
+hc-esc-hc-varmap-key: hc-varmap-key-value
+hc-esc-hc-varmap-val: hc-varmap-val-value
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+
+Hello World
\ No newline at end of file
diff --git a/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized
new file mode 100644
index 0000000000..80f310240c
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/invalidHeader.httpbytes.serialized
@@ -0,0 +1,8 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+hc-sk: xyzzy
+hc-resp-date: badbadbad
+hc-req-date: 165214800000
+
+Hello World
diff --git a/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized b/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized
new file mode 100644
index 0000000000..c73e8ca247
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/missingHeader.httpbytes.serialized
@@ -0,0 +1,7 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+
+Hello World
diff --git a/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized b/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized
new file mode 100644
index 0000000000..f2ec4c402e
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/noBody.httpbytes.serialized
@@ -0,0 +1,8 @@
+HTTP/1.1 204 No Content
+Content-type: text/html
+Cache-control: public, max-age=31536000
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+hc-no-content: true
+
diff --git a/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized b/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized
new file mode 100644
index 0000000000..c214ddca07
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/simpleObject.httpbytes.serialized
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+Content-Length: 11
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+
+Hello World
\ No newline at end of file
diff --git a/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized
new file mode 100644
index 0000000000..a383bf2e48
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/variantMap.httpbytes.serialized
@@ -0,0 +1,13 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+Content-Length: 11
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+hc-varmap-key: {Accept-Encoding=gzip}
+hc-varmap-val: {Accept-Encoding=gzip}https://example.com:1234/foo
+hc-varmap-key: {Accept-Encoding=compress}
+hc-varmap-val: {Accept-Encoding=compress}https://example.com:1234/foo
+
+Hello World
\ No newline at end of file
diff --git a/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized
new file mode 100644
index 0000000000..62f79ff50b
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/variantMapMissingKey.httpbytes.serialized
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+hc-varmap-val: {Accept-Encoding=compress}https://example.com:1234/foo
+
+Hello World
diff --git a/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized b/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized
new file mode 100644
index 0000000000..12858e1ab2
--- /dev/null
+++ b/httpclient5-cache/src/test/resources/variantMapMissingValue.httpbytes.serialized
@@ -0,0 +1,9 @@
+HTTP/1.1 200 OK
+Content-type: text/html
+Cache-control: public, max-age=31536000
+hc-sk: xyzzy
+hc-resp-date: 2611108800000
+hc-req-date: 165214800000
+hc-varmap-key: {Accept-Encoding=gzip}
+
+Hello World
diff --git a/pom.xml b/pom.xml
index ab17af6fe5..cc3e1c0384 100644
--- a/pom.xml
+++ b/pom.xml
@@ -303,6 +303,7 @@
src/docbkx/resources/**
src/test/resources/*.truststore
+ src/test/resources/*.serialized
.checkstyle
.externalToolBuilders/**
maven-eclipse.xml