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
28 package org.apache.hc.client5.http.impl.cache;
29
30 import java.io.ByteArrayInputStream;
31 import java.io.ByteArrayOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.time.Instant;
36 import java.util.HashMap;
37 import java.util.Map;
38
39 import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
40 import org.apache.hc.client5.http.cache.HttpCacheEntry;
41 import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
42 import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
43 import org.apache.hc.client5.http.cache.Resource;
44 import org.apache.hc.client5.http.cache.ResourceIOException;
45 import org.apache.hc.core5.annotation.Experimental;
46 import org.apache.hc.core5.http.Header;
47 import org.apache.hc.core5.http.ClassicHttpResponse;
48 import org.apache.hc.core5.http.HttpException;
49 import org.apache.hc.core5.http.HttpRequest;
50 import org.apache.hc.core5.http.HttpResponse;
51 import org.apache.hc.core5.http.HttpVersion;
52 import org.apache.hc.core5.http.ProtocolVersion;
53 import org.apache.hc.core5.http.impl.io.AbstractMessageParser;
54 import org.apache.hc.core5.http.impl.io.AbstractMessageWriter;
55 import org.apache.hc.core5.http.impl.io.DefaultHttpResponseParser;
56 import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl;
57 import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl;
58 import org.apache.hc.core5.http.io.SessionInputBuffer;
59 import org.apache.hc.core5.http.io.SessionOutputBuffer;
60 import org.apache.hc.core5.http.message.BasicHttpRequest;
61 import org.apache.hc.core5.http.message.BasicLineFormatter;
62 import org.apache.hc.core5.http.message.StatusLine;
63 import org.apache.hc.core5.util.CharArrayBuffer;
64 import org.apache.hc.core5.util.TimeValue;
65
66
67
68
69
70
71
72 @Experimental
73 public class HttpByteArrayCacheEntrySerializer implements HttpCacheEntrySerializer<byte[]> {
74 public static final HttpByteArrayCacheEntrySerializer INSTANCE = new HttpByteArrayCacheEntrySerializer();
75
76 private static final String SC_CACHE_ENTRY_PREFIX = "hc-";
77
78 private static final String SC_HEADER_NAME_STORAGE_KEY = SC_CACHE_ENTRY_PREFIX + "sk";
79 private static final String SC_HEADER_NAME_RESPONSE_DATE = SC_CACHE_ENTRY_PREFIX + "resp-date";
80 private static final String SC_HEADER_NAME_REQUEST_DATE = SC_CACHE_ENTRY_PREFIX + "req-date";
81 private static final String SC_HEADER_NAME_NO_CONTENT = SC_CACHE_ENTRY_PREFIX + "no-content";
82 private static final String SC_HEADER_NAME_VARIANT_MAP_KEY = SC_CACHE_ENTRY_PREFIX + "varmap-key";
83 private static final String SC_HEADER_NAME_VARIANT_MAP_VALUE = SC_CACHE_ENTRY_PREFIX + "varmap-val";
84
85 private static final String SC_CACHE_ENTRY_PRESERVE_PREFIX = SC_CACHE_ENTRY_PREFIX + "esc-";
86
87 private static final int BUFFER_SIZE = 8192;
88
89 public HttpByteArrayCacheEntrySerializer() {
90 }
91
92 @Override
93 public byte[] serialize(final HttpCacheStorageEntry httpCacheEntry) throws ResourceIOException {
94 if (httpCacheEntry.getKey() == null) {
95 throw new IllegalStateException("Cannot serialize cache object with null storage key");
96 }
97
98
99
100
101 final HttpRequest httpRequest = new BasicHttpRequest(httpCacheEntry.getContent().getRequestMethod(), "/");
102
103 final CacheValidityPolicy cacheValidityPolicy = new NoAgeCacheValidityPolicy();
104 final CachedHttpResponseGenerator cachedHttpResponseGenerator = new CachedHttpResponseGenerator(cacheValidityPolicy);
105
106 final SimpleHttpResponse httpResponse = cachedHttpResponseGenerator.generateResponse(httpRequest, httpCacheEntry.getContent());
107
108 try(final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
109 escapeHeaders(httpResponse);
110 addMetadataPseudoHeaders(httpResponse, httpCacheEntry);
111
112 final byte[] bodyBytes = httpResponse.getBodyBytes();
113 final int resourceLength;
114
115 if (bodyBytes == null) {
116
117 httpResponse.addHeader(SC_HEADER_NAME_NO_CONTENT, Boolean.TRUE.toString());
118 resourceLength = 0;
119 } else {
120 resourceLength = bodyBytes.length;
121 }
122
123
124
125 final SessionOutputBufferImpl outputBuffer = new SessionOutputBufferImpl(BUFFER_SIZE);
126 final AbstractMessageWriter<SimpleHttpResponse> httpResponseWriter = makeHttpResponseWriter(outputBuffer);
127 httpResponseWriter.write(httpResponse, outputBuffer, out);
128 outputBuffer.flush(out);
129 final byte[] headerBytes = out.toByteArray();
130
131 final byte[] bytes = new byte[headerBytes.length + resourceLength];
132 System.arraycopy(headerBytes, 0, bytes, 0, headerBytes.length);
133 if (resourceLength > 0) {
134 System.arraycopy(bodyBytes, 0, bytes, headerBytes.length, resourceLength);
135 }
136 return bytes;
137 } catch(final IOException|HttpException e) {
138 throw new ResourceIOException("Exception while serializing cache entry", e);
139 }
140 }
141
142 @Override
143 public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws ResourceIOException {
144 try (final InputStream in = makeByteArrayInputStream(serializedObject);
145 final ByteArrayOutputStream bytesOut = new ByteArrayOutputStream(serializedObject.length)
146 ) {
147 final SessionInputBufferImpl inputBuffer = new SessionInputBufferImpl(BUFFER_SIZE);
148 final AbstractMessageParser<ClassicHttpResponse> responseParser = makeHttpResponseParser();
149 final ClassicHttpResponse response = responseParser.parse(inputBuffer, in);
150
151
152 final String storageKey = getCachePseudoHeaderAndRemove(response, SC_HEADER_NAME_STORAGE_KEY);
153 final Instant requestDate = getCachePseudoHeaderDateAndRemove(response, SC_HEADER_NAME_REQUEST_DATE);
154 final Instant responseDate = getCachePseudoHeaderDateAndRemove(response, SC_HEADER_NAME_RESPONSE_DATE);
155 final boolean noBody = getCachePseudoHeaderBooleanAndRemove(response, SC_HEADER_NAME_NO_CONTENT);
156 final Map<String, String> variantMap = getVariantMapPseudoHeadersAndRemove(response);
157 unescapeHeaders(response);
158
159 final Resource resource;
160 if (noBody) {
161
162 resource = null;
163 } else {
164 copyBytes(inputBuffer, in, bytesOut);
165 resource = new HeapResource(bytesOut.toByteArray());
166 }
167
168 final HttpCacheEntry httpCacheEntry = new HttpCacheEntry(
169 requestDate,
170 responseDate,
171 response.getCode(),
172 response.getHeaders(),
173 resource,
174 variantMap
175 );
176
177 return new HttpCacheStorageEntry(storageKey, httpCacheEntry);
178 } catch (final IOException|HttpException e) {
179 throw new ResourceIOException("Error deserializing cache entry", e);
180 }
181 }
182
183
184
185
186
187
188
189
190
191 protected AbstractMessageWriter<SimpleHttpResponse> makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
192 return new SimpleHttpResponseWriter();
193 }
194
195
196
197
198
199
200
201
202
203 protected InputStream makeByteArrayInputStream(final byte[] bytes) {
204 return new ByteArrayInputStream(bytes);
205 }
206
207
208
209
210
211
212
213
214 protected AbstractMessageParser<ClassicHttpResponse> makeHttpResponseParser() {
215 return new DefaultHttpResponseParser();
216 }
217
218
219
220
221
222
223
224
225 private static void escapeHeaders(final HttpResponse httpResponse) {
226 final Header[] headers = httpResponse.getHeaders();
227 for (final Header header : headers) {
228 if (header.getName().startsWith(SC_CACHE_ENTRY_PREFIX)) {
229 httpResponse.removeHeader(header);
230 httpResponse.addHeader(SC_CACHE_ENTRY_PRESERVE_PREFIX + header.getName(), header.getValue());
231 }
232 }
233 }
234
235
236
237
238
239
240
241 private void unescapeHeaders(final HttpResponse httpResponse) {
242 final Header[] headers = httpResponse.getHeaders();
243 for (final Header header : headers) {
244 if (header.getName().startsWith(SC_CACHE_ENTRY_PRESERVE_PREFIX)) {
245 httpResponse.removeHeader(header);
246 httpResponse.addHeader(header.getName().substring(SC_CACHE_ENTRY_PRESERVE_PREFIX.length()), header.getValue());
247 }
248 }
249 }
250
251
252
253
254
255
256 private void addMetadataPseudoHeaders(final HttpResponse httpResponse, final HttpCacheStorageEntry httpCacheEntry) {
257 httpResponse.addHeader(SC_HEADER_NAME_STORAGE_KEY, httpCacheEntry.getKey());
258 httpResponse.addHeader(SC_HEADER_NAME_RESPONSE_DATE, Long.toString(httpCacheEntry.getContent().getResponseInstant().toEpochMilli()));
259 httpResponse.addHeader(SC_HEADER_NAME_REQUEST_DATE, Long.toString(httpCacheEntry.getContent().getRequestInstant().toEpochMilli()));
260
261
262
263
264 for (final Map.Entry<String, String> entry : httpCacheEntry.getContent().getVariantMap().entrySet()) {
265
266 httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_KEY, entry.getKey());
267 httpResponse.addHeader(SC_HEADER_NAME_VARIANT_MAP_VALUE, entry.getValue());
268 }
269 }
270
271
272
273
274
275
276
277
278
279 private static String getCachePseudoHeaderAndRemove(final HttpResponse response, final String name) throws ResourceIOException {
280 final String headerValue = getOptionalCachePseudoHeaderAndRemove(response, name);
281 if (headerValue == null) {
282 throw new ResourceIOException("Expected cache header '" + name + "' not found");
283 }
284 return headerValue;
285 }
286
287
288
289
290
291
292
293
294 private static String getOptionalCachePseudoHeaderAndRemove(final HttpResponse response, final String name) {
295 final Header header = response.getFirstHeader(name);
296 if (header == null) {
297 return null;
298 }
299 response.removeHeader(header);
300 return header.getValue();
301 }
302
303
304
305
306
307
308
309
310
311 private static Instant getCachePseudoHeaderDateAndRemove(final HttpResponse response, final String name) throws ResourceIOException{
312 final String value = getCachePseudoHeaderAndRemove(response, name);
313 response.removeHeaders(name);
314 try {
315 final long timestamp = Long.parseLong(value);
316 return Instant.ofEpochMilli(timestamp);
317 } catch (final NumberFormatException e) {
318 throw new ResourceIOException("Invalid value for header '" + name + "'", e);
319 }
320 }
321
322
323
324
325
326
327
328
329 private static boolean getCachePseudoHeaderBooleanAndRemove(final ClassicHttpResponse response, final String name) {
330
331 return Boolean.parseBoolean(getOptionalCachePseudoHeaderAndRemove(response, name));
332 }
333
334
335
336
337
338
339
340
341 private static Map<String, String> getVariantMapPseudoHeadersAndRemove(final HttpResponse response) throws ResourceIOException {
342 final Header[] headers = response.getHeaders();
343 final Map<String, String> variantMap = new HashMap<>(0);
344 String lastKey = null;
345 for (final Header header : headers) {
346 if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_KEY)) {
347 lastKey = header.getValue();
348 response.removeHeader(header);
349 } else if (header.getName().equals(SC_HEADER_NAME_VARIANT_MAP_VALUE)) {
350 if (lastKey == null) {
351 throw new ResourceIOException("Found mismatched variant map key/value headers");
352 }
353 variantMap.put(lastKey, header.getValue());
354 lastKey = null;
355 response.removeHeader(header);
356 }
357 }
358
359 if (lastKey != null) {
360 throw new ResourceIOException("Found mismatched variant map key/value headers");
361 }
362
363 return variantMap;
364 }
365
366
367
368
369
370
371
372
373
374 private static void copyBytes(final SessionInputBuffer srcBuf, final InputStream src, final OutputStream dest) throws IOException {
375 final byte[] buf = new byte[BUFFER_SIZE];
376 int lastBytesRead;
377 while ((lastBytesRead = srcBuf.read(buf, src)) != -1) {
378 dest.write(buf, 0, lastBytesRead);
379 }
380 }
381
382
383
384
385
386
387
388 private static class SimpleHttpResponseWriter extends AbstractMessageWriter<SimpleHttpResponse> {
389
390 public SimpleHttpResponseWriter() {
391 super(BasicLineFormatter.INSTANCE);
392 }
393
394 @Override
395 protected void writeHeadLine(
396 final SimpleHttpResponse message, final CharArrayBuffer lineBuf) {
397 final ProtocolVersion transportVersion = message.getVersion();
398 BasicLineFormatter.INSTANCE.formatStatusLine(lineBuf, new StatusLine(
399 transportVersion != null ? transportVersion : HttpVersion.HTTP_1_1,
400 message.getCode(),
401 message.getReasonPhrase()));
402 }
403 }
404
405
406
407
408
409
410
411 private static class NoAgeCacheValidityPolicy extends CacheValidityPolicy {
412 @Override
413 public TimeValue getCurrentAge(final HttpCacheEntry entry, final Instant now) {
414 return TimeValue.ZERO_MILLISECONDS;
415 }
416 }
417 }