Skip to content

Commit 820b899

Browse files
authored
abstract away everything related to http (#34)
* implement http abstraction * add RequestFactory * adapt MeiliSearchHttpRequest to use DefaultHttpClient * adapt MeiliSearchHttpRequest to use DefaultHttpClient * add unit test for ApacheHttpClient * move Constructor to AbstractHttpClient * make testCompile extend from compileOnly * fix FutureCallback * fix body / params handling and RequestFactory * improve stream handling * add HttpClient powered by OkHttp * rename OkHttpHttpClient to CustomOkHttpClient and add tests
1 parent ae6f5c4 commit 820b899

18 files changed

+891
-85
lines changed

build.gradle

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,29 @@ java {
2222
repositories {
2323
// Use jcenter for resolving dependencies.
2424
// You can declare any Maven/Ivy/file repository here.
25-
jcenter()
2625
mavenCentral() // Required for Lombok dependency
26+
jcenter()
27+
}
28+
29+
configurations {
30+
testCompile.extendsFrom compileOnly
2731
}
2832

2933
dependencies {
3034
// This dependency is used internally, and not exposed to consumers on their own compile classpath.
3135
implementation 'com.google.guava:guava:29.0-jre'
3236
implementation 'com.google.code.gson:gson:2.8.6'
3337
implementation 'org.json:json:20200518'
38+
// https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5
39+
compileOnly group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.0.3'
40+
compileOnly 'com.squareup.okhttp3:okhttp:4.9.0'
41+
3442
// Use JUnit test framework
3543
testImplementation(platform('org.junit:junit-bom:5.7.0'))
3644
testImplementation('org.junit.jupiter:junit-jupiter')
45+
// https://mvnrepository.com/artifact/org.mockito/mockito-core
46+
testImplementation 'org.mockito:mockito-core:3.5.13'
47+
testImplementation 'org.hamcrest:hamcrest:2.2'
3748

3849
// Lombok
3950
compileOnly 'org.projectlombok:lombok:1.18.12'

src/main/java/com/meilisearch/sdk/Config.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,12 @@ public Config(String hostUrl, String apiKey) {
2626
this.hostUrl = hostUrl;
2727
this.apiKey = apiKey;
2828
}
29+
30+
public String getHostUrl() {
31+
return hostUrl;
32+
}
33+
34+
public String getApiKey() {
35+
return apiKey;
36+
}
2937
}
Lines changed: 26 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,53 @@
11
package com.meilisearch.sdk;
22

3-
import java.io.BufferedReader;
4-
import java.io.IOException;
5-
import java.io.InputStreamReader;
6-
import java.net.HttpURLConnection;
7-
import java.net.URL;
8-
import java.nio.charset.StandardCharsets;
9-
import java.util.Optional;
3+
import com.meilisearch.sdk.http.AbstractHttpClient;
4+
import com.meilisearch.sdk.http.DefaultHttpClient;
5+
import com.meilisearch.sdk.http.factory.BasicRequestFactory;
6+
import com.meilisearch.sdk.http.factory.RequestFactory;
7+
import com.meilisearch.sdk.http.request.HttpMethod;
8+
import com.meilisearch.sdk.http.response.HttpResponse;
9+
10+
import java.util.Collections;
1011

1112
class MeiliSearchHttpRequest {
12-
private final Config config;
13+
private final AbstractHttpClient client;
14+
private final RequestFactory factory;
1315

1416
protected MeiliSearchHttpRequest(Config config) {
15-
this.config = config;
17+
this.client = new DefaultHttpClient(config);
18+
this.factory = new BasicRequestFactory();
1619
}
1720

18-
/**
19-
* Create and get a validated HTTP connection to url with method and API key
20-
*
21-
* @param url URL to connect to
22-
* @param method HTTP method to use for the connection
23-
* @param apiKey API Key to use for the connection
24-
* @return Validated connection (otherwise, will throw a {@link IOException})
25-
* @throws IOException If unable to establish connection
26-
*/
27-
private HttpURLConnection getConnection(final URL url, final String method, final String apiKey) throws IOException {
28-
if (url == null || "".equals(method)) throw new IOException("Unable to open an HttpURLConnection with no URL or method");
29-
30-
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
31-
connection.setRequestMethod(method);
32-
connection.setRequestProperty("Content-Type", "application/json");
33-
34-
// Use API key header only if one is provided
35-
if (!"".equals(apiKey)) {
36-
connection.setRequestProperty("X-Meili-API-Key", apiKey);
37-
}
38-
39-
// Ensure connection is set
40-
Optional<HttpURLConnection> connectionOptional = Optional.of(connection);
41-
return connectionOptional.orElseThrow(IOException::new);
42-
}
43-
44-
45-
private String parseConnectionResponse(HttpURLConnection connection) throws IOException {
46-
BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));
47-
StringBuilder sb = new StringBuilder();
48-
String responseLine;
49-
50-
while ((responseLine = br.readLine()) != null) {
51-
sb.append(responseLine);
52-
}
53-
54-
br.close();
55-
56-
return sb.toString();
21+
public MeiliSearchHttpRequest(AbstractHttpClient client, RequestFactory factory) {
22+
this.client = client;
23+
this.factory = factory;
5724
}
5825

5926

60-
String get(String api) throws Exception {
27+
public String get(String api) throws Exception {
6128
return this.get(api, "");
6229
}
6330

64-
6531
String get(String api, String param) throws Exception {
66-
StringBuilder urlBuilder = new StringBuilder(config.hostUrl + api);
67-
if (!param.equals("")) {
68-
urlBuilder.append(param);
69-
}
70-
71-
URL url = new URL(urlBuilder.toString());
72-
HttpURLConnection connection = this.getConnection(url, "GET", this.config.apiKey);
73-
74-
return this.parseConnectionResponse(connection);
32+
HttpResponse<?> httpResponse = this.client.get(factory.create(HttpMethod.GET, api + param, Collections.emptyMap(), null));
33+
return new String(httpResponse.getContentAsBytes());
7534
}
7635

7736

78-
String post(String api, String params) throws IOException {
79-
URL url = new URL(config.hostUrl + api);
80-
81-
HttpURLConnection connection = this.getConnection(url, "POST", config.apiKey);
82-
connection.setDoOutput(true);
83-
connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
84-
connection.setRequestProperty("Content-Length", String.valueOf(params.length()));
85-
connection.getOutputStream().write(params.getBytes(StandardCharsets.UTF_8));
86-
connection.connect();
87-
88-
return this.parseConnectionResponse(connection);
37+
String post(String api, String body) throws Exception {
38+
HttpResponse<?> httpResponse = this.client.post(factory.create(HttpMethod.POST, api, Collections.emptyMap(), body));
39+
return new String(httpResponse.getContentAsBytes());
8940
}
9041

9142

92-
String put(String api, String params) throws Exception {
93-
URL url = new URL(config.hostUrl + api);
94-
95-
HttpURLConnection connection = this.getConnection(url, "PUT", config.apiKey);
96-
connection.setDoOutput(true);
97-
connection.getOutputStream().write(params.getBytes());
98-
connection.connect();
99-
100-
return this.parseConnectionResponse(connection);
43+
String put(String api, String body) throws Exception {
44+
HttpResponse<?> httpResponse = this.client.put(factory.create(HttpMethod.PUT, api, Collections.emptyMap(), body));
45+
return new String(httpResponse.getContentAsBytes());
10146
}
10247

10348

10449
String delete(String api) throws Exception {
105-
URL url = new URL(config.hostUrl + api);
106-
107-
HttpURLConnection connection = this.getConnection(url, "DELETE", config.apiKey);
108-
connection.connect();
109-
return this.parseConnectionResponse(connection);
50+
HttpResponse<?> httpResponse = this.client.put(factory.create(HttpMethod.DELETE, api, Collections.emptyMap(), null));
51+
return new String(httpResponse.getContentAsBytes());
11052
}
11153
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.meilisearch.sdk.http;
2+
3+
import com.meilisearch.sdk.Config;
4+
import com.meilisearch.sdk.http.request.HttpRequest;
5+
import com.meilisearch.sdk.http.response.HttpResponse;
6+
7+
public abstract class AbstractHttpClient implements HttpClient<HttpRequest<?>, HttpResponse<?>> {
8+
protected final Config config;
9+
10+
public AbstractHttpClient(Config config) {
11+
this.config = config;
12+
}
13+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package com.meilisearch.sdk.http;
2+
3+
import com.meilisearch.sdk.Config;
4+
import com.meilisearch.sdk.http.request.HttpRequest;
5+
import com.meilisearch.sdk.http.response.BasicHttpResponse;
6+
import com.meilisearch.sdk.http.response.HttpResponse;
7+
import org.apache.hc.client5.http.async.HttpAsyncClient;
8+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
9+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
10+
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
11+
import org.apache.hc.client5.http.async.methods.SimpleResponseConsumer;
12+
import org.apache.hc.client5.http.impl.async.HttpAsyncClients;
13+
import org.apache.hc.client5.http.protocol.HttpClientContext;
14+
import org.apache.hc.core5.concurrent.FutureCallback;
15+
import org.apache.hc.core5.http.ContentType;
16+
import org.apache.hc.core5.http.NameValuePair;
17+
import org.apache.hc.core5.reactor.IOReactorConfig;
18+
import org.apache.hc.core5.util.Timeout;
19+
20+
import java.util.Arrays;
21+
import java.util.concurrent.CancellationException;
22+
import java.util.concurrent.CompletableFuture;
23+
import java.util.concurrent.ExecutionException;
24+
import java.util.stream.Collectors;
25+
26+
27+
public class ApacheHttpClient extends AbstractHttpClient {
28+
29+
private final HttpAsyncClient client;
30+
31+
public ApacheHttpClient(Config config) {
32+
super(config);
33+
final IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
34+
.setSoTimeout(Timeout.ofSeconds(5))
35+
.build();
36+
37+
this.client = HttpAsyncClients.custom()
38+
.setIOReactorConfig(ioReactorConfig)
39+
.build();
40+
}
41+
42+
public ApacheHttpClient(Config config, HttpAsyncClient client) {
43+
super(config);
44+
this.client = client;
45+
}
46+
47+
48+
private HttpResponse<?> execute(HttpRequest<?> request) throws Exception {
49+
CompletableFuture<SimpleHttpResponse> response = new CompletableFuture<>();
50+
client.execute(
51+
SimpleRequestProducer.create(mapRequest(request)),
52+
SimpleResponseConsumer.create(),
53+
null,
54+
HttpClientContext.create(),
55+
getCallback(response)
56+
);
57+
try {
58+
return response.thenApply(this::mapResponse).get();
59+
} catch (CancellationException | ExecutionException e) {
60+
// todo: throw dedicated exception
61+
throw new Exception(e);
62+
}
63+
}
64+
65+
@Override
66+
public HttpResponse<?> get(HttpRequest<?> request) throws Exception {
67+
return execute(request);
68+
}
69+
70+
@Override
71+
public HttpResponse<?> post(HttpRequest<?> request) throws Exception {
72+
return execute(request);
73+
}
74+
75+
@Override
76+
public HttpResponse<?> put(HttpRequest<?> request) throws Exception {
77+
return execute(request);
78+
}
79+
80+
@Override
81+
public HttpResponse<?> delete(HttpRequest<?> request) throws Exception {
82+
return execute(request);
83+
}
84+
85+
private SimpleHttpRequest mapRequest(HttpRequest<?> request) {
86+
SimpleHttpRequest httpRequest = new SimpleHttpRequest(request.getMethod().name(), request.getPath());
87+
if (request.hasContent())
88+
httpRequest.setBody(request.getContentAsBytes(), ContentType.APPLICATION_JSON);
89+
httpRequest.addHeader("X-Meili-API-Key", this.config.getApiKey());
90+
return httpRequest;
91+
}
92+
93+
private HttpResponse<?> mapResponse(SimpleHttpResponse response) {
94+
return new BasicHttpResponse(
95+
Arrays.stream(response.getHeaders()).collect(Collectors.toConcurrentMap(NameValuePair::getName, NameValuePair::getValue)),
96+
response.getCode(),
97+
response.getBodyText()
98+
);
99+
}
100+
101+
private FutureCallback<SimpleHttpResponse> getCallback(CompletableFuture<SimpleHttpResponse> completableFuture) {
102+
return new FutureCallback<SimpleHttpResponse>() {
103+
@Override
104+
public void completed(SimpleHttpResponse result) {
105+
completableFuture.complete(result);
106+
}
107+
108+
@Override
109+
public void failed(Exception ex) {
110+
completableFuture.completeExceptionally(ex);
111+
}
112+
113+
@Override
114+
public void cancelled() {
115+
completableFuture.cancel(false);
116+
}
117+
};
118+
}
119+
}

0 commit comments

Comments
 (0)