[FEAT] develop a new RESTApi interface to be easiest and flexible
This commit is contained in:
parent
e7e8c48c5c
commit
ecc8829e8c
@ -41,6 +41,10 @@ public class RESTApi {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public RESTApiRequest request(final String urlOffset) {
|
||||
return new RESTApiRequest(this.baseUrl + urlOffset, this.token);
|
||||
}
|
||||
|
||||
public <TYPE_RESPONSE> List<TYPE_RESPONSE> gets(final Class<TYPE_RESPONSE> clazz, final String urlOffset)
|
||||
throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
final HttpClient client = HttpClient.newHttpClient();
|
||||
|
460
src/org/kar/archidata/tools/RESTApiRequest.java
Normal file
460
src/org/kar/archidata/tools/RESTApiRequest.java
Normal file
@ -0,0 +1,460 @@
|
||||
package org.kar.archidata.tools;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpClient.Version;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpRequest.BodyPublishers;
|
||||
import java.net.http.HttpRequest.Builder;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.bson.types.ObjectId;
|
||||
import org.kar.archidata.exception.RESTErrorResponseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonParseException;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
|
||||
import jakarta.ws.rs.core.HttpHeaders;
|
||||
|
||||
public class RESTApiRequest {
|
||||
final static Logger LOGGER = LoggerFactory.getLogger(RESTApiRequest.class);
|
||||
private final String url;
|
||||
private final String token;
|
||||
private final ObjectMapper mapper;
|
||||
private String serializedBodyString = null;
|
||||
private byte[] serializedBodyByte = null;
|
||||
private String contentType = null;
|
||||
private final Map<String, String> headers = new HashMap<>();
|
||||
private final Map<String, String> queryParam = new HashMap<>();
|
||||
private String verb = "GET";
|
||||
|
||||
/**
|
||||
* Constructor to initialize the RESTApiRequest with base URL and authorization token.
|
||||
*
|
||||
* @param url The base URL of the API.
|
||||
* @param token The Bearer token for authentication.
|
||||
*/
|
||||
public RESTApiRequest(final String url, final String token) {
|
||||
this.url = url;
|
||||
this.token = token;
|
||||
this.mapper = ContextGenericTools.createObjectMapper();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP method (GET, POST, etc.).
|
||||
*
|
||||
* @param verb The HTTP verb to use.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest verb(final String verb) {
|
||||
this.verb = verb;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request body as a raw String.
|
||||
*
|
||||
* @param body The raw string body.
|
||||
* @param contentType The content type of the request body.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public <TYPE_BODY> RESTApiRequest bodyString(final String body, final String contentType) {
|
||||
this.serializedBodyByte = null;
|
||||
this.serializedBodyString = body;
|
||||
this.contentType = contentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a Map to a JSON string and sets it as the body.
|
||||
*
|
||||
* @param data A map representing the body content.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
* @throws JsonProcessingException If the map fails to serialize.
|
||||
*/
|
||||
public <TYPE_BODY> RESTApiRequest bodyMap(final Map<String, Object> data) throws JsonProcessingException {
|
||||
this.serializedBodyByte = null;
|
||||
this.serializedBodyString = this.mapper.writeValueAsString(data);
|
||||
this.contentType = "application/json";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the request body as a byte array.
|
||||
*
|
||||
* @param body The byte array representing the body.
|
||||
* @param contentType The content type of the body.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public <TYPE_BODY> RESTApiRequest bodyByte(final byte[] body, final String contentType) {
|
||||
this.serializedBodyByte = body;
|
||||
this.serializedBodyString = null;
|
||||
this.contentType = contentType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a POJO into JSON and sets it as the body.
|
||||
*
|
||||
* @param body A Java object to serialize.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
* @throws JsonProcessingException If serialization fails.
|
||||
*/
|
||||
public <TYPE_BODY> RESTApiRequest bodyJson(final TYPE_BODY body) throws JsonProcessingException {
|
||||
this.serializedBodyString = this.mapper.writeValueAsString(body);
|
||||
this.contentType = "application/json";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a multipart/form-data request body from a map of fields.
|
||||
* Handles both File and standard form fields.
|
||||
*
|
||||
* @param body A map of key-values where values can be File or String.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
* @throws IOException If reading files fails.
|
||||
*/
|
||||
public <TYPE_BODY> RESTApiRequest bodyMultipart(final Map<String, Object> body) throws IOException {
|
||||
this.serializedBodyString = null;
|
||||
LOGGER.trace("body (MULTIPART)");
|
||||
// Create multipart key element
|
||||
final String boundary = (new ObjectId()).toString();
|
||||
this.contentType = "multipart/form-data; boundary=" + boundary;
|
||||
// create the body;
|
||||
final List<byte[]> bodyParts = new ArrayList<>();
|
||||
|
||||
for (final Map.Entry<String, Object> entry : body.entrySet()) {
|
||||
final StringBuilder partHeader = new StringBuilder();
|
||||
partHeader.append("--").append(boundary).append("\r\n");
|
||||
if (entry.getValue() instanceof File) {
|
||||
final File file = (File) entry.getValue();
|
||||
partHeader.append("Content-Disposition: form-data; name=\"").append(entry.getKey())
|
||||
.append("\"; filename=\"").append(file.getName()).append("\"\r\n");
|
||||
partHeader.append("Content-Type: application/octet-stream\r\n\r\n");
|
||||
bodyParts.add(partHeader.toString().getBytes());
|
||||
bodyParts.add(Files.readAllBytes(file.toPath()));
|
||||
bodyParts.add("\r\n".getBytes());
|
||||
} else {
|
||||
partHeader.append("Content-Disposition: form-data; name=\"").append(entry.getKey())
|
||||
.append("\"\r\n\r\n");
|
||||
if (entry.getValue() == null) {
|
||||
partHeader.append("null\r\n");
|
||||
} else {
|
||||
partHeader.append(entry.getValue().toString()).append("\r\n");
|
||||
}
|
||||
bodyParts.add(partHeader.toString().getBytes());
|
||||
}
|
||||
}
|
||||
bodyParts.add(("--" + boundary + "--\r\n").getBytes());
|
||||
|
||||
final int totalSize = bodyParts.stream().mapToInt(b -> b.length).sum();
|
||||
this.serializedBodyByte = new byte[totalSize];
|
||||
int position = 0;
|
||||
for (final byte[] part : bodyParts) {
|
||||
System.arraycopy(part, 0, this.serializedBodyByte, position, part.length);
|
||||
position += part.length;
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a query parameter to the request URL.
|
||||
*
|
||||
* @param paramKey The parameter name.
|
||||
* @param body The parameter value.
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public <TYPE_PARAM> RESTApiRequest queryParam(final String paramKey, final TYPE_PARAM body) {
|
||||
this.queryParam.put(paramKey, body.toString());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a GET request and parses the response as a List of objects.
|
||||
*
|
||||
* @param clazz The class of the expected response elements.
|
||||
* @return A list of parsed objects.
|
||||
* @throws RESTErrorResponseException If an error response is received.
|
||||
* @throws IOException If the request fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
public <TYPE_RESPONSE> List<TYPE_RESPONSE> gets(final Class<TYPE_RESPONSE> clazz)
|
||||
throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
verb("GET");
|
||||
final HttpRequest request = request();
|
||||
return callAndParseRequestList(clazz, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to GET.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest get() {
|
||||
verb("GET");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to PUT.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest put() {
|
||||
verb("PUT");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to PATCH.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest patch() {
|
||||
verb("PATCH");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to DELETE.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest delete() {
|
||||
verb("DELETE");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to ARCHIVE.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest archive() {
|
||||
verb("ARCHIVE");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the HTTP verb to RESTORE.
|
||||
*
|
||||
* @return The updated RESTApiRequest instance.
|
||||
*/
|
||||
public RESTApiRequest restore() {
|
||||
verb("RESTORE");
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request and parses the response into a List of objects.
|
||||
*
|
||||
* @param clazz The class of each element in the expected response.
|
||||
* @return List of parsed response objects.
|
||||
* @throws RESTErrorResponseException If an error response is received.
|
||||
* @throws IOException If the request fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
public <TYPE_RESPONSE> List<TYPE_RESPONSE> requestList(final Class<TYPE_RESPONSE> clazz)
|
||||
throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
final HttpRequest request = request();
|
||||
return callAndParseRequestList(clazz, request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request and parses the response into a single object.
|
||||
*
|
||||
* @param clazz The class of the expected response.
|
||||
* @return Parsed response object.
|
||||
* @throws RESTErrorResponseException If an error response is received.
|
||||
* @throws IOException If the request fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
public <TYPE_RESPONSE> TYPE_RESPONSE request(final Class<TYPE_RESPONSE> clazz)
|
||||
throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
final HttpRequest request = request();
|
||||
return callAndParseRequest(clazz, request);
|
||||
}
|
||||
|
||||
public static String buildQueryParams(final Map<String, String> params) {
|
||||
return params.entrySet().stream().map(entry -> URLEncoder.encode(entry.getKey(), StandardCharsets.UTF_8) + "="
|
||||
+ URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).collect(Collectors.joining("&"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the final HttpRequest based on method, headers, and body.
|
||||
*
|
||||
* @return A built HttpRequest instance.
|
||||
* @throws RESTErrorResponseException If error happens during body serialization.
|
||||
* @throws IOException If body serialization fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
public HttpRequest request() throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
Builder requestBuilding = null;
|
||||
final String queryParams = buildQueryParams(this.queryParam);
|
||||
if (queryParams != null && !queryParams.isEmpty()) {
|
||||
requestBuilding = createRequestBuilder(this.url);
|
||||
} else {
|
||||
requestBuilding = createRequestBuilder(this.url + "?" + queryParams);
|
||||
}
|
||||
if (this.contentType != null) {
|
||||
requestBuilding.header("Content-Type", this.contentType);
|
||||
}
|
||||
for (final Map.Entry<String, String> entry : this.headers.entrySet()) {
|
||||
requestBuilding.header(entry.getKey(), entry.getValue());
|
||||
}
|
||||
if (this.serializedBodyString != null) {
|
||||
LOGGER.trace("publish body: {}", this.serializedBodyString);
|
||||
return requestBuilding.method(this.verb, BodyPublishers.ofString(this.serializedBodyString)).build();
|
||||
}
|
||||
if (this.serializedBodyByte != null) {
|
||||
LOGGER.trace("publish body: {}", this.serializedBodyString);
|
||||
return requestBuilding.method(this.verb, BodyPublishers.ofByteArray(this.serializedBodyByte)).build();
|
||||
}
|
||||
return requestBuilding.method(this.verb, BodyPublishers.ofString("")).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a raw GET request and returns the raw HttpResponse as bytes.
|
||||
*
|
||||
* @param urlOffset The URL path relative to the base URL.
|
||||
* @return Raw HTTP response in byte array.
|
||||
* @throws IOException If the request fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
public HttpResponse<byte[]> getRaw(final String urlOffset) throws IOException, InterruptedException {
|
||||
final Builder requestBuilding = createRequestBuilder(urlOffset);
|
||||
final HttpRequest request = requestBuilding.method("GET", BodyPublishers.ofString("")).build();
|
||||
final HttpClient client = HttpClient.newHttpClient();
|
||||
// client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
|
||||
return client.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and initializes a HttpRequest.Builder with authorization and URL.
|
||||
*
|
||||
* @param urlOffset The URL path relative to the base URL.
|
||||
* @return Initialized HttpRequest.Builder.
|
||||
*/
|
||||
public Builder createRequestBuilder(final String urlOffset) {
|
||||
Builder requestBuilding = HttpRequest.newBuilder().version(Version.HTTP_1_1)
|
||||
.uri(URI.create(this.url + urlOffset));
|
||||
if (this.token != null) {
|
||||
requestBuilding = requestBuilding.header(HttpHeaders.AUTHORIZATION, "Bearer " + this.token);
|
||||
}
|
||||
return requestBuilding;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request and parses the response as a single object.
|
||||
*
|
||||
* @param clazzReturn The expected class of the response.
|
||||
* @param request The built HttpRequest to send.
|
||||
* @return Parsed object of the response.
|
||||
* @throws RESTErrorResponseException If an error response is received.
|
||||
* @throws IOException If the response cannot be parsed or network fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TYPE_RESPONSE> TYPE_RESPONSE callAndParseRequest(
|
||||
final Class<TYPE_RESPONSE> clazzReturn,
|
||||
final HttpRequest request) throws RESTErrorResponseException, IOException, InterruptedException {
|
||||
final HttpClient client = HttpClient.newHttpClient();
|
||||
// client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
|
||||
final HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (clazzReturn == HttpResponse.class) {
|
||||
return (TYPE_RESPONSE) httpResponse;
|
||||
}
|
||||
if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {
|
||||
LOGGER.trace("Receive Error: {}", httpResponse.body());
|
||||
try {
|
||||
final RESTErrorResponseException out = this.mapper.readValue(httpResponse.body(),
|
||||
RESTErrorResponseException.class);
|
||||
throw out;
|
||||
} catch (final InvalidDefinitionException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
} catch (final MismatchedInputException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
} catch (final JsonParseException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
}
|
||||
}
|
||||
if (clazzReturn == Void.class || clazzReturn == void.class) {
|
||||
return null;
|
||||
}
|
||||
if (clazzReturn.equals(String.class)) {
|
||||
return (TYPE_RESPONSE) httpResponse.body();
|
||||
}
|
||||
LOGGER.trace("Receive model: {} with data: '{}'", clazzReturn.getCanonicalName(), httpResponse.body());
|
||||
return this.mapper.readValue(httpResponse.body(), clazzReturn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the request and parses the response as a list of objects.
|
||||
*
|
||||
* @param clazzReturn The class of the expected response elements.
|
||||
* @param request The HttpRequest to send.
|
||||
* @return List of parsed response objects.
|
||||
* @throws RESTErrorResponseException If an error response is received.
|
||||
* @throws IOException If the response cannot be parsed or network fails.
|
||||
* @throws InterruptedException If the request is interrupted.
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <TYPE_RESPONSE> List<TYPE_RESPONSE> callAndParseRequestList(
|
||||
final Class<TYPE_RESPONSE> clazzReturn,
|
||||
final HttpRequest request) throws IOException, InterruptedException, RESTErrorResponseException {
|
||||
final HttpClient client = HttpClient.newHttpClient();
|
||||
// client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
|
||||
final HttpResponse<String> httpResponse = client.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
if (httpResponse.statusCode() < 200 || httpResponse.statusCode() >= 300) {
|
||||
LOGGER.trace("Receive Error: {}", httpResponse.body());
|
||||
try {
|
||||
final RESTErrorResponseException out = this.mapper.readValue(httpResponse.body(),
|
||||
RESTErrorResponseException.class);
|
||||
throw out;
|
||||
} catch (final InvalidDefinitionException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
} catch (final MismatchedInputException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
} catch (final JsonParseException ex) {
|
||||
ex.printStackTrace();
|
||||
LOGGER.error("body: {}", httpResponse.body());
|
||||
throw new IOException("RestAPI Fail to parse the error " + ex.getClass().getName() + " ["
|
||||
+ httpResponse.statusCode() + "] " + httpResponse.body());
|
||||
}
|
||||
}
|
||||
LOGGER.trace("Receive model: List<{}> with data: '{}'", clazzReturn.getCanonicalName(), httpResponse.body());
|
||||
return this.mapper.readValue(httpResponse.body(),
|
||||
this.mapper.getTypeFactory().constructCollectionType(List.class, clazzReturn));
|
||||
}
|
||||
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user