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  package org.apache.hc.client5.http.cache;
28  
29  import java.io.Serializable;
30  import java.time.Instant;
31  import java.util.Collection;
32  import java.util.Collections;
33  import java.util.Date;
34  import java.util.HashMap;
35  import java.util.HashSet;
36  import java.util.Iterator;
37  import java.util.Map;
38  import java.util.Set;
39  import java.util.concurrent.atomic.AtomicReference;
40  import java.util.stream.Collectors;
41  
42  import org.apache.hc.client5.http.utils.DateUtils;
43  import org.apache.hc.client5.http.validator.ETag;
44  import org.apache.hc.core5.annotation.Contract;
45  import org.apache.hc.core5.annotation.Internal;
46  import org.apache.hc.core5.annotation.ThreadingBehavior;
47  import org.apache.hc.core5.http.Header;
48  import org.apache.hc.core5.http.HttpHeaders;
49  import org.apache.hc.core5.http.HttpStatus;
50  import org.apache.hc.core5.http.MessageHeaders;
51  import org.apache.hc.core5.http.Method;
52  import org.apache.hc.core5.http.ProtocolException;
53  import org.apache.hc.core5.http.message.HeaderGroup;
54  import org.apache.hc.core5.util.Args;
55  
56  /**
57   * Structure used to store an {@link org.apache.hc.core5.http.HttpResponse} in a cache.
58   * Some entries can optionally depend on system resources that may require
59   * explicit deallocation. In such a case {@link #getResource()} should return
60   * a non null instance of {@link Resource} that must be deallocated by calling
61   * {@link Resource#dispose()} method when no longer used.
62   *
63   * @since 4.1
64   */
65  @Contract(threading = ThreadingBehavior.IMMUTABLE)
66  public class HttpCacheEntry implements MessageHeaders, Serializable {
67  
68      private static final long serialVersionUID = -6300496422359477413L;
69  
70      private final Instant requestDate;
71      private final Instant responseDate;
72      private final String method;
73      private final String requestURI;
74      private final HeaderGroup requestHeaders;
75      private final int status;
76      private final HeaderGroup responseHeaders;
77      private final Resource resource;
78      private final Set<String> variants;
79      private final AtomicReference<Instant> dateRef;
80      private final AtomicReference<Instant> expiresRef;
81      private final AtomicReference<Instant> lastModifiedRef;
82  
83      private final AtomicReference<ETag> eTagRef;
84  
85      /**
86       * Internal constructor that makes no validation of the input parameters and makes
87       * no copies of the original client request and the origin response.
88       */
89      @Internal
90      public HttpCacheEntry(
91              final Instant requestDate,
92              final Instant responseDate,
93              final String method,
94              final String requestURI,
95              final HeaderGroup requestHeaders,
96              final int status,
97              final HeaderGroup responseHeaders,
98              final Resource resource,
99              final Collection<String> variants) {
100         super();
101         this.requestDate = requestDate;
102         this.responseDate = responseDate;
103         this.method = method;
104         this.requestURI = requestURI;
105         this.requestHeaders = requestHeaders;
106         this.status = status;
107         this.responseHeaders = responseHeaders;
108         this.resource = resource;
109         this.variants = variants != null ? Collections.unmodifiableSet(new HashSet<>(variants)) : null;
110         this.dateRef = new AtomicReference<>();
111         this.expiresRef = new AtomicReference<>();
112         this.lastModifiedRef = new AtomicReference<>();
113         this.eTagRef = new AtomicReference<>();
114     }
115 
116     /**
117      * Create a new {@link HttpCacheEntry} with variants.
118      * @param requestDate
119      *          Date/time when the request was made (Used for age
120      *            calculations)
121      * @param responseDate
122      *          Date/time that the response came back (Used for age
123      *            calculations)
124      * @param status
125      *          HTTP status from origin response
126      * @param responseHeaders
127      *          Header[] from original HTTP Response
128      * @param resource representing origin response body
129      * @param variantMap describing cache entries that are variants
130      *   of this parent entry; this maps a "variant key" (derived
131      *   from the varying request headers) to a "cache key" (where
132      *   in the cache storage the particular variant is located)
133      * @deprecated  Use {{@link HttpCacheEntryFactory}
134      */
135     @Deprecated
136     public HttpCacheEntry(
137             final Date requestDate,
138             final Date responseDate,
139             final int status,
140             final Header[] responseHeaders,
141             final Resource resource,
142             final Map<String, String> variantMap) {
143         this(DateUtils.toInstant(requestDate), DateUtils.toInstant(responseDate), status, responseHeaders, resource, variantMap);
144     }
145 
146     /**
147      * Create a new {@link HttpCacheEntry} with variants.
148      *
149      * @param requestDate     Date/time when the request was made (Used for age calculations)
150      * @param responseDate    Date/time that the response came back (Used for age calculations)
151      * @param status          HTTP status from origin response
152      * @param responseHeaders Header[] from original HTTP Response
153      * @param resource        representing origin response body
154      * @param variantMap      describing cache entries that are variants of this parent entry; this
155      *                        maps a "variant key" (derived from the varying request headers) to a
156      *                        "cache key" (where in the cache storage the particular variant is
157      *                        located)
158      * @deprecated  Use {{@link HttpCacheEntryFactory}
159      */
160     @Deprecated
161     public HttpCacheEntry(
162             final Instant requestDate,
163             final Instant responseDate,
164             final int status,
165             final Header[] responseHeaders,
166             final Resource resource,
167             final Map<String, String> variantMap) {
168         super();
169         Args.notNull(requestDate, "Request date");
170         Args.notNull(responseDate, "Response date");
171         Args.check(status >= HttpStatus.SC_SUCCESS, "Status code");
172         Args.notNull(responseHeaders, "Response headers");
173         this.requestDate = requestDate;
174         this.responseDate = responseDate;
175         this.method = Method.GET.name();
176         this.requestURI = "/";
177         this.requestHeaders = new HeaderGroup();
178         this.status = status;
179         this.responseHeaders = new HeaderGroup();
180         this.responseHeaders.setHeaders(responseHeaders);
181         this.resource = resource;
182         this.variants = variantMap != null ? Collections.unmodifiableSet(new HashSet<>(variantMap.keySet())) : null;
183         this.dateRef = new AtomicReference<>();
184         this.expiresRef = new AtomicReference<>();
185         this.lastModifiedRef = new AtomicReference<>();
186         this.eTagRef = new AtomicReference<>();
187     }
188 
189     /**
190      * Create a new {@link HttpCacheEntry}.
191      *
192      * @param requestDate     Date/time when the request was made (Used for age calculations)
193      * @param responseDate    Date/time that the response came back (Used for age calculations)
194      * @param status          HTTP status from origin response
195      * @param responseHeaders Header[] from original HTTP Response
196      * @param resource        representing origin response body
197      * @deprecated  Use {{@link HttpCacheEntryFactory}
198      */
199     @Deprecated
200     public HttpCacheEntry(final Date requestDate, final Date responseDate, final int status,
201                           final Header[] responseHeaders, final Resource resource) {
202         this(requestDate, responseDate, status, responseHeaders, resource, new HashMap<>());
203     }
204 
205     /**
206      * Create a new {@link HttpCacheEntry}.
207      *
208      * @param requestDate
209      *          Date/time when the request was made (Used for age
210      *            calculations)
211      * @param responseDate
212      *          Date/time that the response came back (Used for age
213      *            calculations)
214      * @param status
215      *          HTTP status from origin response
216      * @param responseHeaders
217      *          Header[] from original HTTP Response
218      * @param resource representing origin response body
219      *
220      * @deprecated  Use {{@link HttpCacheEntryFactory}
221      */
222     @Deprecated
223     public HttpCacheEntry(final Instant requestDate, final Instant responseDate, final int status,
224                           final Header[] responseHeaders, final Resource resource) {
225         this(requestDate, responseDate, status, responseHeaders, resource, new HashMap<>());
226     }
227 
228     /**
229      * Returns the status from the origin {@link org.apache.hc.core5.http.HttpResponse}.
230      */
231     public int getStatus() {
232         return status;
233     }
234 
235     /**
236      * Returns the time the associated origin request was initiated by the
237      * caching module.
238      * @return {@link Date}
239      * @deprecated USe {@link #getRequestInstant()}
240      */
241     @Deprecated
242     public Date getRequestDate() {
243         return DateUtils.toDate(requestDate);
244     }
245 
246     /**
247      * Returns the time the associated origin request was initiated by the
248      * caching module.
249      * @return {@link Instant}
250      * @since 5.2
251      */
252     public Instant getRequestInstant() {
253         return requestDate;
254     }
255 
256     /**
257      * Returns the time the origin response was received by the caching module.
258      * @return {@link Date}
259      * @deprecated  Use {@link #getResponseInstant()}
260      */
261     @Deprecated
262     public Date getResponseDate() {
263         return DateUtils.toDate(responseDate);
264     }
265 
266     /**
267      * Returns the time the origin response was received by the caching module.
268      *
269      * @return {@link Instant}
270      * @since 5.2
271      */
272     public Instant getResponseInstant() {
273         return responseDate;
274     }
275 
276     /**
277      * Returns all the headers that were on the origin response.
278      */
279     @Override
280     public Header[] getHeaders() {
281         return responseHeaders.getHeaders();
282     }
283 
284     /**
285      * Returns the first header from the origin response with the given
286      * name.
287      */
288     @Override
289     public Header getFirstHeader(final String name) {
290         return responseHeaders.getFirstHeader(name);
291     }
292 
293     /**
294      * @since 5.0
295      */
296     @Override
297     public Header getLastHeader(final String name) {
298         return responseHeaders.getLastHeader(name);
299     }
300 
301     /**
302      * Gets all the headers with the given name that were on the origin
303      * response.
304      */
305     @Override
306     public Header[] getHeaders(final String name) {
307         return responseHeaders.getHeaders(name);
308     }
309 
310     /**
311      * @since 5.0
312      */
313     @Override
314     public boolean containsHeader(final String name) {
315         return responseHeaders.containsHeader(name);
316     }
317 
318     /**
319      * @since 5.0
320      */
321     @Override
322     public int countHeaders(final String name) {
323         return responseHeaders.countHeaders(name);
324     }
325 
326     /**
327      * @since 5.0
328      */
329     @Override
330     public Header getHeader(final String name) throws ProtocolException {
331         return responseHeaders.getHeader(name);
332     }
333 
334     /**
335      * @since 5.0
336      */
337     @Override
338     public Iterator<Header> headerIterator() {
339         return responseHeaders.headerIterator();
340     }
341 
342     /**
343      * @since 5.0
344      */
345     @Override
346     public Iterator<Header> headerIterator(final String name) {
347         return responseHeaders.headerIterator(name);
348     }
349 
350     /**
351      * @since 5.4
352      */
353     public MessageHeaders responseHeaders() {
354         return responseHeaders;
355     }
356 
357     /**
358      * Gets the Date value of the "Date" header or null if the header is missing or cannot be
359      * parsed.
360      *
361      * @since 4.3
362      */
363     public Date getDate() {
364         return DateUtils.toDate(getInstant());
365     }
366 
367     private static final Instant NON_VALUE = Instant.ofEpochSecond(Instant.MIN.getEpochSecond(), 0);
368 
369     private Instant getInstant(final AtomicReference<Instant> ref, final String headerName) {
370         Instant instant = ref.get();
371         if (instant == null) {
372             instant = DateUtils.parseStandardDate(this, headerName);
373             if (instant == null) {
374                 instant = NON_VALUE;
375             }
376             if (!ref.compareAndSet(null, instant)) {
377                 instant = ref.get();
378             }
379         }
380         return instant != null && instant != NON_VALUE ? instant : null;
381     }
382 
383     /**
384      * @since 5.2
385      */
386     public Instant getInstant() {
387         return getInstant(dateRef, HttpHeaders.DATE);
388     }
389 
390     /**
391      * @since 5.4
392      */
393     public Instant getExpires() {
394         return getInstant(expiresRef, HttpHeaders.EXPIRES);
395     }
396 
397     /**
398      * @since 5.4
399      */
400     public Instant getLastModified() {
401         return getInstant(lastModifiedRef, HttpHeaders.LAST_MODIFIED);
402     }
403 
404     /**
405      * @since 5.4
406      */
407     public ETag getETag() {
408         ETag eTag = eTagRef.get();
409         if (eTag == null) {
410             eTag = ETag.get(this);
411             if (eTag == null) {
412                 return null;
413             }
414             if (!eTagRef.compareAndSet(null, eTag)) {
415                 eTag = eTagRef.get();
416             }
417         }
418         return eTag;
419     }
420 
421     /**
422      * Returns the {@link Resource} containing the origin response body.
423      */
424     public Resource getResource() {
425         return this.resource;
426     }
427 
428     /**
429      * Indicates whether the origin response indicated the associated
430      * resource had variants (i.e. that the Vary header was set on the
431      * origin response).
432      */
433     public boolean hasVariants() {
434         return variants != null;
435     }
436 
437     /**
438      * Returns all known variants.
439      *
440      * @since 5.4
441      */
442     public Set<String> getVariants() {
443         return variants != null ? variants : Collections.emptySet();
444     }
445 
446     /**
447      * @deprecated No longer applicable. Use {@link #getVariants()} instead.
448      */
449     @Deprecated
450     public Map<String, String> getVariantMap() {
451         return variants != null ? variants.stream()
452                 .collect(Collectors.toMap(e -> e, e -> e + requestURI)) : Collections.emptyMap();
453     }
454 
455     /**
456      * Returns the HTTP request method that was used to create the cached
457      * response entry.
458      *
459      * @since 4.4
460      */
461     public String getRequestMethod() {
462         return method;
463     }
464 
465     /**
466      * @since 5.4
467      */
468     public String getRequestURI() {
469         return requestURI;
470     }
471 
472     /**
473      * @since 5.4
474      */
475     public MessageHeaders requestHeaders() {
476         return requestHeaders;
477     }
478 
479     /**
480      * @since 5.4
481      */
482     public Iterator<Header> requestHeaderIterator() {
483         return requestHeaders.headerIterator();
484     }
485 
486     /**
487      * @since 5.4
488      */
489     public Iterator<Header> requestHeaderIterator(final String headerName) {
490         return requestHeaders.headerIterator(headerName);
491     }
492 
493     /**
494      * Tests if the given {@link HttpCacheEntry} is newer than the given {@link MessageHeaders}
495      * by comparing values of their {@literal DATE} header. In case the given entry, or the message,
496      * or their {@literal DATE} headers are null, this method returns {@code false}.
497      *
498      * @since 5.4
499      */
500     public static boolean isNewer(final HttpCacheEntry entry, final MessageHeaders message) {
501         if (entry == null || message == null) {
502             return false;
503         }
504         final Instant cacheDate = entry.getInstant();
505         if (cacheDate == null) {
506             return false;
507         }
508         final Instant messageDate = DateUtils.parseStandardDate(message, HttpHeaders.DATE);
509         if (messageDate == null) {
510             return false;
511         }
512         return cacheDate.compareTo(messageDate) > 0;
513     }
514 
515     /**
516      * Provides a string representation of this instance suitable for
517      * human consumption.
518      */
519     @Override
520     public String toString() {
521         return "HttpCacheEntry{" +
522                 "requestDate=" + requestDate +
523                 ", responseDate=" + responseDate +
524                 ", method='" + method + '\'' +
525                 ", requestURI='" + requestURI + '\'' +
526                 ", requestHeaders=" + requestHeaders +
527                 ", status=" + status +
528                 ", responseHeaders=" + responseHeaders +
529                 ", resource=" + resource +
530                 ", variants=" + variants +
531                 '}';
532     }
533 }