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