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.net.URI;
30  import java.time.Instant;
31  import java.util.Collection;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.Locale;
35  import java.util.Set;
36  
37  import org.apache.hc.client5.http.impl.cache.CacheKeyGenerator;
38  import org.apache.hc.client5.http.utils.DateUtils;
39  import org.apache.hc.core5.annotation.Contract;
40  import org.apache.hc.core5.annotation.ThreadingBehavior;
41  import org.apache.hc.core5.http.Header;
42  import org.apache.hc.core5.http.HttpHeaders;
43  import org.apache.hc.core5.http.HttpHost;
44  import org.apache.hc.core5.http.HttpMessage;
45  import org.apache.hc.core5.http.HttpRequest;
46  import org.apache.hc.core5.http.HttpResponse;
47  import org.apache.hc.core5.http.HttpStatus;
48  import org.apache.hc.core5.http.message.BasicHeader;
49  import org.apache.hc.core5.http.message.HeaderGroup;
50  import org.apache.hc.core5.http.message.MessageSupport;
51  import org.apache.hc.core5.util.Args;
52  
53  /**
54   * {@link HttpCacheEntry} factory.
55   *
56   * @since 5.4
57   */
58  @Contract(threading = ThreadingBehavior.IMMUTABLE)
59  public class HttpCacheEntryFactory {
60  
61      /**
62       * Default instance of {@link HttpCacheEntryFactory}.
63       */
64      public static final HttpCacheEntryFactory INSTANCE = new HttpCacheEntryFactory();
65  
66      private static HeaderGroup headers(final Iterator<Header> it) {
67          final HeaderGroup headerGroup = new HeaderGroup();
68          while (it.hasNext()) {
69              headerGroup.addHeader(it.next());
70          }
71          return headerGroup;
72      }
73  
74      HeaderGroup mergeHeaders(final HttpCacheEntry entry, final HttpResponse response) {
75          final HeaderGroup headerGroup = new HeaderGroup();
76          for (final Iterator<Header> it = entry.headerIterator(); it.hasNext(); ) {
77              final Header entryHeader = it.next();
78              final String headerName = entryHeader.getName();
79              if (!response.containsHeader(headerName)) {
80                  headerGroup.addHeader(entryHeader);
81              }
82          }
83          final Set<String> responseHopByHop = MessageSupport.hopByHopConnectionSpecific(response);
84          for (final Iterator<Header> it = response.headerIterator(); it.hasNext(); ) {
85              final Header responseHeader = it.next();
86              final String headerName = responseHeader.getName();
87              if (!responseHopByHop.contains(headerName.toLowerCase(Locale.ROOT))) {
88                  headerGroup.addHeader(responseHeader);
89              }
90          }
91          return headerGroup;
92      }
93  
94      /**
95       * This method should be provided by the core
96       */
97      static HeaderGroup filterHopByHopHeaders(final HttpMessage message) {
98          final Set<String> hopByHop = MessageSupport.hopByHopConnectionSpecific(message);
99          final HeaderGroup headerGroup = new HeaderGroup();
100         for (final Iterator<Header> it = message.headerIterator(); it.hasNext(); ) {
101             final Header header = it.next();
102             if (!hopByHop.contains(header.getName())) {
103                 headerGroup.addHeader(header);
104             }
105         }
106         return headerGroup;
107     }
108 
109     static void ensureDate(final HeaderGroup headers, final Instant instant) {
110         if (!headers.containsHeader(HttpHeaders.DATE)) {
111             headers.addHeader(new BasicHeader(HttpHeaders.DATE, DateUtils.formatStandardDate(instant)));
112         }
113     }
114 
115     /**
116      * Creates a new root {@link HttpCacheEntry} (parent of multiple variants).
117      *
118      * @param latestVariant    The most recently created variant entry
119      * @param variants         describing cache entries that are variants of this parent entry; this
120      *                         maps a "variant key" (derived from the varying request headers) to a
121      *                         "cache key" (where in the cache storage the particular variant is
122      *                         located)
123      */
124     public HttpCacheEntry createRoot(final HttpCacheEntry latestVariant,
125                                      final Collection<String> variants) {
126         Args.notNull(latestVariant, "Request");
127         Args.notNull(variants, "Variants");
128         return new HttpCacheEntry(
129                 latestVariant.getRequestInstant(),
130                 latestVariant.getResponseInstant(),
131                 latestVariant.getRequestMethod(),
132                 latestVariant.getRequestURI(),
133                 headers(latestVariant.requestHeaderIterator()),
134                 latestVariant.getStatus(),
135                 headers(latestVariant.headerIterator()),
136                 null,
137                 variants);
138     }
139 
140     /**
141      * Create a new {@link HttpCacheEntry} with the given {@link Resource}.
142      *
143      * @param requestInstant   Date/time when the request was made (Used for age calculations)
144      * @param responseInstant  Date/time that the response came back (Used for age calculations)
145      * @param host             Target host
146      * @param request          Original client request (a deep copy of this object is made)
147      * @param response         Origin response (a deep copy of this object is made)
148      * @param resource         Resource representing origin response body
149      */
150     public HttpCacheEntry create(final Instant requestInstant,
151                                  final Instant responseInstant,
152                                  final HttpHost host,
153                                  final HttpRequest request,
154                                  final HttpResponse response,
155                                  final Resource resource) {
156         Args.notNull(requestInstant, "Request instant");
157         Args.notNull(responseInstant, "Response instant");
158         Args.notNull(host, "Host");
159         Args.notNull(request, "Request");
160         Args.notNull(response, "Origin response");
161         final String s = CacheKeyGenerator.getRequestUri(host, request);
162         final URI uri = CacheKeyGenerator.normalize(s);
163         final HeaderGroup requestHeaders = filterHopByHopHeaders(request);
164         // Strip AUTHORIZATION from request headers
165         requestHeaders.removeHeaders(HttpHeaders.AUTHORIZATION);
166         final HeaderGroup responseHeaders = filterHopByHopHeaders(response);
167         ensureDate(responseHeaders, responseInstant);
168         return new HttpCacheEntry(
169                 requestInstant,
170                 responseInstant,
171                 request.getMethod(),
172                 uri.toASCIIString(),
173                 requestHeaders,
174                 response.getCode(),
175                 responseHeaders,
176                 resource,
177                 null);
178     }
179 
180     /**
181      * Creates updated entry with the new information from the response. Should only be used for
182      * 304 responses.
183      *
184      * @param requestInstant   Date/time when the request was made (Used for age calculations)
185      * @param responseInstant  Date/time that the response came back (Used for age calculations)
186      * @param host             Target host
187      * @param request          Original client request (a deep copy of this object is made)
188      * @param response         Origin response (a deep copy of this object is made)
189      * @param entry            Existing cache entry.
190      */
191     public HttpCacheEntry createUpdated(
192             final Instant requestInstant,
193             final Instant responseInstant,
194             final HttpHost host,
195             final HttpRequest request,
196             final HttpResponse response,
197             final HttpCacheEntry entry) {
198         Args.notNull(requestInstant, "Request instant");
199         Args.notNull(responseInstant, "Response instant");
200         Args.notNull(response, "Origin response");
201         Args.check(response.getCode() == HttpStatus.SC_NOT_MODIFIED,
202                 "Response must have 304 status code");
203         Args.notNull(entry, "Cache entry");
204         if (HttpCacheEntry.isNewer(entry, response)) {
205             return entry;
206         }
207         final String s = CacheKeyGenerator.getRequestUri(host, request);
208         final URI uri = CacheKeyGenerator.normalize(s);
209         final HeaderGroup requestHeaders = filterHopByHopHeaders(request);
210         // Strip AUTHORIZATION from request headers
211         requestHeaders.removeHeaders(HttpHeaders.AUTHORIZATION);
212         final HeaderGroup mergedHeaders = mergeHeaders(entry, response);
213         return new HttpCacheEntry(
214                 requestInstant,
215                 responseInstant,
216                 request.getMethod(),
217                 uri.toASCIIString(),
218                 requestHeaders,
219                 entry.getStatus(),
220                 mergedHeaders,
221                 entry.getResource(),
222                 null);
223     }
224 
225     /**
226      * Creates a copy of the given {@link HttpCacheEntry}. Please note the underlying
227      * {@link Resource} is copied by reference.
228      */
229     public HttpCacheEntry copy(final HttpCacheEntry entry) {
230         if (entry == null) {
231             return null;
232         }
233         return new HttpCacheEntry(
234                 entry.getRequestInstant(),
235                 entry.getResponseInstant(),
236                 entry.getRequestMethod(),
237                 entry.getRequestURI(),
238                 headers(entry.requestHeaderIterator()),
239                 entry.getStatus(),
240                 headers(entry.headerIterator()),
241                 entry.getResource(),
242                 entry.hasVariants() ? new HashSet<>(entry.getVariants()) : null);
243     }
244 
245 }