Skip to content

Commit 505d4cb

Browse files
authored
Merge pull request #75 from nstdio/metadata-serialization
feat: Replace JSON metadata serializers with binary version.
2 parents b6367e2 + 9c3c576 commit 505d4cb

18 files changed

+852
-1061
lines changed

spotbugs.exclude.xml

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,6 @@
4040
<Method name="&lt;init&gt;"/>
4141
<Bug pattern="EI_EXPOSE_REP2"/>
4242
</Match>
43-
<Match>
44-
<Class name="io.github.nstdio.http.ext.GsonMetadataSerializer$HttpRequestTypeAdapter"/>
45-
<Method name="read"/>
46-
<Bug pattern="SF_SWITCH_NO_DEFAULT"/>
47-
</Match>
48-
<Match>
49-
<Class name="io.github.nstdio.http.ext.GsonMetadataSerializer$ResponseInfoTypeAdapter"/>
50-
<Method name="read"/>
51-
<Bug pattern="SF_SWITCH_NO_DEFAULT"/>
52-
</Match>
53-
<Match>
54-
<Class name="io.github.nstdio.http.ext.GsonMetadataSerializer$CacheEntryMetadataTypeAdapter"/>
55-
<Method name="read"/>
56-
<Bug pattern="SF_SWITCH_NO_DEFAULT"/>
57-
</Match>
5843
<Match>
5944
<Class name="io.github.nstdio.http.ext.DiskCache"/>
6045
<Method name="restore"/>
@@ -96,4 +81,13 @@
9681
<Method name="createFile"/>
9782
<Bug pattern="EXS_EXCEPTION_SOFTENING_RETURN_FALSE"/>
9883
</Match>
84+
<Match>
85+
<Class name="~io\.github\.nstdio\.http\.ext\.BinaryMetadataSerializer\$Externalizable.+"/>
86+
<BugCode name="SECOBDES, IMC"/>
87+
</Match>
88+
<Match>
89+
<Class name="io.github.nstdio.http.ext.BinaryMetadataSerializer"/>
90+
<Method name="read"/>
91+
<BugCode name="SECOBDES"/>
92+
</Match>
9993
</FindBugsFilter>
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
* Copyright (C) 2022 Edgar Asatryan
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.github.nstdio.http.ext;
18+
19+
import java.io.Externalizable;
20+
import java.io.IOException;
21+
import java.io.ObjectInput;
22+
import java.io.ObjectInputStream;
23+
import java.io.ObjectOutput;
24+
import java.io.ObjectOutputStream;
25+
import java.net.URI;
26+
import java.net.http.HttpClient.Version;
27+
import java.net.http.HttpHeaders;
28+
import java.net.http.HttpRequest;
29+
import java.net.http.HttpRequest.Builder;
30+
import java.net.http.HttpResponse.ResponseInfo;
31+
import java.nio.file.Path;
32+
import java.time.Clock;
33+
import java.time.Duration;
34+
import java.util.ArrayList;
35+
import java.util.HashMap;
36+
import java.util.List;
37+
import java.util.Map;
38+
39+
import static java.net.http.HttpRequest.BodyPublishers.noBody;
40+
41+
class BinaryMetadataSerializer implements MetadataSerializer {
42+
private final StreamFactory streamFactory;
43+
44+
BinaryMetadataSerializer(StreamFactory streamFactory) {
45+
this.streamFactory = streamFactory;
46+
}
47+
48+
@Override
49+
public void write(CacheEntryMetadata metadata, Path path) {
50+
try (var out = new ObjectOutputStream(streamFactory.output(path))) {
51+
out.writeObject(new ExternalizableMetadata(metadata));
52+
} catch (IOException ignored) {
53+
}
54+
}
55+
56+
@Override
57+
public CacheEntryMetadata read(Path path) {
58+
try (var input = new ObjectInputStream(streamFactory.input(path))) {
59+
return ((ExternalizableMetadata) input.readObject()).metadata;
60+
} catch (IOException | ClassNotFoundException ignored) {
61+
return null;
62+
}
63+
}
64+
65+
static final class ExternalizableMetadata implements Externalizable {
66+
private static final long serialVersionUID = 15052410042022L;
67+
private CacheEntryMetadata metadata;
68+
69+
public ExternalizableMetadata() {
70+
}
71+
72+
ExternalizableMetadata(CacheEntryMetadata metadata) {
73+
this.metadata = metadata;
74+
}
75+
76+
@Override
77+
public void writeExternal(ObjectOutput out) throws IOException {
78+
out.writeLong(metadata.requestTime());
79+
out.writeLong(metadata.responseTime());
80+
81+
out.writeObject(new ExternalizableResponseInfo(metadata.response()));
82+
out.writeObject(new ExternalizableHttpRequest(metadata.request()));
83+
}
84+
85+
@Override
86+
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
87+
final long requestTime = in.readLong();
88+
final long responseTime = in.readLong();
89+
ResponseInfo responseInfo = ((ExternalizableResponseInfo) in.readObject()).responseInfo;
90+
HttpRequest request = ((ExternalizableHttpRequest) in.readObject()).request;
91+
92+
metadata = CacheEntryMetadata.of(requestTime, responseTime, responseInfo, request, Clock.systemUTC());
93+
}
94+
}
95+
96+
static class ExternalizableHttpRequest implements Externalizable {
97+
private static final long serialVersionUID = 15052410042022L;
98+
private HttpRequest request;
99+
100+
public ExternalizableHttpRequest() {
101+
}
102+
103+
ExternalizableHttpRequest(HttpRequest request) {
104+
this.request = request;
105+
}
106+
107+
@Override
108+
public void writeExternal(ObjectOutput out) throws IOException {
109+
out.writeObject(request.uri());
110+
out.writeUTF(request.method());
111+
out.writeObject(request.version().orElse(null));
112+
out.writeObject(request.timeout().orElse(null));
113+
out.writeObject(new ExternalizableHttpHeaders(request.headers()));
114+
}
115+
116+
@Override
117+
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
118+
Builder builder = HttpRequest.newBuilder()
119+
.uri((URI) in.readObject())
120+
.method(in.readUTF(), noBody());
121+
122+
Version v = (Version) in.readObject();
123+
if (v != null) {
124+
builder.version(v);
125+
}
126+
Duration t = (Duration) in.readObject();
127+
if (t != null) {
128+
builder.timeout(t);
129+
}
130+
131+
Map<String, List<String>> headers = ((ExternalizableHttpHeaders) in.readObject()).headers.map();
132+
for (var entry : headers.entrySet()) {
133+
for (String value : entry.getValue()) {
134+
builder.header(entry.getKey(), value);
135+
}
136+
}
137+
138+
request = builder.build();
139+
}
140+
}
141+
142+
static class ExternalizableResponseInfo implements Externalizable {
143+
private static final long serialVersionUID = 15052410042022L;
144+
private ResponseInfo responseInfo;
145+
146+
public ExternalizableResponseInfo() {
147+
}
148+
149+
ExternalizableResponseInfo(ResponseInfo responseInfo) {
150+
this.responseInfo = responseInfo;
151+
}
152+
153+
@Override
154+
public void writeExternal(ObjectOutput out) throws IOException {
155+
out.writeInt(responseInfo.statusCode());
156+
out.writeObject(new ExternalizableHttpHeaders(responseInfo.headers()));
157+
out.writeObject(responseInfo.version());
158+
}
159+
160+
@Override
161+
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
162+
responseInfo = ImmutableResponseInfo.builder()
163+
.statusCode(in.readInt())
164+
.headers(((ExternalizableHttpHeaders) in.readObject()).headers)
165+
.version((Version) in.readObject())
166+
.build();
167+
}
168+
}
169+
170+
static class ExternalizableHttpHeaders implements Externalizable {
171+
private static final long serialVersionUID = 15052410042022L;
172+
private static final int maxMapSize = 1024;
173+
private static final int maxListSize = 256;
174+
175+
private final boolean respectLimits;
176+
HttpHeaders headers;
177+
178+
public ExternalizableHttpHeaders() {
179+
this(null, true);
180+
}
181+
182+
ExternalizableHttpHeaders(HttpHeaders headers) {
183+
this(headers, true);
184+
}
185+
186+
ExternalizableHttpHeaders(HttpHeaders headers, boolean respectLimits) {
187+
this.headers = headers;
188+
this.respectLimits = respectLimits;
189+
}
190+
191+
private static String mapSizeExceedMessage(int mapSize) {
192+
return String.format("The headers size exceeds max allowed number. Size: %d, Max:%d", mapSize, maxMapSize);
193+
}
194+
195+
private static String listSizeExceedMessage(String headerName, int size) {
196+
return String.format("The values for header '%s' exceeds maximum allowed number. Size:%d, Max:%d",
197+
headerName, size, maxListSize);
198+
}
199+
200+
private static IOException corruptedStream(String desc) {
201+
return new IOException("Corrupted stream: " + desc);
202+
}
203+
204+
@Override
205+
public void writeExternal(ObjectOutput out) throws IOException {
206+
Map<String, List<String>> map = headers.map();
207+
208+
int mapSize = map.size();
209+
if (respectLimits && mapSize > maxMapSize) {
210+
throw new IOException(mapSizeExceedMessage(mapSize));
211+
}
212+
213+
// write map size
214+
out.writeInt(mapSize);
215+
for (var entry : map.entrySet()) {
216+
// header name
217+
String headerName = entry.getKey();
218+
out.writeUTF(headerName);
219+
220+
List<String> values = entry.getValue();
221+
int valuesSize = values.size();
222+
checkValuesSize(headerName, valuesSize);
223+
224+
// write values size
225+
out.writeInt(valuesSize);
226+
// header values
227+
for (String value : values) out.writeUTF(value);
228+
}
229+
}
230+
231+
@Override
232+
public void readExternal(ObjectInput in) throws IOException {
233+
final int mapSize = in.readInt();
234+
checkMapSize(mapSize);
235+
236+
if (mapSize == 0) {
237+
headers = HttpHeaders.of(Map.of(), Headers.ALLOW_ALL);
238+
return;
239+
}
240+
241+
var map = new HashMap<String, List<String>>(mapSize, 1.0f);
242+
for (int i = 0; i < mapSize; i++) {
243+
String headerName = in.readUTF();
244+
245+
int valuesSize = in.readInt();
246+
checkValuesSize(headerName, valuesSize);
247+
248+
var values = new ArrayList<String>(valuesSize);
249+
for (int j = 0; j < valuesSize; j++) values.add(in.readUTF());
250+
251+
map.put(headerName, values);
252+
}
253+
254+
headers = HttpHeaders.of(map, Headers.ALLOW_ALL);
255+
}
256+
257+
private void checkValuesSize(String headerName, int valuesSize) throws IOException {
258+
if (valuesSize <= 0) throw corruptedStream("list size should be positive");
259+
else if (respectLimits && valuesSize > maxListSize)
260+
throw new IOException(listSizeExceedMessage(headerName, valuesSize));
261+
}
262+
263+
private void checkMapSize(int mapSize) throws IOException {
264+
if (mapSize < 0) throw corruptedStream("map size cannot be negative");
265+
else if (respectLimits && mapSize > maxMapSize) throw new IOException(mapSizeExceedMessage(mapSize));
266+
}
267+
}
268+
}

src/main/java/io/github/nstdio/http/ext/Cache.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,15 +47,11 @@ static InMemoryCacheBuilder newInMemoryCacheBuilder() {
4747
}
4848

4949
/**
50-
* Creates a new {@code DiskCacheBuilder} instance. Requires Jackson form dumping cache files on disk.
50+
* Creates a new {@code DiskCacheBuilder} instance.
5151
*
5252
* @return the new {@code DiskCacheBuilder}.
53-
*
54-
* @throws IllegalStateException When Jackson (a.k.a. ObjectMapper) is not in classpath.
5553
*/
5654
static DiskCacheBuilder newDiskCacheBuilder() {
57-
MetadataSerializer.requireAvailability();
58-
5955
return new DiskCacheBuilder();
6056
}
6157

src/main/java/io/github/nstdio/http/ext/ExtendedHttpClient.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public class ExtendedHttpClient extends HttpClient {
6262
* @return an {@code ExtendedHttpClient.Builder}
6363
*/
6464
public static ExtendedHttpClient.Builder newBuilder() {
65-
return new ExtendedHttpClient.Builder();
65+
return new ExtendedHttpClient.Builder(HttpClient.newBuilder());
6666
}
6767

6868
/**
@@ -200,7 +200,7 @@ private <T> Sender<T> syncSender() {
200200
return ctx -> {
201201
try {
202202
return completedFuture(delegate.send(ctx.request(), ctx.bodyHandler()));
203-
} catch (IOException | InterruptedException e) {
203+
} catch (Throwable e) {
204204
return CompletableFuture.failedFuture(e);
205205
}
206206
};
@@ -221,11 +221,12 @@ interface Sender<T> extends Function<RequestContext, CompletableFuture<HttpRespo
221221
}
222222

223223
public static class Builder implements HttpClient.Builder {
224-
private final HttpClient.Builder delegate = HttpClient.newBuilder();
224+
private final HttpClient.Builder delegate;
225225
private boolean transparentEncoding;
226226
private Cache cache = Cache.noop();
227227

228-
Builder() {
228+
Builder(HttpClient.Builder delegate) {
229+
this.delegate = delegate;
229230
}
230231

231232
//<editor-fold desc="Delegating Methods">

0 commit comments

Comments
 (0)