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.util.Collections;
38  import java.util.Date;
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  
49  import static org.junit.Assert.assertArrayEquals;
50  import static org.junit.Assert.assertEquals;
51  import static org.junit.Assert.assertNull;
52  import static org.junit.Assert.fail;
53  
54  class HttpByteArrayCacheEntrySerializerTestUtils {
55      private final static String TEST_RESOURCE_DIR = "src/test/resources/";
56      static final String TEST_STORAGE_KEY = "xyzzy";
57  
58      /**
59       * Template for incrementally building a new HttpCacheStorageEntry test object, starting from defaults.
60       */
61      static class HttpCacheStorageEntryTestTemplate {
62          Resource resource;
63          Date requestDate;
64          Date responseDate;
65          int responseCode;
66          Header[] responseHeaders;
67          Map<String, String> variantMap;
68          String storageKey;
69  
70          /**
71           * Return a new HttpCacheStorageEntryTestTemplate instance with all default values.
72           *
73           * @return new HttpCacheStorageEntryTestTemplate instance
74           */
75          static HttpCacheStorageEntryTestTemplate makeDefault() {
76              return new HttpCacheStorageEntryTestTemplate(DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE);
77          }
78  
79          /**
80           * Convert this template to a HttpCacheStorageEntry object.
81           * @return HttpCacheStorageEntry object
82           */
83          HttpCacheStorageEntry toEntry() {
84              return new HttpCacheStorageEntry(storageKey,
85                      new HttpCacheEntry(
86                              requestDate,
87                              responseDate,
88                              responseCode,
89                              responseHeaders,
90                              resource,
91                              variantMap));
92          }
93  
94          /**
95           * Create a new template with all null values.
96           */
97          private HttpCacheStorageEntryTestTemplate() {
98          }
99  
100         /**
101          * Create a new template values copied from the given template
102          *
103          * @param src Template to copy values from
104          */
105         private HttpCacheStorageEntryTestTemplate(final HttpCacheStorageEntryTestTemplate src) {
106             this.resource = src.resource;
107             this.requestDate = src.requestDate;
108             this.responseDate = src.responseDate;
109             this.responseCode = src.responseCode;
110             this.responseHeaders = src.responseHeaders;
111             this.variantMap = src.variantMap;
112             this.storageKey = src.storageKey;
113         }
114     }
115 
116     /**
117      * Template with all default values.
118      *
119      * Used by HttpCacheStorageEntryTestTemplate#makeDefault()
120      */
121     private static final HttpCacheStorageEntryTestTemplate DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE = new HttpCacheStorageEntryTestTemplate();
122     static {
123         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.resource = new HeapResource("Hello World".getBytes(StandardCharsets.UTF_8));
124         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.requestDate = new Date(165214800000L);
125         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseDate = new Date(2611108800000L);
126         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseCode = 200;
127         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.responseHeaders = new Header[]{
128                 new BasicHeader("Content-type", "text/html"),
129                 new BasicHeader("Cache-control", "public, max-age=31536000"),
130         };
131         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.variantMap = Collections.emptyMap();
132         DEFAULT_HTTP_CACHE_STORAGE_ENTRY_TEST_TEMPLATE.storageKey = TEST_STORAGE_KEY;
133     }
134 
135     /**
136      * Test serializing and deserializing the given object with the given factory.
137      * <p>
138      * Compares fields to ensure the deserialized object is equivalent to the original object.
139      *
140      * @param serializer Factory for creating serializers
141      * @param httpCacheStorageEntry    Original object to serialize and test against
142      * @throws Exception if anything goes wrong
143      */
144     static void testWithCache(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry) throws Exception {
145         final byte[] testBytes = serializer.serialize(httpCacheStorageEntry);
146         verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, testBytes);
147     }
148 
149     /**
150      * Verify that the given bytes deserialize to the given storage key and an equivalent cache entry.
151      *
152      * @param serializer Deserializer
153      * @param httpCacheStorageEntry Cache entry to verify
154      * @param testBytes Bytes to deserialize
155      * @throws Exception if anything goes wrong
156      */
157     static void verifyHttpCacheEntryFromBytes(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final byte[] testBytes) throws Exception {
158         final HttpCacheStorageEntry testEntry = httpCacheStorageEntryFromBytes(serializer, testBytes);
159 
160         assertCacheEntriesEqual(httpCacheStorageEntry, testEntry);
161     }
162 
163     /**
164      * Verify that the given test file deserializes to a cache entry equivalent to the one given.
165      *
166      * @param serializer Deserializer
167      * @param httpCacheStorageEntry    Cache entry to verify
168      * @param testFileName  Name of test file to deserialize
169      * @param reserializeFiles If true, test files will be regenerated and saved to disk
170      * @throws Exception if anything goes wrong
171      */
172     static void verifyHttpCacheEntryFromTestFile(final HttpCacheEntrySerializer<byte[]> serializer,
173                                                  final HttpCacheStorageEntry httpCacheStorageEntry,
174                                                  final String testFileName,
175                                                  final boolean reserializeFiles) throws Exception {
176         if (reserializeFiles) {
177             final File toFile = makeTestFileObject(testFileName);
178             saveEntryToFile(serializer, httpCacheStorageEntry, toFile);
179         }
180 
181         final byte[] bytes = readTestFileBytes(testFileName);
182 
183         verifyHttpCacheEntryFromBytes(serializer, httpCacheStorageEntry, bytes);
184     }
185 
186     /**
187      * Get the bytes of the given test file.
188      *
189      * @param testFileName Name of test file to get bytes from
190      * @return Bytes from the given test file
191      * @throws Exception if anything goes wrong
192      */
193     static byte[] readTestFileBytes(final String testFileName) throws Exception {
194         final File testFile = makeTestFileObject(testFileName);
195         try(final FileInputStream testStream = new FileInputStream(testFile)) {
196             return readFullyStrict(testStream, testFile.length());
197         }
198     }
199 
200     /**
201      * Create a new cache object from the given bytes.
202      *
203      * @param serializer Deserializer
204      * @param testBytes         Bytes to deserialize
205      * @return Deserialized object
206      */
207     static HttpCacheStorageEntry httpCacheStorageEntryFromBytes(final HttpCacheEntrySerializer<byte[]> serializer, final byte[] testBytes) throws ResourceIOException {
208         return serializer.deserialize(testBytes);
209     }
210 
211     /**
212      * Assert that the given objects are equivalent
213      *
214      * @param expected Expected cache entry object
215      * @param actual   Actual cache entry object
216      * @throws Exception if anything goes wrong
217      */
218     static void assertCacheEntriesEqual(final HttpCacheStorageEntry expected, final HttpCacheStorageEntry actual) throws Exception {
219         assertEquals(expected.getKey(), actual.getKey());
220 
221         final HttpCacheEntry expectedContent = expected.getContent();
222         final HttpCacheEntry actualContent = actual.getContent();
223 
224         assertEquals(expectedContent.getRequestDate(), actualContent.getRequestDate());
225         assertEquals(expectedContent.getResponseDate(), actualContent.getResponseDate());
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("Expected same variantMap values for key '" + key + "'",
231                     expectedContent.getVariantMap().get(key), actualContent.getVariantMap().get(key));
232         }
233 
234         // Verify that the same headers are present on the expected and actual content.
235         for(final Header expectedHeader: expectedContent.getHeaders()) {
236             final Header actualHeader = actualContent.getFirstHeader(expectedHeader.getName());
237 
238             if (actualHeader == null) {
239                 if (expectedHeader.getName().equalsIgnoreCase("content-length")) {
240                     // This header is added by the cache implementation, and can be safely ignored
241                 } else {
242                     fail("Expected header " + expectedHeader.getName() + " was not found");
243                 }
244             } else {
245                 assertEquals(expectedHeader.getName(), actualHeader.getName());
246                 assertEquals(expectedHeader.getValue(), actualHeader.getValue());
247             }
248         }
249 
250         if (expectedContent.getResource() == null) {
251             assertNull("Expected null resource", actualContent.getResource());
252         } else {
253             final byte[] expectedBytes = readFullyStrict(
254                     expectedContent.getResource().getInputStream(),
255                     (int) expectedContent.getResource().length()
256             );
257             final byte[] actualBytes = readFullyStrict(
258                     actualContent.getResource().getInputStream(),
259                     (int) actualContent.getResource().length()
260             );
261             assertArrayEquals(expectedBytes, actualBytes);
262         }
263     }
264 
265     /**
266      * Get a File object for the given test file.
267      *
268      * @param testFileName Name of test file
269      * @return File for this test file
270      */
271     static File makeTestFileObject(final String testFileName) {
272         return new File(TEST_RESOURCE_DIR + testFileName);
273     }
274 
275     /**
276      * Save the given cache entry serialized to the given file.
277      *
278      * @param serializer Serializer
279      * @param httpCacheStorageEntry Cache entry to serialize and save
280      * @param outFile Output file to write to
281      * @throws Exception if anything goes wrong
282      */
283     static void saveEntryToFile(final HttpCacheEntrySerializer<byte[]> serializer, final HttpCacheStorageEntry httpCacheStorageEntry, final File outFile) throws Exception {
284         final byte[] bytes = serializer.serialize(httpCacheStorageEntry);
285 
286         try (OutputStream out = new FileOutputStream(outFile)) {
287             out.write(bytes);
288         }
289     }
290 
291     /**
292      * Copy bytes from the given input stream to the given destination buffer until the buffer is full,
293      * or end-of-file is reached, and return the number of bytes read.
294      *
295      * @param src Input stream to read from
296      * @param dest Output buffer to write to
297      * @return Number of bytes read
298      * @throws IOException if an I/O error occurs
299      */
300     private static int readFully(final InputStream src, final byte[] dest) throws IOException {
301         final int destPos = 0;
302         final int length = dest.length;
303         int totalBytesRead = 0;
304         int lastBytesRead;
305 
306         while (totalBytesRead < length && (lastBytesRead = src.read(dest, destPos + totalBytesRead, length - totalBytesRead)) != -1) {
307             totalBytesRead += lastBytesRead;
308         }
309         return totalBytesRead;
310     }
311 
312     /**
313      * Copy bytes from the given input stream to a new buffer until the given length is reached,
314      * and returns the new buffer.  If end-of-file is reached first, an IOException is thrown
315      *
316      * @param src Input stream to read from
317      * @param length Maximum bytes to read
318      * @return All bytes from file
319      * @throws IOException if an I/O error occurs or end-of-file is reached before the requested
320      *                     number of bytes have been read
321      */
322     static byte[] readFullyStrict(final InputStream src, final long length) throws IOException {
323         if (length > Integer.MAX_VALUE) {
324             throw new IllegalArgumentException(String.format("Length %d is too large to fit in an array", length));
325         }
326         final int intLength = (int) length;
327         final byte[] dest = new byte[intLength];
328         final int bytesRead = readFully(src, dest);
329 
330         if (bytesRead == intLength) {
331             return dest;
332         } else {
333             throw new IOException(String.format("Expected to read %d bytes but only got %d", intLength, bytesRead));
334         }
335     }
336 }