Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding websocket support #96

Merged
merged 3 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 53 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ A Java library for making http client requests easily.
- [Installation](#-installation)
- [Features](#-features)
- [CleverClient Builder](#cleverclient-builder)
- [Http Client](#http-client) **NEW**
- [Http Client Options](#http-client-options)
- [WebSocket Options](#websocket-options)
- [Interface Annotations](#interface-annotations)
- [Supported Response Types](#supported-response-types)
- [Interface Default Methods](#interface-default-methods)
- [Exception Handling](#exception-handling)
- [Examples](#-examples)
- [Contributing](#-contributing)
- [License](#-license)
- [Show Us Your Love](#-show-us-your-love)

## 💡 Description

Expand Down Expand Up @@ -117,17 +119,19 @@ Take in account that you need to use **Java 11 or greater**.

We have the following attributes to create a CleverClient object:

| Attribute | Description | Required |
| -------------------|------------------------------------------------------------|-----------|
| baseUrl | Api's url | mandatory |
| headers | Map of headers (name/value) | optional |
| header | Single header as a name and a value | optional |
| clientAdapter | Object of one of the specialized HttpClientAdapter classes | optional |
| requestInterceptor | Function to modify the request once is built | optional |
| bodyInspector | Function to inspect the `@Body` request parameter | optional |
| endsOfStream | List of texts used to mark the end of streams | optional |
| endOfStream | Text used to mark the end of streams | optional |
| objectMapper | Provides Json conversions either to/from objects | optional |
| Attribute | Description | Required |
| -------------------|--------------------------------------------------------------|-----------|
| baseUrl | Api's url | mandatory |
| headers | Map of headers (name/value) | optional |
| header | Single header as a name and a value | optional |
| bodyInspector | Function to inspect the `@Body` request parameter | optional |
| requestInterceptor | Function to modify the request once is built | optional |
| responseInterceptor| Function to modify the response after it's received | optional |
| clientAdapter | Http client implementation (Java HttpClient or OkHttp based) | optional |
| webSocketAdapter | WebSocket implementation (Java HttpClient or OkHttp based) | optional |
| endsOfStream | List of texts used to mark the end of streams | optional |
| endOfStream | Text used to mark the end of streams | optional |
| objectMapper | Provides Json conversions either to/from objects | optional |

```end(s)OfStream``` is required when you have endpoints sending back streams of data (Server Sent Events - SSE).

Expand All @@ -153,28 +157,36 @@ var objectMapper = new ObjectMapper()
var cleverClient = CleverClient.builder()
.baseUrl(BASE_URL)
.header(HEADER_NAME, HEADER_VALUE)
.clientAdapter(new JavaHttpClientAdapter(httpClient))
.requestInterceptor(request -> {
var url = request.getUrl();
url + (url.contains("?") ? "&" : "?") + "env=testing";
request.setUrl(url);
return request;
})
.bodyInspector(body -> {
var validator = new Validator();
var violations = validator.validate(body);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
})
.requestInterceptor(request -> {
var url = request.getUrl();
url + (url.contains("?") ? "&" : "?") + "env=testing";
request.setUrl(url);
return request;
})
.responseInterceptor(response -> {
var modifiedBody = customProcessing(response.getBody());
response.setBody(modifiedBody);
return response;
})
.clientAdapter(new JavaHttpClientAdapter(httpClient))
.webSocketAdapter(new JavaHttpWebSocketAdapter(httpClient))
.endOfStream(END_OF_STREAM)
.objectMapper(objectMapper)
.build();
```

### Http Client
### Http Client Options

With CleverClient you have two Http client alternatives: Java's HttpClient or OkHttp. The Builder attribute ```clientAdapter``` is used to indicate which to use. If you don't indicate any Http client, the Java's HttpClient will be used by default:
The Builder attribute ```clientAdapter``` determines which Http client implementation to use. CleverClient supports two implementations out of the box:
- Java's HttpClient (default) via ```JavaHttpClientAdapter```
- Square's OkHttp via ```OkHttpClientAdapter```

| clientAdapter's value | Description |
|-------------------------------------------------|-------------------------------------|
Expand All @@ -183,6 +195,19 @@ With CleverClient you have two Http client alternatives: Java's HttpClient or Ok
| new OkHttpClientAdapter() | Uses a default OkHttpClient |
| new OkHttpClientAdapter(customOkHttpClient) | Uses a custom OkHttpClient |

### WebSocket Options

The Builder attribute ```webSocketAdapter``` lets you specify which WebSocket implementation to use. Similar to ```clientAdapter```, you can choose between:
- Java's HttpClient (default) via ```JavaHttpWebSocketAdapter```
- Square's OkHttp via ```OkHttpWebSocketAdapter```

| webSocketAdapter's value | Description |
|----------------------------------------------------|-------------------------------------|
| new JavaHttpWebSocketAdapter() | Uses a default Java's HttpClient |
| new JavaHttpWebSocketAdapter(customJavaHttpClient) | Uses a custom Java's HttpClient |
| new OkHttpWebSocketAdapter() | Uses a default OkHttpClient |
| new OkHttpWebSocketAdapter(customOkHttpClient) | Uses a custom OkHttpClient |

### Interface Annotations

| Annotation | Target | Attributes | Required Attrs | Mult |
Expand Down Expand Up @@ -380,3 +405,10 @@ Please read our [Contributing](CONTRIBUTING.md) guide to learn and understand ho
CleverClient is licensed under the MIT License. See the
[LICENSE](https://github.com/sashirestela/cleverclient/blob/main/LICENSE) file
for more information.

## ❤ Show Us Your Love
Thanks for using **cleverclient**. If you find this project valuable there are a few ways you can show us your love, preferably all of them 🙂:

* Letting your friends know about this project 🗣📢.
* Writing a brief review on your social networks ✍🌐.
* Giving us a star on Github ⭐.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package io.github.sashirestela.cleverclient.example;

import io.github.sashirestela.cleverclient.websocket.JavaHttpWebSocketAdapter;
import io.github.sashirestela.cleverclient.websocket.WebSocketAdapter;

import java.util.Map;

public class WebSocketExample {

protected WebSocketAdapter webSocketAdapter;

public WebSocketExample() {
this.webSocketAdapter = new JavaHttpWebSocketAdapter();
}

public void run() {
final String BASE_URL = "wss://s13970.blr1.piesocket.com/v3/1?api_key=" + System.getenv("PIESOCKET_API_KEY")
+ "&notify_self=1";

webSocketAdapter.onOpen(() -> System.out.println("Connected"));
webSocketAdapter.onMessage(message -> System.out.println("Received: " + message));
webSocketAdapter.onClose((code, message) -> System.out.println("Closed"));

webSocketAdapter.connect(BASE_URL, Map.of()).join();
webSocketAdapter.send("Hello World!").join();
webSocketAdapter.send("Welcome to the Jungle!").join();
webSocketAdapter.close();
}

public static void main(String[] args) {
var example = new WebSocketExample();
example.run();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.github.sashirestela.cleverclient.example;

import io.github.sashirestela.cleverclient.websocket.OkHttpWebSocketAdapter;

public class WebSocketExampleOkHttp extends WebSocketExample {

public WebSocketExampleOkHttp() {
this.webSocketAdapter = new OkHttpWebSocketAdapter();
}

public static void main(String[] args) {
var example = new WebSocketExampleOkHttp();
example.run();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.github.sashirestela.cleverclient.http.HttpResponseData;
import io.github.sashirestela.cleverclient.support.Configurator;
import io.github.sashirestela.cleverclient.util.CommonUtil;
import io.github.sashirestela.cleverclient.websocket.WebSocketAdapter;
import lombok.Builder;
import lombok.Getter;
import lombok.NonNull;
Expand Down Expand Up @@ -37,6 +38,7 @@ public class CleverClient {
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final UnaryOperator<HttpResponseData> responseInterceptor;
private final HttpClientAdapter clientAdapter;
private final WebSocketAdapter webSockewAdapter;
private final HttpProcessor httpProcessor;

/**
Expand All @@ -49,6 +51,8 @@ public class CleverClient {
* @param responseInterceptor Function to modify the response once it has been received.
* @param clientAdapter Component to call http services. If none is passed the
* JavaHttpClientAdapter will be used. Optional.
* @param webSocketAdapter Component to do web socket interactions. If none is passed the
* JavaHttpWebSocketAdapter will be used. Optional.
* @param endsOfStream Texts used to mark the final of streams when handling server sent
* events (SSE). Optional.
* @param objectMapper Provides Json conversions either to and from objects. Optional.
Expand All @@ -57,8 +61,8 @@ public class CleverClient {
@SuppressWarnings("java:S107")
public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> headers, Consumer<Object> bodyInspector,
UnaryOperator<HttpRequestData> requestInterceptor, UnaryOperator<HttpResponseData> responseInterceptor,
HttpClientAdapter clientAdapter, @Singular("endOfStream") List<String> endsOfStream,
ObjectMapper objectMapper) {
HttpClientAdapter clientAdapter, WebSocketAdapter webSocketAdapter,
@Singular("endOfStream") List<String> endsOfStream, ObjectMapper objectMapper) {
this.baseUrl = baseUrl;
this.headers = Optional.ofNullable(headers).orElse(Map.of());
this.bodyInspector = bodyInspector;
Expand All @@ -67,6 +71,7 @@ public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> heade
this.clientAdapter = Optional.ofNullable(clientAdapter).orElse(new JavaHttpClientAdapter());
this.clientAdapter.setRequestInterceptor(this.requestInterceptor);
this.clientAdapter.setResponseInterceptor(this.responseInterceptor);
this.webSockewAdapter = webSocketAdapter;

this.httpProcessor = HttpProcessor.builder()
.baseUrl(this.baseUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.sashirestela.cleverclient.websocket;

@FunctionalInterface
public interface Action {

void execute();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.github.sashirestela.cleverclient.websocket;

import io.github.sashirestela.cleverclient.support.CleverClientException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.WebSocket;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;

public class JavaHttpWebSocketAdapter extends WebSocketAdapter {

private static final Logger logger = LoggerFactory.getLogger(JavaHttpWebSocketAdapter.class);
private HttpClient httpClient;
private WebSocket webSocket;
private final StringBuilder dataBuffer = new StringBuilder();
private CompletableFuture<Void> sendFuture;
private CompletableFuture<Void> closeFuture;

public JavaHttpWebSocketAdapter(HttpClient httpClient) {
this.httpClient = httpClient;
logger.debug("Created JavaHttpWebSocketAdapter");
}

public JavaHttpWebSocketAdapter() {
this(HttpClient.newHttpClient());
}

@Override
@SuppressWarnings("java:S3776")
public CompletableFuture<Void> connect(String url, Map<String, String> headers) {
logger.debug("Connecting to WebSocket URL: {}", url);
logger.debug("Connection headers: {}", headers);

WebSocket.Builder builder = this.httpClient.newWebSocketBuilder();
headers.forEach(builder::header);

CompletableFuture<Void> connectFuture = new CompletableFuture<>();

builder.buildAsync(URI.create(url), new WebSocket.Listener() {

@Override
public void onOpen(WebSocket webSocket) {
JavaHttpWebSocketAdapter.this.webSocket = webSocket;
logger.debug("WebSocket connection established");
if (openCallback != null) {
openCallback.execute();
}
connectFuture.complete(null);
webSocket.request(1);
}

@Override
public CompletionStage<?> onText(WebSocket webSocket, CharSequence data, boolean last) {
logger.trace("Received text data chunk, last={}", last);
dataBuffer.append(data);
if (last) {
if (messageCallback != null) {
String completeMessage = dataBuffer.toString();
logger.debug("Received message: {}", completeMessage);
messageCallback.accept(completeMessage);
}
dataBuffer.setLength(0);
if (sendFuture != null) {
sendFuture.complete(null);
sendFuture = null;
}
}
webSocket.request(1);
return CompletableFuture.completedFuture(null);
}

@Override
public CompletionStage<?> onClose(WebSocket webSocket, int statusCode, String reason) {
logger.debug("WebSocket closing with code: {}, reason: {}", statusCode, reason);
if (closeCallback != null) {
closeCallback.accept(statusCode, reason);
}
if (closeFuture != null) {
closeFuture.complete(null);
}
return CompletableFuture.completedFuture(null);
}

@Override
public void onError(WebSocket webSocket, Throwable error) {
logger.error("WebSocket error occurred", error);
if (errorCallback != null) {
errorCallback.accept(error);
}
if (sendFuture != null) {
sendFuture.completeExceptionally(error);
}
if (closeFuture != null) {
closeFuture.completeExceptionally(error);
}
connectFuture.completeExceptionally(error);
}

});

return connectFuture;
}

@Override
public CompletableFuture<Void> send(String message) {
if (webSocket == null) {
logger.error("Attempt to send message before WebSocket connection is established");
CompletableFuture<Void> future = new CompletableFuture<>();
future.completeExceptionally(new CleverClientException("WebSocket is not connected"));
return future;
}

logger.debug("Sending message: {}", message);
sendFuture = new CompletableFuture<>();
webSocket.sendText(message, true);
return sendFuture;
}

@Override
public void close() {
if (webSocket != null) {
logger.debug("Initiating WebSocket close");
closeFuture = new CompletableFuture<>();
webSocket.sendClose(WebSocket.NORMAL_CLOSURE, "Closing connection");
try {
closeFuture.join();
logger.debug("WebSocket close completed normally");
} catch (Exception e) {
logger.error("Error during WebSocket close", e);
if (errorCallback != null) {
errorCallback.accept(e);
}
}
}
}

}
Loading
Loading