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.io.UnsupportedEncodingException;
30  import java.net.URI;
31  import java.net.URISyntaxException;
32  import java.net.URLEncoder;
33  import java.nio.charset.StandardCharsets;
34  import java.util.ArrayList;
35  import java.util.Collections;
36  import java.util.Iterator;
37  import java.util.List;
38  
39  import org.apache.hc.client5.http.cache.HeaderConstants;
40  import org.apache.hc.client5.http.cache.HttpCacheEntry;
41  import org.apache.hc.core5.annotation.Contract;
42  import org.apache.hc.core5.annotation.ThreadingBehavior;
43  import org.apache.hc.core5.function.Resolver;
44  import org.apache.hc.core5.http.Header;
45  import org.apache.hc.core5.http.HeaderElement;
46  import org.apache.hc.core5.http.HttpHost;
47  import org.apache.hc.core5.http.HttpRequest;
48  import org.apache.hc.core5.http.message.MessageSupport;
49  
50  /**
51   * @since 4.1
52   */
53  @Contract(threading = ThreadingBehavior.STATELESS)
54  public class CacheKeyGenerator implements Resolver<URI, String> {
55  
56      public static final CacheKeyGenerator INSTANCE = new CacheKeyGenerator();
57  
58      @Override
59      public String resolve(final URI uri) {
60          return generateKey(uri);
61      }
62  
63      /**
64       * Computes a key for the given request {@link URI} that can be used as
65       * a unique identifier for cached resources. The URI is expected to
66       * in an absolute form.
67       *
68       * @param requestUri request URI
69       * @return cache key
70       */
71      public String generateKey(final URI requestUri) {
72          try {
73              final URI normalizeRequestUri = HttpCacheSupport.normalize(requestUri);
74              return normalizeRequestUri.toASCIIString();
75          } catch (final URISyntaxException ex) {
76              return requestUri.toASCIIString();
77          }
78      }
79  
80      /**
81       * Computes a key for the given {@link HttpHost} and {@link HttpRequest}
82       * that can be used as a unique identifier for cached resources.
83       *
84       * @param host The host for this request
85       * @param request the {@link HttpRequest}
86       * @return cache key
87       */
88      public String generateKey(final HttpHost host, final HttpRequest request) {
89          final String s = HttpCacheSupport.getRequestUri(request, host);
90          try {
91              return generateKey(new URI(s));
92          } catch (final URISyntaxException ex) {
93              return s;
94          }
95      }
96  
97      private String getFullHeaderValue(final Header[] headers) {
98          if (headers == null) {
99              return "";
100         }
101         final StringBuilder buf = new StringBuilder();
102         for (int i = 0; i < headers.length; i++) {
103             final Header hdr = headers[i];
104             if (i > 0) {
105                 buf.append(", ");
106             }
107             buf.append(hdr.getValue().trim());
108         }
109         return buf.toString();
110     }
111 
112     /**
113      * Computes a key for the given {@link HttpHost} and {@link HttpRequest}
114      * that can be used as a unique identifier for cached resources. if the request has a
115      * {@literal VARY} header the identifier will also include variant key.
116      *
117      * @param host The host for this request
118      * @param request the {@link HttpRequest}
119      * @param entry the parent entry used to track the variants
120      * @return cache key
121      */
122     public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry) {
123         if (!entry.hasVariants()) {
124             return generateKey(host, request);
125         }
126         return generateVariantKey(request, entry) + generateKey(host, request);
127     }
128 
129     /**
130      * Computes a "variant key" from the headers of a given request that are
131      * covered by the Vary header of a given cache entry. Any request whose
132      * varying headers match those of this request should have the same
133      * variant key.
134      * @param req originating request
135      * @param entry cache entry in question that has variants
136      * @return variant key
137      */
138     public String generateVariantKey(final HttpRequest req, final HttpCacheEntry entry) {
139         final List<String> variantHeaderNames = new ArrayList<>();
140         final Iterator<HeaderElement> it = MessageSupport.iterate(entry, HeaderConstants.VARY);
141         while (it.hasNext()) {
142             final HeaderElement elt = it.next();
143             variantHeaderNames.add(elt.getName());
144         }
145         Collections.sort(variantHeaderNames);
146 
147         final StringBuilder buf;
148         try {
149             buf = new StringBuilder("{");
150             boolean first = true;
151             for (final String headerName : variantHeaderNames) {
152                 if (!first) {
153                     buf.append("&");
154                 }
155                 buf.append(URLEncoder.encode(headerName, StandardCharsets.UTF_8.name()));
156                 buf.append("=");
157                 buf.append(URLEncoder.encode(getFullHeaderValue(req.getHeaders(headerName)),
158                         StandardCharsets.UTF_8.name()));
159                 first = false;
160             }
161             buf.append("}");
162         } catch (final UnsupportedEncodingException uee) {
163             throw new RuntimeException("couldn't encode to UTF-8", uee);
164         }
165         return buf.toString();
166     }
167 
168 }