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.IOException;
31  import java.io.InputStream;
32  import java.net.URL;
33  import java.nio.charset.StandardCharsets;
34  import java.time.Instant;
35  import java.util.HashSet;
36  import java.util.Set;
37  
38  import org.apache.hc.client5.http.cache.HttpCacheEntry;
39  import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
40  import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
41  import org.apache.hc.client5.http.cache.ResourceIOException;
42  import org.apache.hc.core5.http.ContentType;
43  import org.apache.hc.core5.http.HttpHeaders;
44  import org.apache.hc.core5.http.HttpStatus;
45  import org.apache.hc.core5.http.message.BasicHeader;
46  import org.apache.hc.core5.util.ByteArrayBuffer;
47  import org.hamcrest.MatcherAssert;
48  import org.hamcrest.Matchers;
49  import org.junit.jupiter.api.Assertions;
50  import org.junit.jupiter.api.BeforeEach;
51  import org.junit.jupiter.api.Test;
52  
53  public class TestHttpByteArrayCacheEntrySerializer {
54  
55      private HttpCacheEntrySerializer<byte[]> httpCacheEntrySerializer;
56  
57      @BeforeEach
58      public void before() {
59          httpCacheEntrySerializer = HttpByteArrayCacheEntrySerializer.INSTANCE;
60      }
61  
62      @Test
63      public void testSimpleSerializeAndDeserialize() throws Exception {
64          final String content = "Hello World";
65          final ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8);
66          final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
67                  "GET", "/stuff", HttpTestUtils.headers(),
68                  HttpStatus.SC_OK, HttpTestUtils.headers(new BasicHeader(HttpHeaders.CONTENT_TYPE, contentType.toString())),
69                  new HeapResource(content.getBytes(contentType.getCharset())),
70                  null);
71          final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
72          final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
73  
74          final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
75          MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
76          MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
77      }
78  
79      @Test
80      public void testSerializeAndDeserializeLargeContent() throws Exception {
81          final ContentType contentType = ContentType.IMAGE_JPEG;
82          final HeapResource resource = load(getClass().getResource("/ApacheLogo.png"));
83          final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
84                  "GET", "/stuff", HttpTestUtils.headers(),
85                  HttpStatus.SC_OK, HttpTestUtils.headers(new BasicHeader(HttpHeaders.CONTENT_TYPE, contentType.toString())),
86                  resource,
87                  null);
88          final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
89          final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
90  
91          final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
92          MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
93          MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
94      }
95  
96      /**
97       * Deserialize a cache entry in a bad format, expecting an exception.
98       */
99      @Test
100     public void testInvalidCacheEntry() throws Exception {
101         // This file is a JPEG not a cache entry, so should fail to deserialize
102         final HeapResource resource = load(getClass().getResource("/ApacheLogo.png"));
103         Assertions.assertThrows(ResourceIOException.class, () ->
104                 httpCacheEntrySerializer.deserialize(resource.get()));
105     }
106 
107     /**
108      * Deserialize truncated cache entries.
109      */
110     @Test
111     public void testTruncatedCacheEntry() throws Exception {
112         final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
113                 "HC-Key: unique-cache-key\n" +
114                 "HC-Resource-Length: 11\n" +
115                 "HC-Request-Instant: 1686210849596\n" +
116                 "HC-Response-Instant: 1686210849596\n" +
117                 "\n" +
118                 "GET /stuff HTTP/1.1\n" +
119                 "\n" +
120                 "HTTP/1.1 200 \n" +
121                 "Content-Type: text/plain; charset=UTF-8\n" +
122                 "Cache-control: public, max-age=31536000\n" +
123                 "\n" +
124                 "Huh?";
125         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
126         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
127                 httpCacheEntrySerializer.deserialize(bytes1));
128         Assertions.assertEquals("Unexpected end of cache content", exception1.getMessage());
129 
130         final String content2 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
131                 "HC-Key: unique-cache-key\n" +
132                 "HC-Resource-Length: 11\n" +
133                 "HC-Request-Instant: 1686210849596\n" +
134                 "HC-Response-Instant: 1686210849596\n" +
135                 "\n" +
136                 "GET /stuff HTTP/1.1\n" +
137                 "\n" +
138                 "HTTP/1.1 200 \n" +
139                 "Content-Type: text/plain; charset=UTF-8\n" +
140                 "Cache-control: public, max-age=31536000\n";
141         final byte[] bytes2 = content2.getBytes(StandardCharsets.UTF_8);
142         final ResourceIOException exception2 = Assertions.assertThrows(ResourceIOException.class, () ->
143                 httpCacheEntrySerializer.deserialize(bytes2));
144         Assertions.assertEquals("Unexpected end of stream", exception2.getMessage());
145 
146         final String content3 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
147                 "HC-Key: unique-cache-key\n" +
148                 "HC-Resource-Length: 11\n" +
149                 "HC-Request-Instant: 1686210849596\n" +
150                 "HC-Response-Instant: 1686210849596\n" +
151                 "\n" +
152                 "GET /stuff HTTP/1.1\n" +
153                 "\n";
154         final byte[] bytes3 = content3.getBytes(StandardCharsets.UTF_8);
155         final ResourceIOException exception3 = Assertions.assertThrows(ResourceIOException.class, () ->
156                 httpCacheEntrySerializer.deserialize(bytes3));
157         Assertions.assertEquals("Unexpected end of stream", exception3.getMessage());
158 
159         final String content4 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
160                 "HC-Key: unique-cache-key\n" +
161                 "HC-Resource-Length: 11\n" +
162                 "HC-Request-Instant: 1686210849596\n" +
163                 "HC-Response-Instant: 1686210849596\n";
164         final byte[] bytes4 = content4.getBytes(StandardCharsets.UTF_8);
165         final ResourceIOException exception4 = Assertions.assertThrows(ResourceIOException.class, () ->
166                 httpCacheEntrySerializer.deserialize(bytes4));
167         Assertions.assertEquals("Unexpected end of stream", exception4.getMessage());
168 
169         final String content5 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
170                 "HC-Key: unique-cache-key\n";
171         final byte[] bytes5 = content5.getBytes(StandardCharsets.UTF_8);
172         final ResourceIOException exception5 = Assertions.assertThrows(ResourceIOException.class, () ->
173                 httpCacheEntrySerializer.deserialize(bytes5));
174         Assertions.assertEquals("Unexpected end of stream", exception5.getMessage());
175 
176         final String content6 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n";
177         final byte[] bytes6 = content6.getBytes(StandardCharsets.UTF_8);
178         final ResourceIOException exception6 = Assertions.assertThrows(ResourceIOException.class, () ->
179                 httpCacheEntrySerializer.deserialize(bytes6));
180         Assertions.assertEquals("Unexpected end of stream", exception6.getMessage());
181 
182         final String content7 = "HttpClient CacheEntry 1\n";
183         final byte[] bytes7 = content7.getBytes(StandardCharsets.UTF_8);
184         final ResourceIOException exception7 = Assertions.assertThrows(ResourceIOException.class, () ->
185                 httpCacheEntrySerializer.deserialize(bytes7));
186         Assertions.assertEquals("Unexpected cache entry version line", exception7.getMessage());
187     }
188 
189     /**
190      * Deserialize cache entries with a missing mandatory header.
191      */
192     @Test
193     public void testMissingHeaderCacheEntry() throws Exception {
194         final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
195                 "HC-Key: unique-cache-key\n" +
196                 "HC-Resource-Length: 11\n" +
197                 "HC-Response-Instant: 1686210849596\n" +
198                 "\n" +
199                 "GET /stuff HTTP/1.1\n" +
200                 "\n" +
201                 "HTTP/1.1 200 \n" +
202                 "Content-Type: text/plain; charset=UTF-8\n" +
203                 "Cache-control: public, max-age=31536000\n" +
204                 "\n" +
205                 "Hello World";
206         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
207         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
208                 httpCacheEntrySerializer.deserialize(bytes1));
209         Assertions.assertEquals("Invalid cache header format", exception1.getMessage());
210 
211         final String content2 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
212                 "HC-Key: unique-cache-key\n" +
213                 "HC-Resource-Length: 11\n" +
214                 "HC-Request-Instant: 1686210849596\n" +
215                 "\n" +
216                 "GET /stuff HTTP/1.1\n" +
217                 "\n" +
218                 "HTTP/1.1 200 \n" +
219                 "Content-Type: text/plain; charset=UTF-8\n" +
220                 "Cache-control: public, max-age=31536000\n" +
221                 "\n" +
222                 "Hello World";
223         final byte[] bytes2 = content2.getBytes(StandardCharsets.UTF_8);
224         final ResourceIOException exception2 = Assertions.assertThrows(ResourceIOException.class, () ->
225                 httpCacheEntrySerializer.deserialize(bytes2));
226         Assertions.assertEquals("Invalid cache header format", exception2.getMessage());
227     }
228 
229     /**
230      * Deserialize cache entries with an invalid header value.
231      */
232     @Test
233     public void testInvalidHeaderCacheEntry() throws Exception {
234         final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
235                 "HC-Key: unique-cache-key\n" +
236                 "HC-Resource-Length: 11\n" +
237                 "HC-Request-Instant: boom\n" +
238                 "HC-Response-Instant: 1686210849596\n" +
239                 "\n" +
240                 "GET /stuff HTTP/1.1\n" +
241                 "\n" +
242                 "HTTP/1.1 200 \n" +
243                 "Content-Type: text/plain; charset=UTF-8\n" +
244                 "Cache-control: public, max-age=31536000\n" +
245                 "\n" +
246                 "Hello World";
247         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
248         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
249                 httpCacheEntrySerializer.deserialize(bytes1));
250         Assertions.assertEquals("Invalid cache header format", exception1.getMessage());
251         final String content2 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
252                 "HC-Key: unique-cache-key\n" +
253                 "HC-Resource-Length: 11\n" +
254                 "HC-Request-Instant: 1686210849596\n" +
255                 "HC-Response-Instant: boom\n" +
256                 "\n" +
257                 "GET /stuff HTTP/1.1\n" +
258                 "\n" +
259                 "HTTP/1.1 200 \n" +
260                 "Content-Type: text/plain; charset=UTF-8\n" +
261                 "Cache-control: public, max-age=31536000\n" +
262                 "\n" +
263                 "Hello World";
264         final byte[] bytes2 = content1.getBytes(StandardCharsets.UTF_8);
265         final ResourceIOException exception2 = Assertions.assertThrows(ResourceIOException.class, () ->
266                 httpCacheEntrySerializer.deserialize(bytes2));
267         Assertions.assertEquals("Invalid cache header format", exception2.getMessage());
268     }
269 
270     /**
271      * Deserialize cache entries with an invalid request line.
272      */
273     @Test
274     public void testInvalidRequestLineCacheEntry() throws Exception {
275         final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
276                 "HC-Key: unique-cache-key\n" +
277                 "HC-Resource-Length: 11\n" +
278                 "HC-Request-Instant: 1686210849596\n" +
279                 "HC-Response-Instant: 1686210849596\n" +
280                 "\n" +
281                 "GET boom\n" +
282                 "\n" +
283                 "HTTP/1.1 200 \n" +
284                 "Content-Type: text/plain; charset=UTF-8\n" +
285                 "Cache-control: public, max-age=31536000\n" +
286                 "\n" +
287                 "Hello World";
288         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
289         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
290                 httpCacheEntrySerializer.deserialize(bytes1));
291         Assertions.assertEquals("Invalid cache header format", exception1.getMessage());
292     }
293 
294     /**
295      * Deserialize cache entries with an invalid request line.
296      */
297     @Test
298     public void testInvalidStatusLineCacheEntry() throws Exception {
299         final String content1 = HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
300                 "HC-Key: unique-cache-key\n" +
301                 "HC-Resource-Length: 11\n" +
302                 "HC-Request-Instant: 1686210849596\n" +
303                 "HC-Response-Instant: 1686210849596\n" +
304                 "\n" +
305                 "GET /stuff HTTP/1.1\n" +
306                 "\n" +
307                 "HTTP/1.1 boom \n" +
308                 "Content-Type: text/plain; charset=UTF-8\n" +
309                 "Cache-control: public, max-age=31536000\n" +
310                 "\n" +
311                 "Hello World";
312         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
313         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
314                 httpCacheEntrySerializer.deserialize(bytes1));
315         Assertions.assertEquals("Invalid cache header format", exception1.getMessage());
316     }
317 
318     /**
319      * Serialize and deserialize a cache entry with no headers.
320      */
321     @Test
322     public void noHeadersTest() throws Exception {
323         final String content = "Hello World";
324         final ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8);
325         final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
326                 "GET", "/stuff", HttpTestUtils.headers(),
327                 HttpStatus.SC_OK, HttpTestUtils.headers(),
328                 new HeapResource(content.getBytes(contentType.getCharset())),
329                 null);
330         final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
331         final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
332 
333         final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
334         MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
335         MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
336     }
337 
338     /**
339      * Serialize and deserialize a cache entry with an empty body.
340      */
341     @Test
342     public void emptyBodyTest() throws Exception {
343         final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
344                 "GET", "/stuff", HttpTestUtils.headers(),
345                 HttpStatus.SC_OK, HttpTestUtils.headers(),
346                 new HeapResource(new byte[] {}),
347                 null);
348         final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
349         final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
350 
351         final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
352         MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
353         MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
354     }
355 
356     /**
357      * Serialize and deserialize a cache entry with no body.
358      */
359     @Test
360     public void noBodyTest() throws Exception {
361         final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
362                 "GET", "/stuff", HttpTestUtils.headers(),
363                 HttpStatus.SC_OK, HttpTestUtils.headers(),
364                 null,
365                 null);
366         final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
367         final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
368 
369         final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
370         MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
371         MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
372     }
373 
374     /**
375      * Serialize and deserialize a cache entry with a variant map.
376      */
377     @Test
378     public void testSimpleVariantMap() throws Exception {
379         final String content = "Hello World";
380         final ContentType contentType = ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8);
381         final Set<String> variants = new HashSet<>();
382         variants.add("{Accept-Encoding=gzip}");
383         variants.add("{Accept-Encoding=compress}");
384         final HttpCacheEntry cacheEntry = new HttpCacheEntry(Instant.now(), Instant.now(),
385                 "GET", "/stuff", HttpTestUtils.headers(),
386                 HttpStatus.SC_OK, HttpTestUtils.headers(new BasicHeader(HttpHeaders.CONTENT_TYPE, contentType.toString())),
387                 new HeapResource(content.getBytes(contentType.getCharset())),
388                 variants);
389         final HttpCacheStorageEntry storageEntry = new HttpCacheStorageEntry("unique-cache-key", cacheEntry);
390         final byte[] serialized = httpCacheEntrySerializer.serialize(storageEntry);
391 
392         final HttpCacheStorageEntry deserialized = httpCacheEntrySerializer.deserialize(serialized);
393         MatcherAssert.assertThat(deserialized.getKey(), Matchers.equalTo(storageEntry.getKey()));
394         MatcherAssert.assertThat(deserialized.getContent(), HttpCacheEntryMatcher.equivalent(storageEntry.getContent()));
395     }
396 
397     /**
398      * Deserialize cache entries with trailing garbage.
399      */
400     @Test
401     public void testDeserializeCacheEntryWithTrailingGarbage() throws Exception {
402         final String content1 =HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
403                 "HC-Key: unique-cache-key\n" +
404                         "HC-Resource-Length: 11\n" +
405                         "HC-Request-Instant: 1686210849596\n" +
406                         "HC-Response-Instant: 1686210849596\n" +
407                         "\n" +
408                         "GET /stuff HTTP/1.1\n" +
409                         "\n" +
410                         "HTTP/1.1 200 \n" +
411                         "Content-Type: text/plain; charset=UTF-8\n" +
412                         "Cache-control: public, max-age=31536000\n" +
413                         "\n" +
414                         "Hello World..... Rubbish";
415         final byte[] bytes1 = content1.getBytes(StandardCharsets.UTF_8);
416         final ResourceIOException exception1 = Assertions.assertThrows(ResourceIOException.class, () ->
417                 httpCacheEntrySerializer.deserialize(bytes1));
418         Assertions.assertEquals("Unexpected content at the end of cache content", exception1.getMessage());
419 
420         final String content2 =HttpByteArrayCacheEntrySerializer.HC_CACHE_VERSION_LINE + "\n" +
421                 "HC-Key: unique-cache-key\n" +
422                         "HC-Request-Instant: 1686210849596\n" +
423                         "HC-Response-Instant: 1686210849596\n" +
424                         "\n" +
425                         "GET /stuff HTTP/1.1\n" +
426                         "\n" +
427                         "HTTP/1.1 200 \n" +
428                         "Content-Type: text/plain; charset=UTF-8\n" +
429                         "Cache-control: public, max-age=31536000\n" +
430                         "\n" +
431                         "Rubbish";
432         final byte[] bytes2 = content2.getBytes(StandardCharsets.UTF_8);
433         final ResourceIOException exception2 = Assertions.assertThrows(ResourceIOException.class, () ->
434                 httpCacheEntrySerializer.deserialize(bytes2));
435         Assertions.assertEquals("Unexpected content at the end of cache content", exception1.getMessage());
436     }
437 
438     static HeapResource load(final URL resource) throws IOException {
439         try (final InputStream in = resource.openStream()) {
440             final ByteArrayBuffer buf = new ByteArrayBuffer(1024);
441             final byte[] tmp = new byte[2048];
442             int len;
443             while ((len = in.read(tmp)) != -1) {
444                 buf.append(tmp, 0, len);
445             }
446             return new HeapResource(buf.toByteArray());
447         }
448     }
449 
450 }