diff --git a/spotbugs.exclude.xml b/spotbugs.exclude.xml
index 20e59c4..8726b13 100644
--- a/spotbugs.exclude.xml
+++ b/spotbugs.exclude.xml
@@ -40,21 +40,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -96,4 +81,13 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java
new file mode 100644
index 0000000..dd0e9b7
--- /dev/null
+++ b/src/main/java/io/github/nstdio/http/ext/BinaryMetadataSerializer.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (C) 2022 Edgar Asatryan
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.github.nstdio.http.ext;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutput;
+import java.io.ObjectOutputStream;
+import java.net.URI;
+import java.net.http.HttpClient.Version;
+import java.net.http.HttpHeaders;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.Builder;
+import java.net.http.HttpResponse.ResponseInfo;
+import java.nio.file.Path;
+import java.time.Clock;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static java.net.http.HttpRequest.BodyPublishers.noBody;
+
+class BinaryMetadataSerializer implements MetadataSerializer {
+ private final StreamFactory streamFactory;
+
+ BinaryMetadataSerializer(StreamFactory streamFactory) {
+ this.streamFactory = streamFactory;
+ }
+
+ @Override
+ public void write(CacheEntryMetadata metadata, Path path) {
+ try (var out = new ObjectOutputStream(streamFactory.output(path))) {
+ out.writeObject(new ExternalizableMetadata(metadata));
+ } catch (IOException ignored) {
+ }
+ }
+
+ @Override
+ public CacheEntryMetadata read(Path path) {
+ try (var input = new ObjectInputStream(streamFactory.input(path))) {
+ return ((ExternalizableMetadata) input.readObject()).metadata;
+ } catch (IOException | ClassNotFoundException ignored) {
+ return null;
+ }
+ }
+
+ static final class ExternalizableMetadata implements Externalizable {
+ private static final long serialVersionUID = 15052410042022L;
+ private CacheEntryMetadata metadata;
+
+ public ExternalizableMetadata() {
+ }
+
+ ExternalizableMetadata(CacheEntryMetadata metadata) {
+ this.metadata = metadata;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeLong(metadata.requestTime());
+ out.writeLong(metadata.responseTime());
+
+ out.writeObject(new ExternalizableResponseInfo(metadata.response()));
+ out.writeObject(new ExternalizableHttpRequest(metadata.request()));
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ final long requestTime = in.readLong();
+ final long responseTime = in.readLong();
+ ResponseInfo responseInfo = ((ExternalizableResponseInfo) in.readObject()).responseInfo;
+ HttpRequest request = ((ExternalizableHttpRequest) in.readObject()).request;
+
+ metadata = CacheEntryMetadata.of(requestTime, responseTime, responseInfo, request, Clock.systemUTC());
+ }
+ }
+
+ static class ExternalizableHttpRequest implements Externalizable {
+ private static final long serialVersionUID = 15052410042022L;
+ private HttpRequest request;
+
+ public ExternalizableHttpRequest() {
+ }
+
+ ExternalizableHttpRequest(HttpRequest request) {
+ this.request = request;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeObject(request.uri());
+ out.writeUTF(request.method());
+ out.writeObject(request.version().orElse(null));
+ out.writeObject(request.timeout().orElse(null));
+ out.writeObject(new ExternalizableHttpHeaders(request.headers()));
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ Builder builder = HttpRequest.newBuilder()
+ .uri((URI) in.readObject())
+ .method(in.readUTF(), noBody());
+
+ Version v = (Version) in.readObject();
+ if (v != null) {
+ builder.version(v);
+ }
+ Duration t = (Duration) in.readObject();
+ if (t != null) {
+ builder.timeout(t);
+ }
+
+ Map> headers = ((ExternalizableHttpHeaders) in.readObject()).headers.map();
+ for (var entry : headers.entrySet()) {
+ for (String value : entry.getValue()) {
+ builder.header(entry.getKey(), value);
+ }
+ }
+
+ request = builder.build();
+ }
+ }
+
+ static class ExternalizableResponseInfo implements Externalizable {
+ private static final long serialVersionUID = 15052410042022L;
+ private ResponseInfo responseInfo;
+
+ public ExternalizableResponseInfo() {
+ }
+
+ ExternalizableResponseInfo(ResponseInfo responseInfo) {
+ this.responseInfo = responseInfo;
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ out.writeInt(responseInfo.statusCode());
+ out.writeObject(new ExternalizableHttpHeaders(responseInfo.headers()));
+ out.writeObject(responseInfo.version());
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+ responseInfo = ImmutableResponseInfo.builder()
+ .statusCode(in.readInt())
+ .headers(((ExternalizableHttpHeaders) in.readObject()).headers)
+ .version((Version) in.readObject())
+ .build();
+ }
+ }
+
+ static class ExternalizableHttpHeaders implements Externalizable {
+ private static final long serialVersionUID = 15052410042022L;
+ private static final int maxMapSize = 1024;
+ private static final int maxListSize = 256;
+
+ private final boolean respectLimits;
+ HttpHeaders headers;
+
+ public ExternalizableHttpHeaders() {
+ this(null, true);
+ }
+
+ ExternalizableHttpHeaders(HttpHeaders headers) {
+ this(headers, true);
+ }
+
+ ExternalizableHttpHeaders(HttpHeaders headers, boolean respectLimits) {
+ this.headers = headers;
+ this.respectLimits = respectLimits;
+ }
+
+ private static String mapSizeExceedMessage(int mapSize) {
+ return String.format("The headers size exceeds max allowed number. Size: %d, Max:%d", mapSize, maxMapSize);
+ }
+
+ private static String listSizeExceedMessage(String headerName, int size) {
+ return String.format("The values for header '%s' exceeds maximum allowed number. Size:%d, Max:%d",
+ headerName, size, maxListSize);
+ }
+
+ private static IOException corruptedStream(String desc) {
+ return new IOException("Corrupted stream: " + desc);
+ }
+
+ @Override
+ public void writeExternal(ObjectOutput out) throws IOException {
+ Map> map = headers.map();
+
+ int mapSize = map.size();
+ if (respectLimits && mapSize > maxMapSize) {
+ throw new IOException(mapSizeExceedMessage(mapSize));
+ }
+
+ // write map size
+ out.writeInt(mapSize);
+ for (var entry : map.entrySet()) {
+ // header name
+ String headerName = entry.getKey();
+ out.writeUTF(headerName);
+
+ List values = entry.getValue();
+ int valuesSize = values.size();
+ checkValuesSize(headerName, valuesSize);
+
+ // write values size
+ out.writeInt(valuesSize);
+ // header values
+ for (String value : values) out.writeUTF(value);
+ }
+ }
+
+ @Override
+ public void readExternal(ObjectInput in) throws IOException {
+ final int mapSize = in.readInt();
+ checkMapSize(mapSize);
+
+ if (mapSize == 0) {
+ headers = HttpHeaders.of(Map.of(), Headers.ALLOW_ALL);
+ return;
+ }
+
+ var map = new HashMap>(mapSize, 1.0f);
+ for (int i = 0; i < mapSize; i++) {
+ String headerName = in.readUTF();
+
+ int valuesSize = in.readInt();
+ checkValuesSize(headerName, valuesSize);
+
+ var values = new ArrayList(valuesSize);
+ for (int j = 0; j < valuesSize; j++) values.add(in.readUTF());
+
+ map.put(headerName, values);
+ }
+
+ headers = HttpHeaders.of(map, Headers.ALLOW_ALL);
+ }
+
+ private void checkValuesSize(String headerName, int valuesSize) throws IOException {
+ if (valuesSize <= 0) throw corruptedStream("list size should be positive");
+ else if (respectLimits && valuesSize > maxListSize)
+ throw new IOException(listSizeExceedMessage(headerName, valuesSize));
+ }
+
+ private void checkMapSize(int mapSize) throws IOException {
+ if (mapSize < 0) throw corruptedStream("map size cannot be negative");
+ else if (respectLimits && mapSize > maxMapSize) throw new IOException(mapSizeExceedMessage(mapSize));
+ }
+ }
+}
diff --git a/src/main/java/io/github/nstdio/http/ext/Cache.java b/src/main/java/io/github/nstdio/http/ext/Cache.java
index 626f9cc..df260d8 100644
--- a/src/main/java/io/github/nstdio/http/ext/Cache.java
+++ b/src/main/java/io/github/nstdio/http/ext/Cache.java
@@ -47,15 +47,11 @@ static InMemoryCacheBuilder newInMemoryCacheBuilder() {
}
/**
- * Creates a new {@code DiskCacheBuilder} instance. Requires Jackson form dumping cache files on disk.
+ * Creates a new {@code DiskCacheBuilder} instance.
*
* @return the new {@code DiskCacheBuilder}.
- *
- * @throws IllegalStateException When Jackson (a.k.a. ObjectMapper) is not in classpath.
*/
static DiskCacheBuilder newDiskCacheBuilder() {
- MetadataSerializer.requireAvailability();
-
return new DiskCacheBuilder();
}
diff --git a/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java b/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java
index 2549ffb..4a3fdfb 100644
--- a/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java
+++ b/src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java
@@ -62,7 +62,7 @@ public class ExtendedHttpClient extends HttpClient {
* @return an {@code ExtendedHttpClient.Builder}
*/
public static ExtendedHttpClient.Builder newBuilder() {
- return new ExtendedHttpClient.Builder();
+ return new ExtendedHttpClient.Builder(HttpClient.newBuilder());
}
/**
@@ -200,7 +200,7 @@ private Sender syncSender() {
return ctx -> {
try {
return completedFuture(delegate.send(ctx.request(), ctx.bodyHandler()));
- } catch (IOException | InterruptedException e) {
+ } catch (Throwable e) {
return CompletableFuture.failedFuture(e);
}
};
@@ -221,11 +221,12 @@ interface Sender extends Function
diff --git a/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java
deleted file mode 100644
index 0fe5fb3..0000000
--- a/src/main/java/io/github/nstdio/http/ext/GsonMetadataSerializer.java
+++ /dev/null
@@ -1,300 +0,0 @@
-/*
- * Copyright (C) 2022 Edgar Asatryan
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.github.nstdio.http.ext;
-
-import com.google.gson.Gson;
-import com.google.gson.GsonBuilder;
-import com.google.gson.TypeAdapter;
-import com.google.gson.stream.JsonReader;
-import com.google.gson.stream.JsonWriter;
-import io.github.nstdio.http.ext.ImmutableResponseInfo.ResponseInfoBuilder;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpHeaders;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse.ResponseInfo;
-import java.nio.file.Path;
-import java.time.Clock;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.List;
-
-import static io.github.nstdio.http.ext.IOUtils.bufferedReader;
-import static io.github.nstdio.http.ext.IOUtils.bufferedWriter;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_CODE;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_HEADERS;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST_TIME;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE_TIME;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_VERSION;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_METHOD;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_TIMEOUT;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_URI;
-import static java.net.http.HttpRequest.BodyPublishers.noBody;
-
-class GsonMetadataSerializer implements MetadataSerializer {
- private final Gson gson;
- private final StreamFactory streamFactory;
-
- GsonMetadataSerializer() {
- this(new SimpleStreamFactory());
- }
-
- GsonMetadataSerializer(StreamFactory streamFactory) {
- var headers = new HttpHeadersTypeAdapter();
- var request = new HttpRequestTypeAdapter(headers);
- var response = new ResponseInfoTypeAdapter(headers);
-
- this.gson = new GsonBuilder()
- .disableHtmlEscaping()
- .registerTypeAdapter(CacheEntryMetadata.class, new CacheEntryMetadataTypeAdapter(request, response))
- .create();
- this.streamFactory = streamFactory;
- }
-
- @Override
- public void write(CacheEntryMetadata metadata, Path path) {
- try (var out = bufferedWriter(streamFactory.output(path))) {
- gson.toJson(metadata, out);
- } catch (IOException ignored) {
- }
- }
-
-
- @Override
- public CacheEntryMetadata read(Path path) {
- try (var in = bufferedReader(streamFactory.input(path))) {
- return gson.fromJson(in, CacheEntryMetadata.class);
- } catch (IOException ignored) {
- return null;
- }
- }
-
- private static class HttpHeadersTypeAdapter extends TypeAdapter {
-
- @Override
- public void write(JsonWriter out, HttpHeaders value) throws IOException {
- out.beginObject();
-
- for (var entry : value.map().entrySet()) {
- out.name(entry.getKey()).beginArray();
-
- for (String headerValue : entry.getValue()) {
- out.value(headerValue);
- }
-
- out.endArray();
- }
-
- out.endObject();
- }
-
- @Override
- public HttpHeaders read(JsonReader in) throws IOException {
- in.beginObject();
- var builder = new HttpHeadersBuilder();
-
- while (in.hasNext()) {
- String name = in.nextName();
- List values = new ArrayList<>(1);
-
- in.beginArray();
- while (in.hasNext()) {
- String value = in.nextString();
- values.add(value);
- }
- in.endArray();
-
- builder.setTrusted(name, values);
- }
-
- in.endObject();
- return builder.build();
- }
- }
-
- private static class HttpRequestTypeAdapter extends TypeAdapter {
- private final TypeAdapter headersTypeAdapter;
-
- HttpRequestTypeAdapter(TypeAdapter headersTypeAdapter) {
- this.headersTypeAdapter = headersTypeAdapter;
- }
-
- @Override
- public void write(JsonWriter out, HttpRequest value) throws IOException {
- out.beginObject();
-
- out.name(FILED_NAME_REQUEST_URI).value(value.uri().toASCIIString());
- out.name(FILED_NAME_REQUEST_METHOD).value(value.method());
-
- String timeoutString = value.timeout().map(Duration::toString).orElse(null);
- if (timeoutString != null) {
- out.name(FILED_NAME_REQUEST_TIMEOUT).value(timeoutString);
- }
- Integer versionOrd = value.version().map(Enum::ordinal).orElse(null);
- if (versionOrd != null) {
- out.name(FIELD_NAME_VERSION).value(versionOrd);
- }
-
- out.name(FIELD_NAME_HEADERS);
- headersTypeAdapter.write(out, value.headers());
-
- out.endObject();
- }
-
- @Override
- public HttpRequest read(JsonReader in) throws IOException {
- in.beginObject();
- HttpRequest.Builder builder = HttpRequest.newBuilder();
-
- while (in.hasNext()) {
- switch (in.nextName()) {
- case FILED_NAME_REQUEST_METHOD:
- builder.method(in.nextString(), noBody());
- break;
- case FILED_NAME_REQUEST_TIMEOUT:
- builder.timeout(Duration.parse(in.nextString()));
- break;
- case FIELD_NAME_VERSION:
- int version = in.nextInt();
- builder.version(HttpClient.Version.values()[version]);
- break;
- case FILED_NAME_REQUEST_URI:
- builder.uri(URI.create(in.nextString()));
- break;
- case FIELD_NAME_HEADERS:
- HttpHeaders headers = headersTypeAdapter.read(in);
- headers.map().forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
- break;
- }
- }
-
- in.endObject();
-
- return builder.build();
- }
- }
-
- private static class ResponseInfoTypeAdapter extends TypeAdapter {
- private final TypeAdapter headersTypeAdapter;
-
- ResponseInfoTypeAdapter(TypeAdapter headersTypeAdapter) {
- this.headersTypeAdapter = headersTypeAdapter;
- }
-
- @Override
- public void write(JsonWriter out, ResponseInfo value) throws IOException {
- out.beginObject();
-
- out.name(FIELD_NAME_CODE).value(value.statusCode());
- out.name(FIELD_NAME_VERSION).value(value.version().ordinal());
-
- out.name(FIELD_NAME_HEADERS);
- headersTypeAdapter.write(out, value.headers());
-
- out.endObject();
- }
-
- @Override
- public ResponseInfo read(JsonReader in) throws IOException {
- in.beginObject();
- ResponseInfoBuilder builder = ImmutableResponseInfo.builder();
- String fieldName;
-
- while (in.hasNext()) {
- fieldName = in.nextName();
-
- switch (fieldName) {
- case FIELD_NAME_CODE:
- builder.statusCode(in.nextInt());
- break;
- case FIELD_NAME_VERSION:
- int version = in.nextInt();
- builder.version(HttpClient.Version.values()[version]);
- break;
- case FIELD_NAME_HEADERS:
- HttpHeaders headers = headersTypeAdapter.read(in);
- builder.headers(headers);
- break;
- }
- }
-
- in.endObject();
-
- return builder.build();
- }
- }
-
- private static class CacheEntryMetadataTypeAdapter extends TypeAdapter {
- private final TypeAdapter requestTypeAdapter;
- private final TypeAdapter responseTypeAdapter;
-
- CacheEntryMetadataTypeAdapter(TypeAdapter requestTypeAdapter, TypeAdapter responseTypeAdapter) {
- this.requestTypeAdapter = requestTypeAdapter;
- this.responseTypeAdapter = responseTypeAdapter;
- }
-
- @Override
- public void write(JsonWriter out, CacheEntryMetadata value) throws IOException {
- out.beginObject();
-
- out.name(FIELD_NAME_REQUEST_TIME).value(value.requestTime());
- out.name(FIELD_NAME_RESPONSE_TIME).value(value.responseTime());
-
- out.name(FIELD_NAME_REQUEST);
- requestTypeAdapter.write(out, value.request());
-
- out.name(FIELD_NAME_RESPONSE);
- responseTypeAdapter.write(out, value.response());
-
- out.endObject();
- }
-
- @Override
- public CacheEntryMetadata read(JsonReader in) throws IOException {
- in.beginObject();
-
- long requestTime = -1;
- long responseTime = -1;
- HttpRequest request = null;
- ResponseInfo response = null;
-
- while (in.hasNext()) {
- switch (in.nextName()) {
- case FIELD_NAME_REQUEST_TIME:
- requestTime = in.nextLong();
- break;
- case FIELD_NAME_RESPONSE_TIME:
- responseTime = in.nextLong();
- break;
- case FIELD_NAME_REQUEST:
- request = requestTypeAdapter.read(in);
- break;
- case FIELD_NAME_RESPONSE:
- response = responseTypeAdapter.read(in);
- break;
- }
- }
-
- in.endObject();
- return CacheEntryMetadata.of(requestTime, responseTime, response, request, Clock.systemUTC());
- }
- }
-}
diff --git a/src/main/java/io/github/nstdio/http/ext/IOUtils.java b/src/main/java/io/github/nstdio/http/ext/IOUtils.java
index 7d53fd3..1841365 100644
--- a/src/main/java/io/github/nstdio/http/ext/IOUtils.java
+++ b/src/main/java/io/github/nstdio/http/ext/IOUtils.java
@@ -16,20 +16,12 @@
package io.github.nstdio.http.ext;
-import java.io.BufferedReader;
-import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
-import static java.nio.charset.StandardCharsets.UTF_8;
-
class IOUtils {
private IOUtils() {
}
@@ -75,12 +67,4 @@ static boolean createFile(Path path) {
return false;
}
}
-
- static BufferedReader bufferedReader(InputStream in) {
- return new BufferedReader(new InputStreamReader(in, UTF_8));
- }
-
- static BufferedWriter bufferedWriter(OutputStream out) {
- return new BufferedWriter(new OutputStreamWriter(out, UTF_8));
- }
}
diff --git a/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java b/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java
deleted file mode 100644
index 237bc21..0000000
--- a/src/main/java/io/github/nstdio/http/ext/JacksonMetadataSerializer.java
+++ /dev/null
@@ -1,362 +0,0 @@
-/*
- * Copyright (C) 2022 Edgar Asatryan
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package io.github.nstdio.http.ext;
-
-import com.fasterxml.jackson.core.JsonGenerator;
-import com.fasterxml.jackson.core.JsonParser;
-import com.fasterxml.jackson.databind.DeserializationContext;
-import com.fasterxml.jackson.databind.JavaType;
-import com.fasterxml.jackson.databind.JsonDeserializer;
-import com.fasterxml.jackson.databind.JsonMappingException;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.ObjectReader;
-import com.fasterxml.jackson.databind.ObjectWriter;
-import com.fasterxml.jackson.databind.SerializerProvider;
-import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
-import com.fasterxml.jackson.databind.module.SimpleModule;
-import com.fasterxml.jackson.databind.ser.std.StdSerializer;
-import com.fasterxml.jackson.databind.type.CollectionType;
-import com.fasterxml.jackson.databind.type.MapType;
-import com.fasterxml.jackson.databind.type.TypeFactory;
-import io.github.nstdio.http.ext.ImmutableResponseInfo.ResponseInfoBuilder;
-
-import java.io.IOException;
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpHeaders;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.Builder;
-import java.net.http.HttpResponse.ResponseInfo;
-import java.nio.file.Path;
-import java.time.Clock;
-import java.time.Duration;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.zip.GZIPInputStream;
-import java.util.zip.GZIPOutputStream;
-
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_CODE;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_HEADERS;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_REQUEST_TIME;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_RESPONSE_TIME;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FIELD_NAME_VERSION;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_METHOD;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_TIMEOUT;
-import static io.github.nstdio.http.ext.MetadataSerializationFields.FILED_NAME_REQUEST_URI;
-import static java.net.http.HttpRequest.BodyPublishers.noBody;
-import static java.nio.file.StandardOpenOption.CREATE;
-import static java.nio.file.StandardOpenOption.READ;
-import static java.nio.file.StandardOpenOption.WRITE;
-
-class JacksonMetadataSerializer implements MetadataSerializer {
-
- private final ObjectWriter writer;
- private final ObjectReader reader;
- private final StreamFactory streamFactory;
-
- JacksonMetadataSerializer() {
- this(new SimpleStreamFactory());
- }
-
- JacksonMetadataSerializer(StreamFactory streamFactory) {
- ObjectMapper mapper = createMapper();
- this.writer = mapper.writerFor(CacheEntryMetadata.class);
- this.reader = mapper.readerFor(CacheEntryMetadata.class);
- this.streamFactory = streamFactory;
- }
-
- private static JsonMappingException unexpectedFieldException(JsonParser p, String fieldName) {
- return new JsonMappingException(p, "Unexpected field name: " + fieldName);
- }
-
- private ObjectMapper createMapper() {
- ObjectMapper mapper = new ObjectMapper();
- SimpleModule simpleModule = new SimpleModule("JsonMetadataSerializer");
-
- simpleModule.addSerializer(CacheEntryMetadata.class, new CacheMetadataSerializer());
- simpleModule.addDeserializer(CacheEntryMetadata.class, new CacheMetadataDeserializer());
-
- simpleModule.addSerializer(HttpRequest.class, new HttpRequestSerializer());
- simpleModule.addDeserializer(HttpRequest.class, new HttpRequestDeserializer());
-
- simpleModule.addSerializer(ResponseInfo.class, new ResponseInfoSerializer());
- simpleModule.addDeserializer(ResponseInfo.class, new ResponseInfoDeserializer());
-
- simpleModule.addSerializer(HttpHeaders.class, new HttpHeadersSerializer());
- simpleModule.addDeserializer(HttpHeaders.class, new HttpHeadersDeserializer());
-
- mapper.registerModule(simpleModule);
- return mapper;
- }
-
- @Override
- public void write(CacheEntryMetadata metadata, Path path) {
- try (var out = new GZIPOutputStream(streamFactory.output(path, WRITE, CREATE))) {
- writer.writeValue(out, metadata);
- } catch (IOException ignore) {
- // noop
- }
- }
-
- @Override
- public CacheEntryMetadata read(Path path) {
- try (var in = new GZIPInputStream(streamFactory.input(path, READ))) {
- return reader.readValue(in);
- } catch (IOException e) {
- return null;
- }
- }
-
- static class CacheMetadataSerializer extends StdSerializer {
- private static final long serialVersionUID = 1L;
-
- CacheMetadataSerializer() {
- super(CacheEntryMetadata.class);
- }
-
- @Override
- public void serialize(CacheEntryMetadata value, JsonGenerator gen, SerializerProvider provider) throws IOException {
- gen.writeStartObject();
-
- gen.writeNumberField(FIELD_NAME_REQUEST_TIME, value.requestTime());
- gen.writeNumberField(FIELD_NAME_RESPONSE_TIME, value.responseTime());
- gen.writeObjectField(FIELD_NAME_REQUEST, value.request());
- gen.writeObjectField(FIELD_NAME_RESPONSE, value.response());
-
- gen.writeEndObject();
- }
- }
-
- private static class CacheMetadataDeserializer extends StdDeserializer {
- private static final long serialVersionUID = 1L;
- private final JavaType requestType = TypeFactory.defaultInstance().constructType(HttpRequest.class);
- private final JavaType responseType = TypeFactory.defaultInstance().constructType(ResponseInfo.class);
-
- CacheMetadataDeserializer() {
- super(CacheEntryMetadata.class);
- }
-
- @Override
- public CacheEntryMetadata deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
- long requestTime = -1;
- long responseTime = -1;
- HttpRequest request = null;
- ResponseInfo response = null;
- String fieldName;
-
- while ((fieldName = p.nextFieldName()) != null) {
- switch (fieldName) {
- case FIELD_NAME_REQUEST_TIME:
- requestTime = p.nextLongValue(-1);
- break;
- case FIELD_NAME_RESPONSE_TIME:
- responseTime = p.nextLongValue(-1);
- break;
- case FIELD_NAME_REQUEST:
- p.nextToken();
- request = (HttpRequest) ctxt.findRootValueDeserializer(requestType).deserialize(p, ctxt);
- break;
- case FIELD_NAME_RESPONSE:
- p.nextToken();
- response = (ResponseInfo) ctxt.findRootValueDeserializer(responseType).deserialize(p, ctxt);
- break;
- default:
- throw unexpectedFieldException(p, fieldName);
- }
- }
-
- return CacheEntryMetadata.of(requestTime, responseTime, response, request, Clock.systemUTC());
- }
- }
-
- static class HttpRequestSerializer extends StdSerializer {
- private static final long serialVersionUID = 1L;
-
- HttpRequestSerializer() {
- super(HttpRequest.class);
- }
-
- @Override
- public void serialize(HttpRequest value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
- gen.writeStartObject();
- gen.writeStringField(FILED_NAME_REQUEST_METHOD, value.method());
-
- String timeoutString = value.timeout().map(Duration::toString).orElse(null);
- if (timeoutString != null) {
- gen.writeStringField(FILED_NAME_REQUEST_TIMEOUT, timeoutString);
- }
-
- gen.writeStringField(FILED_NAME_REQUEST_URI, value.uri().toASCIIString());
- Integer versionOrd = value.version().map(Enum::ordinal).orElse(null);
- if (versionOrd != null) {
- gen.writeNumberField(FIELD_NAME_VERSION, versionOrd);
- }
-
- gen.writeObjectField(FIELD_NAME_HEADERS, value.headers());
-
- gen.writeEndObject();
- }
- }
-
- static class HttpRequestDeserializer extends StdDeserializer {
- private static final long serialVersionUID = 1L;
- private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class);
-
- HttpRequestDeserializer() {
- super(HttpRequest.class);
- }
-
- @Override
- public HttpRequest deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
- Builder builder = HttpRequest.newBuilder();
- String fieldName;
-
- while ((fieldName = p.nextFieldName()) != null) {
- switch (fieldName) {
- case FILED_NAME_REQUEST_METHOD:
- builder.method(p.nextTextValue(), noBody());
- break;
- case FILED_NAME_REQUEST_TIMEOUT:
- String timeout = p.nextTextValue();
- builder.timeout(Duration.parse(timeout));
- break;
- case FIELD_NAME_VERSION:
- int version = p.nextIntValue(-1);
- builder.version(HttpClient.Version.values()[version]);
- break;
- case FILED_NAME_REQUEST_URI:
- builder.uri(URI.create(p.nextTextValue()));
- break;
- case FIELD_NAME_HEADERS:
- p.nextToken();
- HttpHeaders headers = (HttpHeaders) ctxt.findRootValueDeserializer(headersType).deserialize(p, ctxt);
- headers.map().forEach((name, values) -> values.forEach(value -> builder.header(name, value)));
- break;
- default:
- throw unexpectedFieldException(p, fieldName);
- }
- }
-
- return builder.build();
- }
- }
-
- static class ResponseInfoSerializer extends StdSerializer {
- private static final long serialVersionUID = 1L;
-
- ResponseInfoSerializer() {
- super(ResponseInfo.class);
- }
-
- @Override
- public void serialize(ResponseInfo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
- gen.writeStartObject();
-
- gen.writeNumberField(FIELD_NAME_CODE, value.statusCode());
- gen.writeNumberField(FIELD_NAME_VERSION, value.version().ordinal());
- gen.writeObjectField(FIELD_NAME_HEADERS, value.headers());
-
- gen.writeEndObject();
- }
- }
-
- static class ResponseInfoDeserializer extends StdDeserializer {
- private static final long serialVersionUID = 1L;
- private final JavaType headersType = TypeFactory.defaultInstance().constructType(HttpHeaders.class);
- private final HttpClient.Version[] values = HttpClient.Version.values();
-
- ResponseInfoDeserializer() {
- super(ResponseInfo.class);
- }
-
- @Override
- public ResponseInfo deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
- ResponseInfoBuilder builder = ImmutableResponseInfo.builder();
-
- String fieldName;
- while ((fieldName = p.nextFieldName()) != null) {
- switch (fieldName) {
- case FIELD_NAME_CODE:
- builder.statusCode(p.nextIntValue(-1));
- break;
- case FIELD_NAME_VERSION:
- builder.version(values[p.nextIntValue(-1)]);
- break;
- case FIELD_NAME_HEADERS:
- JsonDeserializer