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

Add response interceptor #95

Merged
merged 4 commits into from
Jan 18, 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package io.github.sashirestela.cleverclient.example;

import io.github.sashirestela.cleverclient.CleverClient;
import io.github.sashirestela.cleverclient.example.jsonplaceholder.UserService;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class ResponseInterceptorExample extends AbstractExample {

public ResponseInterceptorExample(String clientAlias) {
super(clientAlias);
}

public ResponseInterceptorExample() {
this("javahttp");
}

public void run() {
var cleverClient = CleverClient.builder()
.baseUrl("https://jsonplaceholder.typicode.com")
.responseInterceptor(response -> {
var body = response.getBody();
var newBody = transformUsers(body);
response.setBody(newBody);
return response;
})
.clientAdapter(clientAdapter)
.build();
var userService = cleverClient.create(UserService.class);

showTitle("Example Read Users");
var usersList = userService.readUsers(1, 5);
usersList.forEach(System.out::println);
}

private String transformUsers(String jsonInput) {
List<String> flatUsers = new ArrayList<>();

// Simpler pattern that matches each field individually
String patternStr = "\"id\":\\s*(\\d+).*?" + // id
"\"name\":\\s*\"([^\"]+)\".*?" + // name
"\"username\":\\s*\"([^\"]+)\".*?" + // username
"\"email\":\\s*\"([^\"]+)\".*?" + // email
"\"street\":\\s*\"([^\"]+)\".*?" + // street
"\"suite\":\\s*\"([^\"]+)\".*?" + // suite
"\"city\":\\s*\"([^\"]+)\".*?" + // city
"\"phone\":\\s*\"([^\"]+)\".*?" + // phone
"\"website\":\\s*\"([^\"]+)\".*?" + // website
"\"company\":\\s*\\{\\s*\"name\":\\s*\"([^\"]+)\""; // company name

Pattern pattern = Pattern.compile(patternStr, Pattern.DOTALL);
Matcher matcher = pattern.matcher(jsonInput);

while (matcher.find()) {
String flatUser = String.format(
"{\n" +
" \"id\": %s,\n" +
" \"name\": \"%s\",\n" +
" \"username\": \"%s\",\n" +
" \"email\": \"%s\",\n" +
" \"address\": \"%s, %s, %s\",\n" +
" \"phone\": \"%s\",\n" +
" \"website\": \"%s\",\n" +
" \"company\": \"%s\"\n" +
"}",
matcher.group(1), // id
matcher.group(2), // name
matcher.group(3), // username
matcher.group(4), // email
matcher.group(5), // street
matcher.group(6), // suite
matcher.group(7), // city
matcher.group(8), // phone
matcher.group(9), // website
matcher.group(10) // company name
);
flatUsers.add(flatUser);
}

// If no matches were found, print input for debugging
if (flatUsers.isEmpty()) {
System.err.println("No matches found in input: " + jsonInput);
return "[]";
}

// Combine all users into a JSON array
return "[\n " + String.join(",\n ", flatUsers) + "\n]";
}

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

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

public class ResponseInterceptorExampleOkHttp extends ResponseInterceptorExample {

public ResponseInterceptorExampleOkHttp() {
super("okhttp");
}

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

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

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class User {

private Integer id;
private String name;
private String username;
private String email;
private String address;
private String phone;
private String website;
private String company;

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

import io.github.sashirestela.cleverclient.annotation.GET;
import io.github.sashirestela.cleverclient.annotation.Query;
import io.github.sashirestela.cleverclient.annotation.Resource;

import java.util.List;

@Resource("/users")
public interface UserService {

@GET
List<User> readUsers(@Query("_page") Integer page, @Query("_limit") Integer limit);

}
44 changes: 26 additions & 18 deletions src/main/java/io/github/sashirestela/cleverclient/CleverClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import io.github.sashirestela.cleverclient.client.JavaHttpClientAdapter;
import io.github.sashirestela.cleverclient.http.HttpProcessor;
import io.github.sashirestela.cleverclient.http.HttpRequestData;
import io.github.sashirestela.cleverclient.http.HttpResponseData;
import io.github.sashirestela.cleverclient.support.Configurator;
import io.github.sashirestela.cleverclient.util.CommonUtil;
import lombok.Builder;
Expand Down Expand Up @@ -32,39 +33,46 @@ public class CleverClient {

private final String baseUrl;
private final Map<String, String> headers;
private final HttpClientAdapter clientAdapter;
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final Consumer<Object> bodyInspector;
private final UnaryOperator<HttpRequestData> requestInterceptor;
private final UnaryOperator<HttpResponseData> responseInterceptor;
private final HttpClientAdapter clientAdapter;
private final HttpProcessor httpProcessor;

/**
* Constructor to create an instance of CleverClient.
*
* @param baseUrl Root of the url of the API service to call. Mandatory.
* @param headers Http headers for all the API service. Optional.
* @param clientAdapter Component to call http services. If none is passed the
* JavaHttpClientAdapter will be used. Optional.
* @param requestInterceptor Function to modify the request once it has been built.
* @param bodyInspector Function to inspect the Body request parameter.
* @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.
* @param baseUrl Root of the url of the API service to call. Mandatory.
* @param headers Http headers for all the API service. Optional.
* @param bodyInspector Function to inspect the Body request parameter.
* @param requestInterceptor Function to modify the request once it has been built.
* @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 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.
*/
@Builder
public CleverClient(@NonNull String baseUrl, @Singular Map<String, String> headers, HttpClientAdapter clientAdapter,
UnaryOperator<HttpRequestData> requestInterceptor, Consumer<Object> bodyInspector,
@Singular("endOfStream") List<String> endsOfStream, ObjectMapper objectMapper) {
@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) {
this.baseUrl = baseUrl;
this.headers = Optional.ofNullable(headers).orElse(Map.of());
this.clientAdapter = Optional.ofNullable(clientAdapter).orElse(new JavaHttpClientAdapter());
this.requestInterceptor = requestInterceptor;
this.bodyInspector = bodyInspector;
this.requestInterceptor = requestInterceptor;
this.responseInterceptor = responseInterceptor;
this.clientAdapter = Optional.ofNullable(clientAdapter).orElse(new JavaHttpClientAdapter());
this.clientAdapter.setRequestInterceptor(this.requestInterceptor);
this.clientAdapter.setResponseInterceptor(this.responseInterceptor);

this.httpProcessor = HttpProcessor.builder()
.baseUrl(this.baseUrl)
.headers(CommonUtil.mapToListOfString(this.headers))
.clientAdapter(this.clientAdapter)
.requestInterceptor(this.requestInterceptor)
.bodyInspector(bodyInspector)
.bodyInspector(this.bodyInspector)
.build();
Configurator.builder()
.endsOfStream(Optional.ofNullable(endsOfStream).orElse(Arrays.asList()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import io.github.sashirestela.cleverclient.ResponseInfo;
import io.github.sashirestela.cleverclient.ResponseInfo.RequestInfo;
import io.github.sashirestela.cleverclient.http.HttpRequestData;
import io.github.sashirestela.cleverclient.http.HttpResponseData;
import io.github.sashirestela.cleverclient.support.CleverClientException;
import io.github.sashirestela.cleverclient.support.CleverClientSSE;
import io.github.sashirestela.cleverclient.support.ReturnType;
import io.github.sashirestela.cleverclient.util.CommonUtil;
import io.github.sashirestela.cleverclient.util.Constant;
import io.github.sashirestela.cleverclient.util.JsonUtil;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -22,15 +24,19 @@
import java.util.stream.Collectors;
import java.util.stream.Stream;

@Setter
public abstract class HttpClientAdapter {

private static Logger logger = LoggerFactory.getLogger(HttpClientAdapter.class);
protected static final String REQUEST_BODY_FORMAT = "Request Body : {}";
protected static final String RESPONSE_CODE_FORMAT = "Response Code : {}";
protected static final String RESPONSE_FORMAT = "Response : {}";

public Object sendRequest(RequestData originalRequest, UnaryOperator<HttpRequestData> requestInterceptor) {
var actualRequest = interceptRequest(originalRequest, requestInterceptor);
protected UnaryOperator<HttpRequestData> requestInterceptor;
protected UnaryOperator<HttpResponseData> responseInterceptor;

public Object sendRequest(RequestData originalRequest) {
var actualRequest = interceptRequest(originalRequest);
logger.debug("Http Call : {} {}", actualRequest.getHttpMethod(), actualRequest.getUrl());
var formattedHeaders = formattedHeaders(actualRequest.getHeaders());
logger.debug("Request Headers : {}", formattedHeaders);
Expand All @@ -47,11 +53,10 @@ public Object sendRequest(RequestData originalRequest, UnaryOperator<HttpRequest

public abstract void shutdown();

private RequestData interceptRequest(RequestData originalRequest,
UnaryOperator<HttpRequestData> requestInterceptor) {
private RequestData interceptRequest(RequestData originalRequest) {
if (requestInterceptor != null) {
var httpRequestData = originalRequest.getHttpRequestData();
httpRequestData = requestInterceptor.apply(httpRequestData);
httpRequestData = this.requestInterceptor.apply(httpRequestData);
return originalRequest
.withUrl(httpRequestData.getUrl())
.withBody(httpRequestData.getBody())
Expand All @@ -61,6 +66,16 @@ private RequestData interceptRequest(RequestData originalRequest,
}
}

protected ResponseData interceptResponse(ResponseData originalResponse) {
if (responseInterceptor != null && originalResponse.getBody() instanceof String) {
var httpResponseData = originalResponse.getHttpResponseData();
httpResponseData = this.responseInterceptor.apply(httpResponseData);
return originalResponse.withBody(httpResponseData.getBody());
} else {
return originalResponse;
}
}

@SuppressWarnings("unchecked")
protected void throwExceptionIfErrorIsPresent(ResponseData response) {
if (!CommonUtil.isInHundredsOf(response.getStatusCode(), Constant.HTTP_SUCCESSFUL)) {
Expand Down Expand Up @@ -113,22 +128,25 @@ private String formattedHeaders(List<String> headers) {
return print.toString();
}

protected Stream<Object> convertToStreamOfObjects(Stream<String> response, ReturnType returnType) {
@SuppressWarnings("unchecked")
protected Stream<Object> convertToStreamOfObjects(ResponseData responseData, ReturnType returnType) {
final var lineRecord = new CleverClientSSE.LineRecord();
return response
return ((Stream<String>) responseData.getBody())
.map(line -> {
logger.debug(RESPONSE_FORMAT, line);
lineRecord.updateWith(line);
return new CleverClientSSE(lineRecord);
})
.filter(CleverClientSSE::isActualData)
.map(item -> JsonUtil.jsonToObject(item.getActualData(), returnType.getBaseClass()));
.map(item -> JsonUtil.jsonToObject(interceptStreamItem(responseData, item.getActualData()),
returnType.getBaseClass()));
}

protected Stream<Object> convertToStreamOfEvents(Stream<String> response, ReturnType returnType) {
@SuppressWarnings("unchecked")
protected Stream<Object> convertToStreamOfEvents(ResponseData responseData, ReturnType returnType) {
final var lineRecord = new CleverClientSSE.LineRecord();
final var events = returnType.getClassByEvent().keySet();
return response
return ((Stream<String>) responseData.getBody())
.map(line -> {
logger.debug(RESPONSE_FORMAT, line);
lineRecord.updateWith(line);
Expand All @@ -137,9 +155,18 @@ protected Stream<Object> convertToStreamOfEvents(Stream<String> response, Return
.filter(CleverClientSSE::isActualData)
.map(item -> Event.builder()
.name(item.getMatchedEvent())
.data(JsonUtil.jsonToObject(item.getActualData(),
.data(JsonUtil.jsonToObject(interceptStreamItem(responseData, item.getActualData()),
returnType.getClassByEvent().get(item.getMatchedEvent())))
.build());
}

private String interceptStreamItem(ResponseData responseData, String text) {
if (this.responseInterceptor == null) {
return text;
}
var httpResponseData = responseData.getHttpResponseData(text);
httpResponseData = this.responseInterceptor.apply(httpResponseData);
return httpResponseData.getBody();
}

}
Loading
Loading