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