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 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
82
83
84
85
86
87
88
89
90
91
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
127
128
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
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
167
168
169
170
171
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
184
185
186
187
188
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
201
202
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
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
268
269
270
271
272
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
301
302
303
304
305
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
318
319
320
321
322
323
324
325
326
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 }