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.time.Instant;
35 import java.util.HashSet;
36 import java.util.Iterator;
37 import java.util.Set;
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.Contract;
46 import org.apache.hc.core5.annotation.ThreadingBehavior;
47 import org.apache.hc.core5.http.ClassicHttpResponse;
48 import org.apache.hc.core5.http.Header;
49 import org.apache.hc.core5.http.HttpVersion;
50 import org.apache.hc.core5.http.ParseException;
51 import org.apache.hc.core5.http.impl.io.AbstractMessageParser;
52 import org.apache.hc.core5.http.impl.io.AbstractMessageWriter;
53 import org.apache.hc.core5.http.impl.io.SessionInputBufferImpl;
54 import org.apache.hc.core5.http.impl.io.SessionOutputBufferImpl;
55 import org.apache.hc.core5.http.io.SessionOutputBuffer;
56 import org.apache.hc.core5.http.message.BasicLineFormatter;
57 import org.apache.hc.core5.http.message.BasicLineParser;
58 import org.apache.hc.core5.http.message.HeaderGroup;
59 import org.apache.hc.core5.http.message.LineFormatter;
60 import org.apache.hc.core5.http.message.LineParser;
61 import org.apache.hc.core5.http.message.RequestLine;
62 import org.apache.hc.core5.http.message.StatusLine;
63 import org.apache.hc.core5.util.CharArrayBuffer;
64 import org.slf4j.Logger;
65 import org.slf4j.LoggerFactory;
66
67
68
69
70
71
72
73
74 @Contract(threading = ThreadingBehavior.STATELESS)
75 public class HttpByteArrayCacheEntrySerializer implements HttpCacheEntrySerializer<byte[]> {
76
77 private static final Logger LOG = LoggerFactory.getLogger(HttpByteArrayCacheEntrySerializer.class);
78
79
80
81
82 private static final int DEFAULT_BUFFER_SIZE = 8192;
83
84 static final String HC_CACHE_VERSION = "1.0";
85 static final String HC_CACHE_VERSION_LINE = "HttpClient CacheEntry " + HC_CACHE_VERSION;
86
87 static final String HC_CACHE_KEY = "HC-Key";
88 static final String HC_CACHE_LENGTH = "HC-Resource-Length";
89 static final String HC_REQUEST_INSTANT = "HC-Request-Instant";
90 static final String HC_RESPONSE_INSTANT = "HC-Response-Instant";
91 static final String HC_VARIANT = "HC-Variant";
92
93
94
95
96 public static final HttpByteArrayCacheEntrySerializer INSTANCE = new HttpByteArrayCacheEntrySerializer();
97
98 private final LineParser lineParser;
99 private final LineFormatter lineFormatter;
100 private final int bufferSize;
101
102
103
104
105
106
107 public HttpByteArrayCacheEntrySerializer(final int bufferSize) {
108 this.lineParser = BasicLineParser.INSTANCE;
109 this.lineFormatter = BasicLineFormatter.INSTANCE;
110 this.bufferSize = bufferSize > 0 ? bufferSize : DEFAULT_BUFFER_SIZE;
111 }
112
113
114
115
116
117
118 public HttpByteArrayCacheEntrySerializer() {
119 this(DEFAULT_BUFFER_SIZE);
120 }
121
122
123
124
125
126
127
128
129
130
131 @Override
132 public byte[] serialize(final HttpCacheStorageEntry storageEntry) throws ResourceIOException {
133 final String key = storageEntry.getKey();
134 final HttpCacheEntry cacheEntry = storageEntry.getContent();
135 final Resource resource = cacheEntry.getResource();
136
137 try (ByteArrayOutputStream out = new ByteArrayOutputStream((resource != null ? (int) resource.length() : 0) + DEFAULT_BUFFER_SIZE)) {
138 final SessionOutputBuffer outputBuffer = new SessionOutputBufferImpl(bufferSize);
139 final CharArrayBuffer line = new CharArrayBuffer(DEFAULT_BUFFER_SIZE);
140
141 line.append(HC_CACHE_VERSION_LINE);
142 outputBuffer.writeLine(line, out);
143
144 line.clear();
145 line.append(HC_CACHE_KEY);
146 line.append(": ");
147 line.append(key);
148 outputBuffer.writeLine(line, out);
149
150 if (resource != null) {
151 line.clear();
152 line.append(HC_CACHE_LENGTH);
153 line.append(": ");
154 line.append(asStr(resource.length()));
155 outputBuffer.writeLine(line, out);
156 }
157
158 line.clear();
159 line.append(HC_REQUEST_INSTANT);
160 line.append(": ");
161 line.append(asStr(cacheEntry.getRequestInstant()));
162 outputBuffer.writeLine(line, out);
163
164 line.clear();
165 line.append(HC_RESPONSE_INSTANT);
166 line.append(": ");
167 line.append(asStr(cacheEntry.getResponseInstant()));
168 outputBuffer.writeLine(line, out);
169
170 for (final String variant : cacheEntry.getVariants()) {
171 line.clear();
172 line.append(HC_VARIANT);
173 line.append(": ");
174 line.append(variant);
175 outputBuffer.writeLine(line, out);
176 }
177 line.clear();
178 outputBuffer.writeLine(line, out);
179
180 line.clear();
181 final RequestLine requestLine = new RequestLine(cacheEntry.getRequestMethod(), cacheEntry.getRequestURI(), HttpVersion.HTTP_1_1);
182 lineFormatter.formatRequestLine(line, requestLine);
183 outputBuffer.writeLine(line, out);
184 for (final Iterator<Header> it = cacheEntry.requestHeaderIterator(); it.hasNext(); ) {
185 line.clear();
186 lineFormatter.formatHeader(line, it.next());
187 outputBuffer.writeLine(line, out);
188 }
189 line.clear();
190 outputBuffer.writeLine(line, out);
191
192 line.clear();
193 final StatusLine statusLine = new StatusLine(HttpVersion.HTTP_1_1, cacheEntry.getStatus(), "");
194 lineFormatter.formatStatusLine(line, statusLine);
195 outputBuffer.writeLine(line, out);
196 for (final Iterator<Header> it = cacheEntry.headerIterator(); it.hasNext(); ) {
197 line.clear();
198 lineFormatter.formatHeader(line, it.next());
199 outputBuffer.writeLine(line, out);
200 }
201 line.clear();
202 outputBuffer.writeLine(line, out);
203 outputBuffer.flush(out);
204
205 if (resource != null) {
206 out.write(resource.get());
207 }
208 out.flush();
209
210 final byte[] bytes = out.toByteArray();
211
212 if (LOG.isDebugEnabled()) {
213 LOG.debug("Serialized cache entry with key {} and {} bytes", key, bytes.length);
214 }
215 return bytes;
216 } catch (final IOException ex) {
217 throw new ResourceIOException("Exception while serializing cache entry", ex);
218 }
219 }
220
221
222
223
224
225
226
227
228
229 @Override
230 public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws ResourceIOException {
231 if (serializedObject == null || serializedObject.length == 0) {
232 throw new ResourceIOException("Serialized object is null or empty");
233 }
234 try (final InputStream in = new ByteArrayInputStream(serializedObject)) {
235 final SessionInputBufferImpl inputBuffer = new SessionInputBufferImpl(bufferSize);
236 final CharArrayBuffer line = new CharArrayBuffer(DEFAULT_BUFFER_SIZE);
237 checkReadResult(inputBuffer.readLine(line, in));
238 final String versionLine = line.toString();
239 if (!versionLine.equals(HC_CACHE_VERSION_LINE)) {
240 throw new ResourceIOException("Unexpected cache entry version line");
241 }
242 String storageKey = null;
243 long length = -1;
244 Instant requestDate = null;
245 Instant responseDate = null;
246 final Set<String> variants = new HashSet<>();
247
248 while (true) {
249 line.clear();
250 checkReadResult(inputBuffer.readLine(line, in));
251 if (line.isEmpty()) {
252 break;
253 }
254 final Header header = lineParser.parseHeader(line);
255 final String name = header.getName();
256 final String value = header.getValue();
257 if (name.equalsIgnoreCase(HC_CACHE_KEY)) {
258 storageKey = value;
259 } else if (name.equalsIgnoreCase(HC_CACHE_LENGTH)) {
260 length = asLong(value);
261 } else if (name.equalsIgnoreCase(HC_REQUEST_INSTANT)) {
262 requestDate = asInstant(value);
263 } else if (name.equalsIgnoreCase(HC_RESPONSE_INSTANT)) {
264 responseDate = asInstant(value);
265 } else if (name.equalsIgnoreCase(HC_VARIANT)) {
266 variants.add(value);
267 } else {
268 throw new ResourceIOException("Unexpected header entry");
269 }
270 }
271
272 if (storageKey == null || requestDate == null || responseDate == null) {
273 throw new ResourceIOException("Invalid cache header format");
274 }
275
276 line.clear();
277 checkReadResult(inputBuffer.readLine(line, in));
278 final RequestLine requestLine = lineParser.parseRequestLine(line);
279 final HeaderGroup requestHeaders = new HeaderGroup();
280 while (true) {
281 line.clear();
282 checkReadResult(inputBuffer.readLine(line, in));
283 if (line.isEmpty()) {
284 break;
285 }
286 requestHeaders.addHeader(lineParser.parseHeader(line));
287 }
288 line.clear();
289 checkReadResult(inputBuffer.readLine(line, in));
290 final StatusLine statusLine = lineParser.parseStatusLine(line);
291 final HeaderGroup responseHeaders = new HeaderGroup();
292 while (true) {
293 line.clear();
294 checkReadResult(inputBuffer.readLine(line, in));
295 if (line.isEmpty()) {
296 break;
297 }
298 responseHeaders.addHeader(lineParser.parseHeader(line));
299 }
300
301 final Resource resource;
302 if (length != -1) {
303 int off = 0;
304 int remaining = (int) length;
305 final byte[] buf = new byte[remaining];
306 while (remaining > 0) {
307 final int i = inputBuffer.read(buf, off, remaining, in);
308 if (i > 0) {
309 off += i;
310 remaining -= i;
311 }
312 if (i == -1) {
313 throw new ResourceIOException("Unexpected end of cache content");
314 }
315 }
316 resource = new HeapResource(buf);
317 } else {
318 resource = null;
319 }
320 if (inputBuffer.read(in) != -1) {
321 throw new ResourceIOException("Unexpected content at the end of cache content");
322 }
323
324 final HttpCacheEntry httpCacheEntry = new HttpCacheEntry(
325 requestDate,
326 responseDate,
327 requestLine.getMethod(),
328 requestLine.getUri(),
329 requestHeaders,
330 statusLine.getStatusCode(),
331 responseHeaders,
332 resource,
333 !variants.isEmpty() ? variants : null
334 );
335
336 if (LOG.isDebugEnabled()) {
337 LOG.debug("Returning deserialized cache entry with storage key '{}'", httpCacheEntry);
338 }
339
340 return new HttpCacheStorageEntry(storageKey, httpCacheEntry);
341 } catch (final ResourceIOException ex) {
342 throw ex;
343 } catch (final ParseException ex) {
344 throw new ResourceIOException("Invalid cache header format", ex);
345 } catch (final IOException ex) {
346 throw new ResourceIOException("I/O error deserializing cache entry", ex);
347 }
348 }
349
350 private static String asStr(final long value) {
351 return Long.toString(value);
352 }
353
354 private static String asStr(final Instant instant) {
355 return Long.toString(instant.toEpochMilli());
356 }
357
358 private static long asLong(final String value) throws ResourceIOException {
359 try {
360 return Long.parseLong(value);
361 } catch (final NumberFormatException ex) {
362 throw new ResourceIOException("Invalid cache header format");
363 }
364 }
365
366 private static Instant asInstant(final String value) throws ResourceIOException {
367 return Instant.ofEpochMilli(asLong(value));
368 }
369
370 private static void checkReadResult(final int n) throws ResourceIOException {
371 if (n == -1) {
372 throw new ResourceIOException("Unexpected end of stream");
373 }
374 }
375
376
377
378
379
380 @Deprecated
381 protected InputStream makeByteArrayInputStream(final byte[] bytes) {
382 return null;
383 }
384
385
386
387
388
389 @Deprecated
390 protected AbstractMessageParser<ClassicHttpResponse> makeHttpResponseParser() {
391 return null;
392 }
393
394
395
396
397
398 @Deprecated
399 protected AbstractMessageWriter<SimpleHttpResponse> makeHttpResponseWriter(final SessionOutputBuffer outputBuffer) {
400 return null;
401 }
402
403 }