Skip to content

Commit a17b7ea

Browse files
committed
Async HTTP with Java 11
1 parent 2eddc22 commit a17b7ea

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+684
-491
lines changed

CHANGELOG.md

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
## Java 11
33
This library now requires Java 11 or higher.
44

5+
## Async API
6+
All methods that make requests to the Exaroton API are now asynchronous. This means that all methods that return a value
7+
now return a `CompletableFuture` instead. To get the result of the request you can use `CompletableFuture#get()` or
8+
`CompletableFuture#join()`.
9+
10+
This also changes where exceptions are thrown. Now the following rules apply:
11+
- IOExceptions are thrown directly by the API methods
12+
- APIExceptions cause the CompletableFuture to complete exceptionally
13+
14+
If you use `join()` or `get()` a `CompletionException` containing the `APIException` will be thrown.
15+
516
## ServerStatus
617
The `ServerStatus` class is now an enum instead of a class with static `int` fields. Each status has a numeric
718
value (`getValue`), a display name (`getName`) and a brand color (`getColor`).
@@ -12,12 +23,30 @@ If a status code is unknown because the API client has not been updated `OFFLINE
1223
This library no longer depends directly on any SLF4J implementation. If you want to see log messages
1324
from this library, you must include an SLF4J implementation in your project.
1425

26+
### API Requests
27+
28+
#### Request Bodies
29+
`ApiRequest#getBody()` and `ApiRequest#getInputStream()` have been replaced by `ApiRequest#getBodyPublisher()`. For a
30+
JSON body you can use `ApiRequest#jsonBodyPublisher(Object)`. This only affects users who extended the request classes.
31+
32+
`PutFileDataRequest`'s constructor has been changed to accept a `Supplier<InputStream>` instead of an `InputStream`.
33+
34+
#### Request Methods
35+
`ApiRequest#requestRaw()`, `ApiRequest#requestString()` and `ApiRequest#request()` have been replaced by
36+
`ExarotonClient#request(ApiRequest, HttpResponse.BodyHandler)` use the respective body handlers to get an input stream,
37+
string or object.
38+
39+
1540
### Other
1641
- `Server#subscribe` and `Server#unsubscribe` now accept the `StreamName` enum instead of any string
1742
- Arrays have been Replaced by Collection's in almost all places
1843
- Config options now return a generic type instead of `Object`
44+
- Renamed `ServerFile#getInfo` to `ServerFile#get`
1945
- Removed `ExarotonClient#getGson()` and `WebsocketClient#getGson()`
20-
- Renamed `Server#setClient` to `Server#init` and added parameter `gson`
46+
- Removed `Server#setClient` and `CreditPool#setClient`
47+
- Removed `ExarotonClient#getBaseUrl()` and `ExarotonClient#createConnection(String, String)`
48+
- Many classes are now final
49+
- Added `ApiStatus` annotations to many classes and methods
2150

2251
## Improvements
2352
- Update dependencies

src/main/java/com/exaroton/api/APIRequest.java

Lines changed: 31 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,30 @@
33

44
import com.google.gson.Gson;
55
import com.google.gson.reflect.TypeToken;
6-
import org.jetbrains.annotations.NotNull;
76

8-
import java.io.*;
9-
import java.lang.reflect.Type;
10-
import java.net.HttpURLConnection;
11-
import java.nio.charset.StandardCharsets;
7+
import java.net.URISyntaxException;
8+
import java.net.URL;
9+
import java.net.http.HttpRequest;
1210
import java.util.HashMap;
1311
import java.util.Map;
14-
import java.util.Objects;
15-
import java.util.stream.Collectors;
1612

17-
public abstract class APIRequest<Datatype> {
13+
public abstract class APIRequest<Response> {
1814
/**
19-
* exaroton API client
15+
* Build the HttpRequest
16+
* @param builder HttpRequest builder with preconfigured options
17+
* @param baseUrl base URL
18+
* @return HttpRequest
19+
* @throws URISyntaxException if the constructed URI is invalid
2020
*/
21-
protected final ExarotonClient client;
21+
public HttpRequest build(Gson gson, HttpRequest.Builder builder, URL baseUrl) throws URISyntaxException {
22+
builder.uri(baseUrl.toURI().resolve(getPath()))
23+
.method(this.getMethod(), getBodyPublisher(gson, builder));
2224

23-
/**
24-
* Gson instance used for (de-)serialization
25-
*/
26-
protected final Gson gson;
25+
for (Map.Entry<String, String> header : this.getHeaders().entrySet()) {
26+
builder.header(header.getKey(), header.getValue());
27+
}
2728

28-
public APIRequest(@NotNull ExarotonClient client, @NotNull Gson gson) {
29-
this.client = Objects.requireNonNull(client);
30-
this.gson = Objects.requireNonNull(gson);
29+
return builder.build();
3130
}
3231

3332
/**
@@ -63,78 +62,6 @@ protected String getPath() {
6362
return path;
6463
}
6564

66-
/**
67-
* Execute this API Request and get the raw InputStream
68-
* @return InputStream
69-
* @throws APIException if the request fails
70-
*/
71-
public InputStream requestRaw() throws APIException {
72-
HttpURLConnection connection = null;
73-
InputStream stream;
74-
try {
75-
connection = client.createConnection(this.getMethod(), this.getPath());
76-
for (Map.Entry<String, String> entry : this.getHeaders().entrySet()) {
77-
connection.setRequestProperty(entry.getKey(), entry.getValue());
78-
}
79-
80-
Object body = this.getBody();
81-
InputStream inputStream = this.getInputStream();
82-
if (body != null) {
83-
inputStream = new ByteArrayInputStream(gson.toJson(body).getBytes(StandardCharsets.UTF_8));
84-
connection.setRequestProperty("Content-Type", "application/json");
85-
}
86-
87-
if (inputStream != null) {
88-
connection.setDoOutput(true);
89-
OutputStream out = connection.getOutputStream();
90-
byte[] buf = new byte[8192];
91-
int length;
92-
while ((length = inputStream.read(buf)) > 0) {
93-
out.write(buf, 0, length);
94-
}
95-
}
96-
stream = connection.getInputStream();
97-
}
98-
catch (IOException e) {
99-
if (connection == null || connection.getErrorStream() == null) {
100-
throw new APIException("Failed to request data from exaroton API", e);
101-
}
102-
103-
stream = connection.getErrorStream();
104-
}
105-
106-
return stream;
107-
}
108-
109-
/**
110-
* Execute this API Request and get the response as a String
111-
* @return response as a String
112-
* @throws APIException if the request fails
113-
*/
114-
public String requestString() throws APIException {
115-
try (InputStream stream = this.requestRaw()) {
116-
return new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))
117-
.lines()
118-
.collect(Collectors.joining("\n"));
119-
}
120-
catch (IOException e) {
121-
throw new APIException("Failed to read input stream", e);
122-
}
123-
}
124-
125-
/**
126-
* Execute this API Request and parse the API response
127-
* @return Parsed API response
128-
* @throws APIException if the request fails
129-
*/
130-
public APIResponse<Datatype> request() throws APIException {
131-
String json = this.requestString();
132-
APIResponse<Datatype> response = gson.fromJson(json, this.getType());
133-
if (!response.isSuccess()) throw new APIException(response.getError());
134-
135-
return response;
136-
}
137-
13865
/**
13966
* @return request headers
14067
*/
@@ -148,7 +75,7 @@ protected HashMap<String, String> getHeaders() {
14875
* get the type required for parsing the JSON response
14976
* @return response type
15077
*/
151-
protected abstract TypeToken<APIResponse<Datatype>> getType();
78+
protected abstract TypeToken<APIResponse<Response>> getType();
15279

15380
/**
15481
* data that will be replaced in the endpoint
@@ -159,17 +86,24 @@ protected HashMap<String, String> getData() {
15986
}
16087

16188
/**
162-
* @return input stream with data that should be sent to the request
89+
* Get the body publisher for the request
90+
* @param gson gson instance
91+
* @param builder request builder to set the Content-Type header
92+
* @param body request body
93+
* @return a body publisher
16394
*/
164-
protected InputStream getInputStream() {
165-
return null;
95+
protected HttpRequest.BodyPublisher jsonBodyPublisher(Gson gson, HttpRequest.Builder builder, Object body) {
96+
builder.header("Content-Type", "application/json");
97+
return HttpRequest.BodyPublishers.ofString(gson.toJson(body));
16698
}
16799

168100
/**
169-
* Get the request body
170-
* @return request body
101+
* Get the body publisher for the request
102+
* @param gson gson instance
103+
* @param builder request builder which can be used to set a Content-Type header
104+
* @return a body publisher
171105
*/
172-
protected Object getBody() {
173-
return null;
106+
protected HttpRequest.BodyPublisher getBodyPublisher(Gson gson, HttpRequest.Builder builder) {
107+
return HttpRequest.BodyPublishers.noBody();
174108
}
175109
}

src/main/java/com/exaroton/api/APIResponse.java

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
package com.exaroton.api;
22

3+
import com.google.gson.Gson;
4+
import com.google.gson.reflect.TypeToken;
5+
6+
import java.net.http.HttpResponse;
7+
import java.nio.ByteBuffer;
8+
import java.nio.charset.StandardCharsets;
9+
import java.util.List;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.concurrent.CompletionException;
12+
import java.util.concurrent.CompletionStage;
13+
import java.util.concurrent.Flow;
14+
315
public class APIResponse<Datatype> {
416

517
/**
@@ -17,19 +29,30 @@ public class APIResponse<Datatype> {
1729
*/
1830
private final Datatype data;
1931

32+
/**
33+
* Create a BodyHandler for APIResponse
34+
* @param gson gson instance
35+
* @param token type token of the response data
36+
* @return BodyHandler
37+
* @param <T> response data type
38+
*/
39+
public static <T> HttpResponse.BodyHandler<APIResponse<T>> bodyHandler(ExarotonClient client, Gson gson, TypeToken<APIResponse<T>> token) {
40+
return responseInfo -> new BodySubscriber<>(client, gson, token);
41+
}
42+
2043
/**
2144
* create an APIResponse
45+
*
2246
* @param success request success
23-
* @param error error message
24-
* @param data response data
47+
* @param error error message
48+
* @param data response data
2549
*/
2650
public APIResponse(boolean success, String error, Datatype data) {
2751
this.success = success;
2852
this.error = error;
2953
this.data = data;
3054
}
3155

32-
3356
/**
3457
* @return request success
3558
*/
@@ -50,4 +73,56 @@ public String getError() {
5073
public Datatype getData() {
5174
return data;
5275
}
76+
77+
78+
private static final class BodySubscriber<T> implements HttpResponse.BodySubscriber<APIResponse<T>> {
79+
private final ExarotonClient client;
80+
private final Gson gson;
81+
private final TypeToken<APIResponse<T>> token;
82+
private final HttpResponse.BodySubscriber<String> parent;
83+
84+
private BodySubscriber(ExarotonClient client, Gson gson, TypeToken<APIResponse<T>> token) {
85+
this.client = client;
86+
this.gson = gson;
87+
this.token = token;
88+
this.parent = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
89+
}
90+
91+
92+
@Override
93+
public CompletionStage<APIResponse<T>> getBody() {
94+
return parent.getBody().thenCompose(json -> {
95+
APIResponse<T> response = gson.fromJson(json, token);
96+
if (!response.isSuccess()) {
97+
return CompletableFuture.failedFuture(new APIException(response.getError()));
98+
}
99+
100+
if (response.getData() instanceof Initializable) {
101+
((Initializable) response.getData()).initialize(client, gson);
102+
}
103+
104+
return CompletableFuture.completedFuture(response);
105+
});
106+
}
107+
108+
@Override
109+
public void onSubscribe(Flow.Subscription subscription) {
110+
parent.onSubscribe(subscription);
111+
}
112+
113+
@Override
114+
public void onNext(List<ByteBuffer> item) {
115+
parent.onNext(item);
116+
}
117+
118+
@Override
119+
public void onError(Throwable throwable) {
120+
parent.onError(throwable);
121+
}
122+
123+
@Override
124+
public void onComplete() {
125+
parent.onComplete();
126+
}
127+
}
53128
}

0 commit comments

Comments
 (0)