Skip to content

Commit b5d77a2

Browse files
committed
Initial client add
1 parent aa32fcb commit b5d77a2

31 files changed

+1825
-0
lines changed

client/pom.xml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<parent>
6+
<artifactId>java11-oss</artifactId>
7+
<groupId>org.avaje</groupId>
8+
<version>2.1.2</version>
9+
</parent>
10+
11+
<modelVersion>4.0.0</modelVersion>
12+
13+
<artifactId>http-client</artifactId>
14+
<groupId>org.avaje.beta.http</groupId>
15+
<version>0.1-SNAPSHOT</version>
16+
17+
<dependencies>
18+
19+
<dependency>
20+
<groupId>org.slf4j</groupId>
21+
<artifactId>slf4j-api</artifactId>
22+
<version>1.7.25</version>
23+
</dependency>
24+
25+
<dependency>
26+
<groupId>io.dinject</groupId>
27+
<artifactId>dinject-controller</artifactId>
28+
<version>1.21</version>
29+
<scope>test</scope>
30+
</dependency>
31+
32+
<dependency>
33+
<groupId>com.fasterxml.jackson.core</groupId>
34+
<artifactId>jackson-databind</artifactId>
35+
<version>2.11.1</version>
36+
</dependency>
37+
38+
<!-- test dependencies -->
39+
40+
<dependency>
41+
<groupId>org.junit.jupiter</groupId>
42+
<artifactId>junit-jupiter-api</artifactId>
43+
<version>5.5.2</version>
44+
<scope>test</scope>
45+
</dependency>
46+
47+
<dependency>
48+
<groupId>org.assertj</groupId>
49+
<artifactId>assertj-core</artifactId>
50+
<version>3.16.1</version>
51+
<scope>test</scope>
52+
</dependency>
53+
54+
</dependencies>
55+
56+
</project>
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package io.avaje.http.client;
2+
3+
import java.util.List;
4+
5+
public interface BodyAdapter {
6+
7+
BodyWriter beanWriter(Class<?> cls);
8+
9+
<T> BodyReader<T> beanReader(Class<T> cls);
10+
11+
<T> BodyReader<List<T>> listReader(Class<T> cls);
12+
13+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package io.avaje.http.client;
2+
3+
public class BodyContent {
4+
5+
public static final String JSON_UTF8 = "application/json; charset=UTF-8";
6+
7+
private final String contentType;
8+
9+
private final byte[] content;
10+
11+
public static BodyContent asJson(byte[] content) {
12+
return new BodyContent(JSON_UTF8, content);
13+
}
14+
15+
public BodyContent(String contentType, byte[] content) {
16+
this.contentType = contentType;
17+
this.content = content;
18+
}
19+
20+
public String contentType() {
21+
return contentType;
22+
}
23+
24+
public byte[] content() {
25+
return content;
26+
}
27+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.avaje.http.client;
2+
3+
public interface BodyReader<T> {
4+
5+
T read(BodyContent content);
6+
7+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package io.avaje.http.client;
2+
3+
/**
4+
* Writes beans as content for a specific content type.
5+
*/
6+
public interface BodyWriter {
7+
8+
/**
9+
* Write the bean as content using the default content type.
10+
* <p>
11+
* Used when all beans sent via POST, PUT, PATCH will be sent as
12+
* a single content type like <code>application/json; charset=utf8</code>.
13+
*/
14+
BodyContent write(Object bean);
15+
16+
/**
17+
* Write the bean as content with the requested content type.
18+
* <p>
19+
* The writer is expected to use the given contentType to determine
20+
* how to write the bean as content.
21+
*/
22+
BodyContent write(Object bean, String contentType);
23+
24+
}
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package io.avaje.http.client;
2+
3+
import java.io.IOException;
4+
import java.net.http.HttpClient;
5+
import java.net.http.HttpHeaders;
6+
import java.net.http.HttpRequest;
7+
import java.net.http.HttpResponse;
8+
import java.time.Duration;
9+
import java.util.List;
10+
import java.util.Map;
11+
12+
class DHttpClientContext implements HttpClientContext {
13+
14+
private final HttpClient httpClient;
15+
private final String baseUrl;
16+
private final Duration requestTimeout;
17+
private final BodyAdapter bodyAdapter;
18+
private final RequestListener requestListener;
19+
20+
DHttpClientContext(HttpClient httpClient, String baseUrl, Duration requestTimeout, BodyAdapter bodyAdapter, RequestListener requestListener) {
21+
this.httpClient = httpClient;
22+
this.baseUrl = baseUrl;
23+
this.requestTimeout = requestTimeout;
24+
this.bodyAdapter = bodyAdapter;
25+
this.requestListener = requestListener;
26+
}
27+
28+
@Override
29+
public HttpClientRequest request() {
30+
return new DHttpClientRequest(this, requestTimeout);
31+
}
32+
33+
@Override
34+
public BodyAdapter converters() {
35+
return bodyAdapter;
36+
}
37+
38+
@Override
39+
public UrlBuilder url() {
40+
return new UrlBuilder(baseUrl);
41+
}
42+
43+
@Override
44+
public HttpClient httpClient() {
45+
return httpClient;
46+
}
47+
48+
@Override
49+
public void checkResponse(HttpResponse<?> response) {
50+
if (response.statusCode() >= 300) {
51+
throw new HttpException(response, this);
52+
}
53+
}
54+
55+
void check(HttpResponse<byte[]> response) {
56+
if (response.statusCode() >= 300) {
57+
throw new HttpException(this, response);
58+
}
59+
}
60+
@Override
61+
public BodyContent readContent(HttpResponse<byte[]> httpResponse) {
62+
byte[] bodyBytes = decodeContent(httpResponse);
63+
final String contentType = getContentType(httpResponse);
64+
return new BodyContent(contentType, bodyBytes);
65+
}
66+
67+
String getContentType(HttpResponse<byte[]> httpResponse) {
68+
return firstHeader(httpResponse.headers(), "Content-Type", "content-type");
69+
}
70+
71+
String getContentEncoding(HttpResponse<byte[]> httpResponse) {
72+
return firstHeader(httpResponse.headers(), "Content-Encoding", "content-encoding");
73+
}
74+
75+
@Override
76+
public byte[] decodeContent(String encoding, byte[] body) {
77+
if (encoding.equals("gzip")) {
78+
return GzipUtil.gzipDecode(body);
79+
}
80+
// todo: register decoders with context and use them
81+
return body;
82+
}
83+
84+
public byte[] decodeContent(HttpResponse<byte[]> httpResponse) {
85+
String encoding = getContentEncoding(httpResponse);
86+
return encoding == null ? httpResponse.body() : decodeContent(encoding, httpResponse.body());
87+
}
88+
89+
String firstHeader(HttpHeaders headers, String... names) {
90+
final Map<String, List<String>> map = headers.map();
91+
for (String key : names) {
92+
final List<String> values = map.get(key);
93+
if (values != null && !values.isEmpty()) {
94+
return values.get(0);
95+
}
96+
}
97+
return null;
98+
}
99+
100+
<T> HttpResponse<T> send(HttpRequest.Builder requestBuilder, HttpResponse.BodyHandler<T> bodyHandler) {
101+
final HttpRequest request = applyFilters(requestBuilder).build();
102+
try {
103+
return httpClient.send(request, bodyHandler);
104+
} catch (IOException e) {
105+
throw new HttpException(499, e);
106+
} catch (InterruptedException e) {
107+
Thread.currentThread().interrupt();
108+
throw new HttpException(499, e);
109+
}
110+
}
111+
112+
private HttpRequest.Builder applyFilters(HttpRequest.Builder hreq) {
113+
return hreq;
114+
}
115+
116+
BodyContent write(Object bean, String contentType) {
117+
return bodyAdapter.beanWriter(bean.getClass()).write(bean, contentType);
118+
}
119+
120+
<T> T readBean(Class<T> cls, BodyContent content) {
121+
return bodyAdapter.beanReader(cls).read(content);
122+
}
123+
124+
<T> List<T> readList(Class<T> cls, BodyContent content) {
125+
return bodyAdapter.listReader(cls).read(content);
126+
}
127+
128+
129+
void afterResponse(DHttpClientRequest request) {
130+
if (requestListener != null) {
131+
requestListener.response(request.listenerEvent());
132+
}
133+
}
134+
135+
void afterResponseHandler(DHttpClientRequest request) {
136+
if (requestListener != null) {
137+
requestListener.response(request.listenerEvent());
138+
}
139+
}
140+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package io.avaje.http.client;
2+
3+
import java.net.http.HttpClient;
4+
import java.time.Duration;
5+
6+
import static java.util.Objects.requireNonNull;
7+
8+
class DHttpClientContextBuilder implements HttpClientContext.Builder {
9+
10+
private HttpClient client;
11+
12+
private String baseUrl;
13+
14+
private Duration requestTimeout = Duration.ofSeconds(20);
15+
16+
private BodyAdapter bodyAdapter;
17+
18+
private RequestListener requestListener;
19+
20+
DHttpClientContextBuilder() {
21+
}
22+
23+
@Override
24+
public HttpClientContext.Builder with(HttpClient client) {
25+
this.client = client;
26+
return this;
27+
}
28+
29+
@Override
30+
public HttpClientContext.Builder withBaseUrl(String baseUrl) {
31+
this.baseUrl = baseUrl;
32+
return this;
33+
}
34+
35+
@Override
36+
public HttpClientContext.Builder withRequestTimeout(Duration requestTimeout) {
37+
this.requestTimeout = requestTimeout;
38+
return this;
39+
}
40+
41+
@Override
42+
public HttpClientContext.Builder withBodyAdapter(BodyAdapter adapter) {
43+
this.bodyAdapter = adapter;
44+
return this;
45+
}
46+
47+
@Override
48+
public HttpClientContext.Builder withRequestListener(RequestListener requestListener) {
49+
this.requestListener = requestListener;
50+
return this;
51+
}
52+
53+
@Override
54+
public HttpClientContext build() {
55+
requireNonNull(baseUrl, "baseUrl is not specified");
56+
requireNonNull(requestTimeout, "requestTimeout is not specified");
57+
if (client == null) {
58+
client = defaultClient();
59+
}
60+
return new DHttpClientContext(client, baseUrl, requestTimeout, bodyAdapter, requestListener);
61+
}
62+
63+
private HttpClient defaultClient() {
64+
return HttpClient.newBuilder()
65+
.connectTimeout(Duration.ofSeconds(20))
66+
.build();
67+
}
68+
69+
}

0 commit comments

Comments
 (0)