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.impl.cache;
28  
29  import java.net.URI;
30  import java.net.URISyntaxException;
31  import java.nio.charset.StandardCharsets;
32  import java.util.ArrayList;
33  import java.util.Collection;
34  import java.util.Iterator;
35  import java.util.List;
36  import java.util.Locale;
37  import java.util.Spliterator;
38  import java.util.Spliterators;
39  import java.util.concurrent.atomic.AtomicBoolean;
40  import java.util.function.Consumer;
41  import java.util.stream.StreamSupport;
42  
43  import org.apache.hc.client5.http.cache.HttpCacheEntry;
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.function.Resolver;
48  import org.apache.hc.core5.http.Header;
49  import org.apache.hc.core5.http.HeaderElement;
50  import org.apache.hc.core5.http.HttpHeaders;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpRequest;
53  import org.apache.hc.core5.http.MessageHeaders;
54  import org.apache.hc.core5.http.NameValuePair;
55  import org.apache.hc.core5.http.URIScheme;
56  import org.apache.hc.core5.http.message.BasicHeaderElementIterator;
57  import org.apache.hc.core5.http.message.BasicHeaderValueFormatter;
58  import org.apache.hc.core5.http.message.BasicNameValuePair;
59  import org.apache.hc.core5.http.message.MessageSupport;
60  import org.apache.hc.core5.net.PercentCodec;
61  import org.apache.hc.core5.net.URIAuthority;
62  import org.apache.hc.core5.net.URIBuilder;
63  import org.apache.hc.core5.util.Args;
64  import org.apache.hc.core5.util.CharArrayBuffer;
65  import org.apache.hc.core5.util.TextUtils;
66  
67  /**
68   * @since 4.1
69   */
70  @Contract(threading = ThreadingBehavior.STATELESS)
71  public class CacheKeyGenerator implements Resolver<URI, String> {
72  
73      public static final CacheKeyGenerator INSTANCE = new CacheKeyGenerator();
74  
75      @Override
76      public String resolve(final URI uri) {
77          return generateKey(uri);
78      }
79  
80      /**
81       * Returns text representation of the request URI of the given {@link HttpRequest}.
82       * This method will use {@link HttpRequest#getPath()}, {@link HttpRequest#getScheme()} and
83       * {@link HttpRequest#getAuthority()} values when available or attributes of target
84       * {@link HttpHost } in order to construct an absolute URI.
85       * <p>
86       * This method will not attempt to ensure validity of the resultant text representation.
87       *
88       * @param target target host
89       * @param request the {@link HttpRequest}
90       *
91       * @return String the request URI
92       */
93      @Internal
94      public static String getRequestUri(final HttpHost target, final HttpRequest request) {
95          Args.notNull(target, "Target");
96          Args.notNull(request, "HTTP request");
97          final StringBuilder buf = new StringBuilder();
98          final URIAuthority authority = request.getAuthority();
99          if (authority != null) {
100             final String scheme = request.getScheme();
101             buf.append(scheme != null ? scheme : URIScheme.HTTP.id).append("://");
102             buf.append(authority.getHostName());
103             if (authority.getPort() >= 0) {
104                 buf.append(":").append(authority.getPort());
105             }
106         } else {
107             buf.append(target.getSchemeName()).append("://");
108             buf.append(target.getHostName());
109             if (target.getPort() >= 0) {
110                 buf.append(":").append(target.getPort());
111             }
112         }
113         final String path = request.getPath();
114         if (path == null) {
115             buf.append("/");
116         } else {
117             if (buf.length() > 0 && !path.startsWith("/")) {
118                 buf.append("/");
119             }
120             buf.append(path);
121         }
122         return buf.toString();
123     }
124 
125     /**
126      * Returns normalized representation of the request URI optimized for use as a cache key.
127      * This method ensures the resultant URI has an explicit port in the authority component,
128      * and explicit path component and no fragment.
129      */
130     @Internal
131     public static URI normalize(final URI requestUri) throws URISyntaxException {
132         Args.notNull(requestUri, "URI");
133         final URIBuilder builder = new URIBuilder(requestUri);
134         if (builder.getHost() != null) {
135             if (builder.getScheme() == null) {
136                 builder.setScheme(URIScheme.HTTP.id);
137             }
138             if (builder.getPort() <= -1) {
139                 if (URIScheme.HTTP.same(builder.getScheme())) {
140                     builder.setPort(80);
141                 } else if (URIScheme.HTTPS.same(builder.getScheme())) {
142                     builder.setPort(443);
143                 }
144             }
145         }
146         builder.setFragment(null);
147         return builder.optimize().build();
148     }
149 
150     /**
151      * Lenient URI parser that normalizes valid {@link URI}s and returns {@code null} for malformed URIs.
152      */
153     @Internal
154     public static URI normalize(final String requestUri) {
155         if (requestUri == null) {
156             return null;
157         }
158         try {
159             return CacheKeyGenerator.normalize(new URI(requestUri));
160         } catch (final URISyntaxException ex) {
161             return null;
162         }
163     }
164 
165     /**
166      * Computes a key for the given request {@link URI} that can be used as
167      * a unique identifier for cached resources. The URI is expected to
168      * in an absolute form.
169      *
170      * @param requestUri request URI
171      * @return cache key
172      */
173     public String generateKey(final URI requestUri) {
174         try {
175             final URI normalizeRequestUri = normalize(requestUri);
176             return normalizeRequestUri.toASCIIString();
177         } catch (final URISyntaxException ex) {
178             return requestUri.toASCIIString();
179         }
180     }
181 
182     /**
183      * Computes a root key for the given {@link HttpHost} and {@link HttpRequest}
184      * that can be used as a unique identifier for cached resources.
185      *
186      * @param host The host for this request
187      * @param request the {@link HttpRequest}
188      * @return cache key
189      */
190     public String generateKey(final HttpHost host, final HttpRequest request) {
191         final String s = getRequestUri(host, request);
192         try {
193             return generateKey(new URI(s));
194         } catch (final URISyntaxException ex) {
195             return s;
196         }
197     }
198 
199     /**
200      * Returns all variant names contained in {@literal VARY} headers of the given message.
201      *
202      * @since 5.3
203      */
204     public static List<String> variantNames(final MessageHeaders message) {
205         if (message == null) {
206             return null;
207         }
208         final List<String> names = new ArrayList<>();
209         for (final Iterator<Header> it = message.headerIterator(HttpHeaders.VARY); it.hasNext(); ) {
210             final Header header = it.next();
211             MessageSupport.parseTokens(header, names::add);
212         }
213         return names;
214     }
215 
216     @Internal
217     public static void normalizeElements(final MessageHeaders message, final String headerName, final Consumer<String> consumer) {
218         // User-Agent as a special case due to its grammar
219         if (headerName.equalsIgnoreCase(HttpHeaders.USER_AGENT)) {
220             final Header header = message.getFirstHeader(headerName);
221             if (header != null) {
222                 consumer.accept(header.getValue().toLowerCase(Locale.ROOT));
223             }
224         } else {
225             normalizeElements(message.headerIterator(headerName), consumer);
226         }
227     }
228 
229     @Internal
230     public static void normalizeElements(final Iterator<Header> iterator, final Consumer<String> consumer) {
231         final Iterator<HeaderElement> it = new BasicHeaderElementIterator(iterator);
232         StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.NONNULL), false)
233                 .filter(e -> !TextUtils.isBlank(e.getName()))
234                 .map(e -> {
235                     if (e.getValue() == null && e.getParameterCount() == 0) {
236                         return e.getName().toLowerCase(Locale.ROOT);
237                     } else {
238                         final CharArrayBuffer buf = new CharArrayBuffer(1024);
239                         BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
240                                 buf,
241                                 new BasicNameValuePair(
242                                     e.getName().toLowerCase(Locale.ROOT),
243                                     !TextUtils.isBlank(e.getValue()) ? e.getValue() : null),
244                                 false);
245                         if (e.getParameterCount() > 0) {
246                             for (final NameValuePair nvp : e.getParameters()) {
247                                 if (!TextUtils.isBlank(nvp.getName())) {
248                                     buf.append(';');
249                                     BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
250                                             buf,
251                                             new BasicNameValuePair(
252                                                     nvp.getName().toLowerCase(Locale.ROOT),
253                                                     !TextUtils.isBlank(nvp.getValue()) ? nvp.getValue() : null),
254                                             false);
255                                 }
256                             }
257                         }
258                         return buf.toString();
259                     }
260                 })
261                 .sorted()
262                 .distinct()
263                 .forEach(consumer);
264     }
265 
266     /**
267      * Computes a "variant key" for the given request and the given variants.
268      * @param request originating request
269      * @param variantNames variant names
270      * @return variant key
271      *
272      * @since 5.3
273      */
274     public String generateVariantKey(final HttpRequest request, final Collection<String> variantNames) {
275         Args.notNull(variantNames, "Variant names");
276         final StringBuilder buf = new StringBuilder("{");
277         final AtomicBoolean firstHeader = new AtomicBoolean();
278         variantNames.stream()
279                 .map(h -> h.toLowerCase(Locale.ROOT))
280                 .sorted()
281                 .distinct()
282                 .forEach(h -> {
283                     if (!firstHeader.compareAndSet(false, true)) {
284                         buf.append("&");
285                     }
286                     buf.append(PercentCodec.encode(h, StandardCharsets.UTF_8)).append("=");
287                     final AtomicBoolean firstToken = new AtomicBoolean();
288                     normalizeElements(request, h, t -> {
289                         if (!firstToken.compareAndSet(false, true)) {
290                             buf.append(",");
291                         }
292                         buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
293                     });
294                 });
295         buf.append("}");
296         return buf.toString();
297     }
298 
299     /**
300      * Computes a "variant key" from the headers of the given request if the given
301      * cache entry can have variants ({@code Vary} header is present).
302      *
303      * @param request originating request
304      * @param entry cache entry
305      * @return variant key if the given cache entry can have variants, {@code null} otherwise.
306      */
307     public String generateVariantKey(final HttpRequest request, final HttpCacheEntry entry) {
308         if (entry.containsHeader(HttpHeaders.VARY)) {
309             final List<String> variantNames = variantNames(entry);
310             return generateVariantKey(request, variantNames);
311         } else {
312             return null;
313         }
314     }
315 
316     /**
317      * Computes a key for the given {@link HttpHost} and {@link HttpRequest}
318      * that can be used as a unique identifier for cached resources. if the request has a
319      * {@literal VARY} header the identifier will also include variant key.
320      *
321      * @param host The host for this request
322      * @param request the {@link HttpRequest}
323      * @param entry the parent entry used to track the variants
324      * @return cache key
325      *
326      * @deprecated Use {@link #generateKey(HttpHost, HttpRequest)} or {@link #generateVariantKey(HttpRequest, Collection)}
327      */
328     @Deprecated
329     public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry) {
330         final String rootKey = generateKey(host, request);
331         final List<String> variantNames = variantNames(entry);
332         if (variantNames.isEmpty()) {
333             return rootKey;
334         } else {
335             return generateVariantKey(request, variantNames) + rootKey;
336         }
337     }
338 
339 }