View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.client5.http.impl.cache;
29  
30  import java.io.File;
31  import java.io.FileInputStream;
32  import java.io.FileOutputStream;
33  import java.io.IOException;
34  import java.io.InputStream;
35  import java.io.OutputStream;
36  import java.nio.charset.StandardCharsets;
37  import java.time.Instant;
38  import java.util.Collections;
39  import java.util.Map;
40  
41  import org.apache.hc.client5.http.cache.HttpCacheEntry;
42  import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
43  import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
44  import org.apache.hc.client5.http.cache.Resource;
45  import org.apache.hc.client5.http.cache.ResourceIOException;
46  import org.apache.hc.core5.http.Header;
47  import org.apache.hc.core5.http.message.BasicHeader;
48  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
49  import static org.junit.jupiter.api.Assertions.assertEquals;
50  import static org.junit.jupiter.api.Assertions.assertNull;
51  import static org.junit.jupiter.api.Assertions.fail;
52  
53  class HttpByteArrayCacheEntrySerializerTestUtils {
54      private final static String TEST_RESOURCE_DIR = "src/test/resources/";
55      static final String TEST_STORAGE_KEY = "xyzzy";
56  
57      /**
58       * Template for incrementally building a new HttpCacheStorageEntry test object, starting from defaults.
59       */
60      static class HttpCacheStorageEntryTestTemplate {
61          Resource resource;
62          Instant requestDate;
63          Instant responseDate;
64          int responseCode;
65          Header[] responseHeaders;
66          Map<String, String> variantMap;
67          String storageKey;
68  
69          /**
70           * Return a new HttpCacheStorageEntryTestTemplate instance with all default values.
71           *
72           * @return new HttpCacheStorageEntryTestTemplate instance
73           */
74          static HttpCacheStorageEntryTestTemplate makeDefault() {
75              return new HttpCacheStorageEntryTestTemplate(DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE);
76          }
77  
78          /**
79           * Convert this template to a HttpCacheStorageEntry object.
80           * @return HttpCacheStorageEntry object
81           */
82          HttpCacheStorageEntry toEntry() {
83              return new HttpCacheStorageEntry(storageKey,
84                      new HttpCacheEntry(
85                              requestDate,
86                              responseDate,
87                              responseCode,
88                              responseHeaders,
89                              resource,
90                              variantMap));
91          }
92  
93          /**
94           * Create a new template with all null values.
95           */
96          private HttpCacheStorageEntryTestTemplate() {
97          }
98  
99          /**
100          * Create a new template values copied from the given template
101          *
102          * @param src Template to copy values from
103          */
104         private HttpCacheStorageEntryTestTemplate(final HttpCacheStorageEntryTestTemplate src) {
105             this.resource = src.resource;
106             this.requestDate = src.requestDate;
107             this.responseDate = src.responseDate;
108             this.responseCode = src.responseCode;
109             this.responseHeaders = src.responseHeaders;
110             this.variantMap = src.variantMap;
111             this.storageKey = src.storageKey;
112         }
113     }
114 
115     /**
116      * Template with all default values.
117      *
118      * Used by HttpCacheStorageEntryTestTemplate#makeDefault()
119      */
120     private static final HttpCacheStorageEntryTestTemplate DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE = new HttpCacheStorageEntryTestTemplate();
121     static {
122         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.resource = new HeapResource("Hello World".getBytes(StandardCharsets.UTF_8));
123         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.requestDate = Instant.ofEpochMilli(165214800000L);
124         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseDate = Instant.ofEpochMilli(2611108800000L);
125         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseCode = 200;
126         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseHeaders = new Header[]{
127                 new BasicHeader("Content-type", "text/html"),
128                 new BasicHeader("Cache-control", "public, max-age=31536000"),
129         };
130         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.variantMap = Collections.emptyMap();
131         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.storageKey = TEST_STORAGE_KEY;
132     }
133 
134     /**
135      * Test serializing and deserializing the given object with the given factory.
136      * <p>
137      * Compares fields to ensure the deserialized object is equivalent to the original object.
138      *
139      * @param serializer Factory for creating serializers
140      * @param httpCacheStorageEntry    Original object to serialize and test against
141      * @throws Exception if anything goes wrong
142      */
143     static void testWithCache(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry) throws Exception {
144         final byte[] testBytes = serializer.serialize(httpCacheStorageEntry);
145         verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, testBytes);
146     }
147 
148     /**
149      * Verify that the given bytes deserialize to the given storage key and an equivalent cache entry.
150      *
151      * @param serializer Deserializer
152      * @param httpCacheStorageEntry Cache entry to verify
153      * @param testBytes Bytes to deserialize
154      * @throws Exception if anything goes wrong
155      */
156     static void verifyHttpCacheEntryFromBytes(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final byte[] testBytes) throws Exception {
157         final HttpCacheStorageEntry testEntry = httpCacheStorageEntryFromBytes(serializer, testBytes);
158 
159         assertCacheEntriesEqual(httpCacheStorageEntry, testEntry);
160     }
161 
162     /**
163      * Verify that the given test file deserializes to a cache entry equivalent to the one given.
164      *
165      * @param serializer Deserializer
166      * @param httpCacheStorageEntry    Cache entry to verify
167      * @param testFileName  Name of test file to deserialize
168      * @param reserializeFiles If true, test files will be regenerated and saved to disk
169      * @throws Exception if anything goes wrong
170      */
171     static void verifyHttpCacheEntryFromTestFile(final HttpCacheEntrySerializer<byte[]> serializer,
172                                                  final HttpCacheStorageEntry httpCacheStorageEntry,
173                                                  final String testFileName,
174                                                  final boolean reserializeFiles) throws Exception {
175         if (reserializeFiles) {
176             final File toFile = makeTestFileObject(testFileName);
177             saveEntryToFile(serializer, httpCacheStorageEntry, toFile);
178         }
179 
180         final byte[] bytes = readTestFileBytes(testFileName);
181 
182         verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, bytes);
183     }
184 
185     /**
186      * Get the bytes of the given test file.
187      *
188      * @param testFileName Name of test file to get bytes from
189      * @return Bytes from the given test file
190      * @throws Exception if anything goes wrong
191      */
192     static byte[] readTestFileBytes(final String testFileName) throws Exception {
193         final File testFile = makeTestFileObject(testFileName);
194         try(final FileInputStream testStream = new FileInputStream(testFile)) {
195             return readFullyStrict(testStream, testFile.length());
196         }
197     }
198 
199     /**
200      * Create a new cache object from the given bytes.
201      *
202      * @param serializer Deserializer
203      * @param testBytes         Bytes to deserialize
204      * @return Deserialized object
205      */
206     static HttpCacheStorageEntry httpCacheStorageEntryFromBytes(final HttpCacheEntrySerializer<byte[]> serializer, final byte[] testBytes) throws ResourceIOException {
207         return serializer.deserialize(testBytes);
208     }
209 
210     /**
211      * Assert that the given objects are equivalent
212      *
213      * @param expected Expected cache entry object
214      * @param actual   Actual cache entry object
215      * @throws Exception if anything goes wrong
216      */
217     static void assertCacheEntriesEqual(final HttpCacheStorageEntry expected, final HttpCacheStorageEntry actual) throws Exception {
218         assertEquals(expected.getKey(), actual.getKey());
219 
220         final HttpCacheEntry expectedContent = expected.getContent();
221         final HttpCacheEntry actualContent = actual.getContent();
222 
223         assertEquals(expectedContent.getRequestInstant(), actualContent.getRequestInstant());
224         assertEquals(expectedContent.getResponseInstant(), actualContent.getResponseInstant());
225 
226         assertEquals(expectedContent.getStatus(), actualContent.getStatus());
227 
228         assertArrayEquals(expectedContent.getVariantMap().keySet().toArray(), actualContent.getVariantMap().keySet().toArray());
229         for (final String key : expectedContent.getVariantMap().keySet()) {
230             assertEquals(expectedContent.getVariantMap().get(key), actualContent.getVariantMap().get(key), "Expected same variantMap values for key '" + key + "'");
231         }
232 
233         // Verify that the same headers are present on the expected and actual content.
234         for(final Header expectedHeader: expectedContent.getHeaders()) {
235             final Header actualHeader = actualContent.getFirstHeader(expectedHeader.getName());
236 
237             if (actualHeader == null) {
238                 if (expectedHeader.getName().equalsIgnoreCase("content-length")) {
239                     // This header is added by the cache implementation, and can be safely ignored
240                 } else {
241                     fail("Expected header " + expectedHeader.getName() + " was not found");
242                 }
243             } else {
244                 assertEquals(expectedHeader.getName(), actualHeader.getName());
245                 assertEquals(expectedHeader.getValue(), actualHeader.getValue());
246             }
247         }
248 
249         if (expectedContent.getResource() == null) {
250             assertNull(actualContent.getResource(), "Expected null resource");
251         } else {
252             final byte[] expectedBytes = readFullyStrict(
253                     expectedContent.getResource().getInputStream(),
254                     (int) expectedContent.getResource().length()
255             );
256             final byte[] actualBytes = readFullyStrict(
257                     actualContent.getResource().getInputStream(),
258                     (int) actualContent.getResource().length()
259             );
260             assertArrayEquals(expectedBytes, actualBytes);
261         }
262     }
263 
264     /**
265      * Get a File object for the given test file.
266      *
267      * @param testFileName Name of test file
268      * @return File for this test file
269      */
270     static File makeTestFileObject(final String testFileName) {
271         return new File(TEST_RESOURCE_DIR + testFileName);
272     }
273 
274     /**
275      * Save the given cache entry serialized to the given file.
276      *
277      * @param serializer Serializer
278      * @param httpCacheStorageEntry Cache entry to serialize and save
279      * @param outFile Output file to write to
280      * @throws Exception if anything goes wrong
281      */
282     static void saveEntryToFile(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final File outFile) throws Exception {
283         final byte[] bytes = serializer.serialize(httpCacheStorageEntry);
284 
285         try (OutputStream out = new FileOutputStream(outFile)) {
286             out.write(bytes);
287         }
288     }
289 
290     /**
291      * Copy bytes from the given input stream to the given destination buffer until the buffer is full,
292      * or end-of-file is reached, and return the number of bytes read.
293      *
294      * @param src Input stream to read from
295      * @param dest Output buffer to write to
296      * @return Number of bytes read
297      * @throws IOException if an I/O error occurs
298      */
299     private static int readFully(final InputStream src, final byte[] dest) throws IOException {
300         final int destPos = 0;
301         final int length = dest.length;
302         int totalBytesRead = 0;
303         int lastBytesRead;
304 
305         while (totalBytesRead < length && (lastBytesRead = src.read(dest, destPos + totalBytesRead, length - totalBytesRead)) != -1) {
306             totalBytesRead += lastBytesRead;
307         }
308         return totalBytesRead;
309     }
310 
311     /**
312      * Copy bytes from the given input stream to a new buffer until the given length is reached,
313      * and returns the new buffer.  If end-of-file is reached first, an IOException is thrown
314      *
315      * @param src Input stream to read from
316      * @param length Maximum bytes to read
317      * @return All bytes from file
318      * @throws IOException if an I/O error occurs or end-of-file is reached before the requested
319      *                     number of bytes have been read
320      */
321     static byte[] readFullyStrict(final InputStream src, final long length) throws IOException {
322         if (length > Integer.MAX_VALUE) {
323             throw new IllegalArgumentException(String.format("Length %d is too large to fit in an array", length));
324         }
325         final int intLength = (int) length;
326         final byte[] dest = new byte[intLength];
327         final int bytesRead = readFully(src, dest);
328 
329         if (bytesRead == intLength) {
330             return dest;
331         } else {
332             throw new IOException(String.format("Expected to read %d bytes but only got %d", intLength, bytesRead));
333         }
334     }
335 }