1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
69
70 @Contract(threading = ThreadingBehavior.STATELESS)
71 public class CacheKeyGenerator implements Resolver<URI, String> {
72
73
74
75
76 public static final CacheKeyGenerator INSTANCE = new CacheKeyGenerator();
77
78 @Override
79 public String resolve(final URI uri) {
80 return generateKey(uri);
81 }
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96 @Internal
97 public static String getRequestUri(final HttpHost target, final HttpRequest request) {
98 Args.notNull(target, "Target");
99 Args.notNull(request, "HTTP request");
100 final StringBuilder buf = new StringBuilder();
101 final URIAuthority authority = request.getAuthority();
102 if (authority != null) {
103 final String scheme = request.getScheme();
104 buf.append(scheme != null ? scheme : URIScheme.HTTP.id).append("://");
105 buf.append(authority.getHostName());
106 if (authority.getPort() >= 0) {
107 buf.append(":").append(authority.getPort());
108 }
109 } else {
110 buf.append(target.getSchemeName()).append("://");
111 buf.append(target.getHostName());
112 if (target.getPort() >= 0) {
113 buf.append(":").append(target.getPort());
114 }
115 }
116 final String path = request.getPath();
117 if (path == null) {
118 buf.append("/");
119 } else {
120 if (buf.length() > 0 && !path.startsWith("/")) {
121 buf.append("/");
122 }
123 buf.append(path);
124 }
125 return buf.toString();
126 }
127
128
129
130
131
132
133 @Internal
134 public static URI normalize(final URI requestUri) throws URISyntaxException {
135 Args.notNull(requestUri, "URI");
136 final URIBuilder builder = new URIBuilder(requestUri);
137 if (builder.getHost() != null) {
138 if (builder.getScheme() == null) {
139 builder.setScheme(URIScheme.HTTP.id);
140 }
141 if (builder.getPort() <= -1) {
142 if (URIScheme.HTTP.same(builder.getScheme())) {
143 builder.setPort(80);
144 } else if (URIScheme.HTTPS.same(builder.getScheme())) {
145 builder.setPort(443);
146 }
147 }
148 }
149 builder.setFragment(null);
150 return builder.optimize().build();
151 }
152
153
154
155
156 @Internal
157 public static URI normalize(final String requestUri) {
158 if (requestUri == null) {
159 return null;
160 }
161 try {
162 return CacheKeyGenerator.normalize(new URI(requestUri));
163 } catch (final URISyntaxException ex) {
164 return null;
165 }
166 }
167
168
169
170
171
172
173
174
175
176 public String generateKey(final URI requestUri) {
177 try {
178 final URI normalizeRequestUri = normalize(requestUri);
179 return normalizeRequestUri.toASCIIString();
180 } catch (final URISyntaxException ex) {
181 return requestUri.toASCIIString();
182 }
183 }
184
185
186
187
188
189
190
191
192
193 public String generateKey(final HttpHost host, final HttpRequest request) {
194 final String s = getRequestUri(host, request);
195 try {
196 return generateKey(new URI(s));
197 } catch (final URISyntaxException ex) {
198 return s;
199 }
200 }
201
202
203
204
205
206
207 public static List<String> variantNames(final MessageHeaders message) {
208 if (message == null) {
209 return null;
210 }
211 final List<String> names = new ArrayList<>();
212 for (final Iterator<Header> it = message.headerIterator(HttpHeaders.VARY); it.hasNext(); ) {
213 final Header header = it.next();
214 MessageSupport.parseTokens(header, names::add);
215 }
216 return names;
217 }
218
219 @Internal
220 public static void normalizeElements(final MessageHeaders message, final String headerName, final Consumer<String> consumer) {
221
222 if (headerName.equalsIgnoreCase(HttpHeaders.USER_AGENT)) {
223 final Header header = message.getFirstHeader(headerName);
224 if (header != null) {
225 consumer.accept(header.getValue().toLowerCase(Locale.ROOT));
226 }
227 } else {
228 normalizeElements(message.headerIterator(headerName), consumer);
229 }
230 }
231
232 @Internal
233 public static void normalizeElements(final Iterator<Header> iterator, final Consumer<String> consumer) {
234 final Iterator<HeaderElement> it = new BasicHeaderElementIterator(iterator);
235 StreamSupport.stream(Spliterators.spliteratorUnknownSize(it, Spliterator.NONNULL), false)
236 .filter(e -> !TextUtils.isBlank(e.getName()))
237 .map(e -> {
238 if (e.getValue() == null && e.getParameterCount() == 0) {
239 return e.getName().toLowerCase(Locale.ROOT);
240 }
241 final CharArrayBuffer buf = new CharArrayBuffer(1024);
242 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
243 buf,
244 new BasicNameValuePair(
245 e.getName().toLowerCase(Locale.ROOT),
246 !TextUtils.isBlank(e.getValue()) ? e.getValue() : null),
247 false);
248 if (e.getParameterCount() > 0) {
249 for (final NameValuePair nvp : e.getParameters()) {
250 if (!TextUtils.isBlank(nvp.getName())) {
251 buf.append(';');
252 BasicHeaderValueFormatter.INSTANCE.formatNameValuePair(
253 buf,
254 new BasicNameValuePair(
255 nvp.getName().toLowerCase(Locale.ROOT),
256 !TextUtils.isBlank(nvp.getValue()) ? nvp.getValue() : null),
257 false);
258 }
259 }
260 }
261 return buf.toString();
262 })
263 .sorted()
264 .distinct()
265 .forEach(consumer);
266 }
267
268
269
270
271
272
273
274
275
276 public String generateVariantKey(final HttpRequest request, final Collection<String> variantNames) {
277 Args.notNull(variantNames, "Variant names");
278 final StringBuilder buf = new StringBuilder("{");
279 final AtomicBoolean firstHeader = new AtomicBoolean();
280 variantNames.stream()
281 .map(h -> h.toLowerCase(Locale.ROOT))
282 .sorted()
283 .distinct()
284 .forEach(h -> {
285 if (!firstHeader.compareAndSet(false, true)) {
286 buf.append("&");
287 }
288 buf.append(PercentCodec.encode(h, StandardCharsets.UTF_8)).append("=");
289 final AtomicBoolean firstToken = new AtomicBoolean();
290 normalizeElements(request, h, t -> {
291 if (!firstToken.compareAndSet(false, true)) {
292 buf.append(",");
293 }
294 buf.append(PercentCodec.encode(t, StandardCharsets.UTF_8));
295 });
296 });
297 buf.append("}");
298 return buf.toString();
299 }
300
301
302
303
304
305
306
307
308
309 public String generateVariantKey(final HttpRequest request, final HttpCacheEntry entry) {
310 if (entry.containsHeader(HttpHeaders.VARY)) {
311 final List<String> variantNames = variantNames(entry);
312 return generateVariantKey(request, variantNames);
313 }
314 return null;
315 }
316
317
318
319
320
321
322
323
324
325
326
327
328
329 @Deprecated
330 public String generateKey(final HttpHost host, final HttpRequest request, final HttpCacheEntry entry) {
331 final String rootKey = generateKey(host, request);
332 final List<String> variantNames = variantNames(entry);
333 if (variantNames.isEmpty()) {
334 return rootKey;
335 }
336 return generateVariantKey(request, variantNames) + rootKey;
337 }
338
339 }