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