RestClient.java
/*
* The coLAB project
* Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
*
* Licensed under the MIT License
*/
package ch.colabproject.colab.generator.plugin.rest;
import ch.colabproject.colab.generator.model.exceptions.HttpException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbException;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.client.ClientResponseContext;
import javax.ws.rs.client.ClientResponseFilter;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import org.glassfish.jersey.media.multipart.FormDataMultiPart;
import org.glassfish.jersey.media.multipart.MultiPartFeature;
import org.glassfish.jersey.media.multipart.file.FileDataBodyPart;
/**
* JakartaEE-based rest client.
*
* @author maxence
*/
public class RestClient {
/**
* Base URL.
*/
private WebTarget webTarget;
/**
* internal HTTP client.
*/
private Client client;
/**
* Jsonb mapper
*/
private final Jsonb jsonb;
/**
* Create a REST client.
*
* @param baseUri base URL
* @param cookieName session cookie name
* @param jsonb jsonb mapper to use, if null a default mapper will be used
* @param clientFeatures REST client feature
*/
public RestClient(String baseUri, String cookieName, Jsonb jsonb, Object... clientFeatures) {
ClientBuilder builder = ClientBuilder.newBuilder();
builder.register(MultiPartFeature.class);
for (Object feature : clientFeatures) {
builder.register(feature);
}
if (cookieName != null && !cookieName.isBlank()) {
builder.register(new CookieFilter(cookieName));
}
this.client = builder.build();
this.webTarget = client.target(baseUri);
if (jsonb != null) {
this.jsonb = jsonb;
} else {
this.jsonb = JsonbBuilder.create();
}
}
/**
* Close the client.
*/
public void close() {
client.close();
}
/**
* Try to read entity
*
* @param <T> entity type
* @param stream input stream to read from
* @param type entity type
*
* @return instance of T or null
*/
private <T> T readEntity(InputStream stream, GenericType<T> type) {
try {
if (stream != null && stream.available() > 0) {
return jsonb.fromJson(stream, type.getType());
} else {
return null;
}
} catch (JsonbException | IOException ex) {
// silent ex
return null;
}
}
/**
* Convert InputStream to string
*
* @param stream the stream to convert
*
* @return the content of the stream as string
*/
private String readTextualEntity(InputStream stream) {
try {
return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
} catch (IOException ex) {
// silent ex
return null;
}
}
private <T> T processResponse(Response response, GenericType<T> type) {
Status.Family family = Status.Family.familyOf(response.getStatus());
if (family == Status.Family.SUCCESSFUL) {
if (response.getStatus() == 204) {
return null;
} else {
if (Response.class.isAssignableFrom(type.getRawType())) {
return (T) response;
}
Object entity = response.getEntity();
if (entity instanceof InputStream) {
InputStream stream = (InputStream) entity;
String contentType = response.getHeaderString("Content-Type");
if (contentType.startsWith("application/json")
|| contentType.startsWith("text/json")) {
return readEntity(stream, type);
} else if (contentType.startsWith("text/plain")
|| contentType.startsWith("text/html")) {
if (type.getRawType() == String.class) {
return (T) readTextualEntity(stream);
} else {
return null;
}
} else {
return null;
}
} else {
return null;
}
}
} else if (family == Status.Family.CLIENT_ERROR) {
HttpException error = readEntity((InputStream) response.getEntity(),
new GenericType<>(HttpException.class));
if (error != null) {
throw error;
} else {
throw new ClientException(Status.fromStatusCode(response.getStatus()));
}
} else if (family == Status.Family.SERVER_ERROR) {
throw new ServerException(Status.fromStatusCode(response.getStatus()));
} else {
throw new ServerException(Status.fromStatusCode(response.getStatus()));
}
}
/**
* Create FormData to send
*
* @param fields form content
*
* @return the formdata to send
*/
private FormDataMultiPart getFormData(Map<String, FormField> fields) {
FormDataMultiPart multipart = new FormDataMultiPart();
fields.forEach((fieldName, data) -> {
MediaType mimeType = data.getMimeType();
if (data.getData() instanceof File) {
FileDataBodyPart filePart;
if (mimeType != null) {
filePart = new FileDataBodyPart(fieldName, (File) data.getData(), mimeType);
} else {
filePart = new FileDataBodyPart(fieldName, (File) data.getData());
}
multipart.bodyPart(filePart);
} else {
if (mimeType != null) {
multipart.field(fieldName, data.getData(), mimeType);
} else {
multipart.field(fieldName, data.getData().toString());
}
}
});
return multipart;
}
/**
* send GET request.
*
* @param <T> expected return type
* @param path rest path
* @param type expected return generic type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T get(String path, GenericType<T> type, String... accept) {
return processResponse(webTarget.path(path).request()
.accept(accept).get(),
type);
}
/**
* send DELETE request.
*
* @param <T> expected return type
* @param path rest path
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T delete(String path, GenericType<T> type, String... accept) {
return processResponse(webTarget.path(path).request()
.accept(accept).delete(),
type);
}
/**
* send JSON POST request.
*
* @param <T> expected return type
* @param path rest path
* @param body POST body
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T post(String path, Object body, GenericType<T> type, String... accept) {
return processResponse(webTarget.path(path).request()
.accept(accept)
.post(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE)),
type);
}
/**
* send multipart form POST request.
*
* @param <T> expected return type
* @param path rest path
* @param fields form data
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T post(
String path,
Map<String, FormField> fields,
GenericType<T> type,
String... accept
) {
FormDataMultiPart multipart = getFormData(fields);
return processResponse(webTarget.path(path).request()
.accept(accept)
.post(Entity.entity(multipart, multipart.getMediaType())),
type);
}
/**
* send POST request with an empty body.
*
* @param <T> expected return type
* @param path rest path
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T post(String path, GenericType<T> type, String... accept) {
return this.post(path, "", type, accept);
}
/**
* send PUT request with JSON body.
*
* @param <T> expected return type
* @param path rest path
* @param body POST body
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T put(String path, Object body, GenericType<T> type, String... accept) {
return processResponse(webTarget.path(path).request()
.accept(accept)
.put(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE)),
type);
}
/**
* send multipart form PUT request.
*
* @param <T> expected return type
* @param path rest path
* @param fields form data
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T put(String path, Map<String, FormField> fields, GenericType<T> type,
String... accept) {
FormDataMultiPart multipart = getFormData(fields);
return processResponse(webTarget.path(path).request()
.accept(accept)
.put(Entity.entity(multipart, multipart.getMediaType())),
type);
}
/**
* send PUT request with an empty body.
*
* @param <T> expected return type
* @param path rest path
* @param type expected return type
* @param accept list of accepted MIME types
*
* @return instance of T
*/
public <T> T put(String path, GenericType<T> type, String... accept) {
return this.put(path, "", type, accept);
}
/**
* Cookie filter make sure the session cookie is set. The sessionId is set by server response.
* After the first request, the previously saved sessionId is injected in all requests.
*/
public static class CookieFilter implements ClientRequestFilter, ClientResponseFilter {
/**
* sessionId
*/
private String sessionId;
/**
* Name of the cookie
*/
private final String cookieName;
/**
* New filter
*
* @param cookieName name of the cookie to manage
*/
public CookieFilter(String cookieName) {
this.cookieName = cookieName;
this.sessionId = null;
}
/**
* If set, add Cookie header to the request.
* <p>
* {@inheritDoc }
*/
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
if (this.sessionId != null) {
List<Object> cookies = new ArrayList<>();
Cookie cookie = new Cookie(cookieName, sessionId);
cookies.add(cookie);
requestContext.getHeaders().put("Cookie", cookies);
}
}
/**
* On request response, fetch set-cookie value
* <p>
* {@inheritDoc }
*/
@Override
public void filter(ClientRequestContext requestContext,
ClientResponseContext responseContext) throws IOException {
NewCookie get = responseContext.getCookies().get(cookieName);
if (get != null) {
if (get.getMaxAge() == 0) {
this.sessionId = null;
} else {
this.sessionId = get.getValue();
}
}
}
}
}