org.appng.api.rest.RestClient Maven / Gradle / Ivy
/*
* Copyright 2011-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.appng.api.rest;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.appng.api.rest.model.Action;
import org.appng.api.rest.model.Datasource;
import org.appng.api.rest.model.ErrorModel;
import org.appng.api.rest.model.Link;
import org.appng.api.rest.model.Parameter;
import org.appng.api.rest.model.Sort.OrderEnum;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.ResourceHttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.DefaultResponseErrorHandler;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* A simple client for the appNG REST API, open for extension. Note that this class is NOT thread-safe!
*/
@Slf4j
public class RestClient {
private static final String PATH_SEPARATOR = "/";
protected RestTemplate restTemplate;
protected Map cookies;
protected String url;
private ObjectMapper objectMapper;
/**
* Creates a new {@link RestClient}.
*
* Note that you probably need to perform a login action before you can use this client.
*
*
* @param url
* the URL pointing to a site's service URL ({@code /service/})
*/
public RestClient(String url) {
this(url, new HashMap<>());
}
/**
* Creates a new {@link RestClient}, using an existing cookie. This cookie should be retrieved from another client
* that performed a login action.
*
* @param url
* the URL pointing to a site's service URL ({@code /service/})
* @param cookies
* the cookie to use
*
* @see RestClient#getCookies()
*/
public RestClient(String url, Map cookies) {
this.url = url;
this.cookies = cookies;
this.restTemplate = new RestTemplate(
Arrays.asList(new ByteArrayHttpMessageConverter(), new StringHttpMessageConverter(),
new MappingJackson2HttpMessageConverter(), new ResourceHttpMessageConverter()));
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
@Override
protected boolean hasError(HttpStatus statusCode) {
return statusCode.series() == HttpStatus.Series.SERVER_ERROR;
}
});
this.objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(Include.NON_ABSENT);
}
/**
* Retrieves the {@link Datasource}.
*
* @param application
* the name of the application
* @param id
* the ID of the {@link Datasource}
*
* @return the {@link Datasource} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity datasource(String application, String id) throws URISyntaxException {
return datasource(application, id, (Pageable) null);
}
/**
* Retrieves the {@link Datasource}.
*
* @param application
* the name of the application
* @param id
* the ID of the {@link Datasource}
* @param pageable
* a {@link Pageable} (optional)
*
* @return the {@link Datasource} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity datasource(String application, String id, Pageable pageable)
throws URISyntaxException {
return datasource(application, id, pageable, null);
}
/**
* Retrieves the {@link Datasource}.
*
* @param application
* the name of the application
* @param id
* the ID of the {@link Datasource}
* @param parameters
* some additional parameters
*
* @return the {@link Datasource} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity datasource(String application, String id,
MultiValueMap parameters) throws URISyntaxException {
return datasource(application, id, null, parameters);
}
/**
* Retrieves the {@link Datasource}.
*
* @param application
* the name of the application
* @param id
* the ID of the {@link Datasource}
* @param pageable
* a {@link Pageable} (optional)
* @param parameters
* some additional parameters
*
* @return the {@link Datasource} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity datasource(String application, String id, Pageable pageable,
MultiValueMap parameters) throws URISyntaxException {
StringBuilder uriBuilder = new StringBuilder(url).append(PATH_SEPARATOR).append(application);
uriBuilder.append("/rest/datasource/").append(id).append("?");
if (null != pageable) {
uriBuilder.append("sort").append(StringUtils.capitalize(id)).append("=").append(pageable.getSortQuery())
.append("&");
}
if (null != parameters) {
parameters.keySet().forEach(key -> {
parameters.get(key).forEach(value -> {
uriBuilder.append(key).append("=").append(value).append("&");
});
});
}
return exchange(new URI(uriBuilder.toString()), null, HttpMethod.GET, Datasource.class);
}
/**
* Retrieves the {@link Action}.
*
* @param application
* the name of the application
* @param eventId
* the event-ID of the {@link Action}
* @param actionId
* the ID of the {@link Action}
* @param pathVariables
* some additional path variables
*
* @return the (unprocessed) {@link Action} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity getAction(String application, String eventId, String actionId,
String... pathVariables) throws URISyntaxException {
URI actionURL = getActionURL(application, eventId, actionId, pathVariables);
return exchange(actionURL, null, HttpMethod.GET, Action.class);
}
private void doLog(String prefix, Object body, HttpStatus httpStatus) {
if (LOGGER.isDebugEnabled()) {
String content = StringUtils.EMPTY;
if (null != body) {
Class> bodyType = body.getClass();
if (!(bodyType.isPrimitive() || bodyType.isArray())
&& bodyType.getPackage().getName().startsWith("org.appng.api.rest.model")) {
try {
content = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(body);
} catch (JsonProcessingException e) {
LOGGER.error("error parsing JSON body", e);
}
} else {
content = body.toString();
}
}
Object status = null != httpStatus ? " " + httpStatus.value() : "";
LOGGER.debug("{}: {} {}", prefix, status, content);
}
}
/**
* Retrieves the {@link Action} represented by the {@link Link}
*
* @param link
* the {@link Link} representing the {@link Action}'s URI
*
* @return the (unprocessed) {@link Action} wrapped in a {@link RestResponseEntity}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity getAction(Link link) throws URISyntaxException {
String[] pathSegments = link.getTarget().split(PATH_SEPARATOR);
String servicePath = StringUtils.join(Arrays.copyOfRange(pathSegments, 3, pathSegments.length), PATH_SEPARATOR);
URI uri = new URI(url + PATH_SEPARATOR + servicePath);
return exchange(uri, null, HttpMethod.GET, Action.class);
}
/**
* Performs an {@link Action}, the URI is defined by the {@link Link}.
*
* @param data
* the {@link Action}-data to send
* @param link
* the {@link Link}
*
* @return a {@link RestResponseEntity} wrapping the resulting {@link Action}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity performAction(Action data, Link link) throws URISyntaxException {
String[] pathSegments = link.getTarget().split(PATH_SEPARATOR);
URI uri = new URI(url + PATH_SEPARATOR
+ StringUtils.join(Arrays.copyOfRange(pathSegments, 3, pathSegments.length), PATH_SEPARATOR));
addFormAction(data);
return exchange(uri, data, HttpMethod.POST, Action.class);
}
private void addFormAction(Action data) {
Parameter formAction = new Parameter();
formAction.setName("form_action");
formAction.setValue(data.getId());
data.addParametersItem(formAction);
}
protected void setCookies(ResponseEntity> entity) {
List setCookies = entity.getHeaders().get(HttpHeaders.SET_COOKIE);
if (null != setCookies) {
for (String c : setCookies) {
int valueStart = c.indexOf('=');
String name = c.substring(0, valueStart);
int end = c.indexOf(';');
String value = c.substring(valueStart + 1, end < 0 ? c.length() : end);
cookies.put(name, value);
LOGGER.debug("received cookie: {}={}", name, value);
}
}
}
protected URI getActionURL(String application, String eventId, String actionId, String[] pathVariables)
throws URISyntaxException {
String uriString = String.format("%s/%s/rest/action/%s/%s/%s", url, application, eventId, actionId,
StringUtils.join(pathVariables, PATH_SEPARATOR));
return new URI(uriString);
}
/**
* Performs an {@link Action}.
*
* @param application
* the name of the application
* @param data
* the {@link Action}-data to send
* @param pathVariables
* some additional path variables
*
* @return a {@link RestResponseEntity} wrapping the resulting {@link Action}
*
* @throws URISyntaxException
* if something is wrong with the URI
*/
public RestResponseEntity performAction(String application, Action data, String... pathVariables)
throws URISyntaxException {
addFormAction(data);
URI actionURL = getActionURL(application, data.getEventId(), data.getId(), pathVariables);
return exchange(actionURL, data, HttpMethod.POST, Action.class);
}
protected HttpHeaders getHeaders(boolean acceptAnyType) {
HttpHeaders headers = new HttpHeaders();
if (!cookies.isEmpty()) {
cookies.keySet().forEach(k -> {
String cookie = cookies.get(k);
headers.add(HttpHeaders.COOKIE, k + "=" + cookie);
LOGGER.debug("sent cookie: {}={}", k, cookies.get(k));
});
}
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
List acceptableMediaTypes;
if (acceptAnyType) {
acceptableMediaTypes = Arrays.asList(MediaType.ALL);
} else {
acceptableMediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_UTF8);
}
headers.setAccept(acceptableMediaTypes);
headers.set(HttpHeaders.USER_AGENT, "appNG Rest Client");
return headers;
}
/**
* Returns the current cookies or this client
*
* @return the cookie's map
*/
public Map getCookies() {
return cookies;
}
/**
* Returns the resource represented by the link as binary data
*
* @param link
* the ink to there resource
*
* @return the resource
*
* @throws URISyntaxException
* if something is wrong with the link
*/
public RestResponseEntity getBinaryData(Link link) throws URISyntaxException {
String path = getRelativePathFromLink(link);
return getResource(path, byte[].class);
}
private String getRelativePathFromLink(Link link) {
String[] pathSegments = link.getTarget().split(PATH_SEPARATOR);
String path = PATH_SEPARATOR
+ StringUtils.join(Arrays.copyOfRange(pathSegments, 3, pathSegments.length), PATH_SEPARATOR);
return path;
}
/**
* Returns the resource represented by the relative path
*
* @param relativePath
* the relative path, e.g. /application/rest/downloads/4711
*
* @return the resource
*
* @throws URISyntaxException
* if something is wrong with the path
*/
public RestResponseEntity getBinaryData(String relativePath) throws URISyntaxException {
return getResource(relativePath, byte[].class);
}
/**
* Retrieves a REST-Resource with a given HTTP method.
* Example:
*
*
* ResponseEntity result = restClient.retrieveResource("/application/rest/calculator/add/47/11", null, Integer.class, HttpMethod.GET)
*
*
* @param path
* the relative path to the resource, starting with the application's name
* @param body
* the request body (optional)
* @param returnType
* the type of the response
* @param method
* the {@link HttpMethod} to use
*
* @return the {@link RestResponseEntity}
*
* @throws URISyntaxException
*/
public RestResponseEntity exchange(String path, OUT body, Class returnType, HttpMethod method)
throws URISyntaxException {
return exchange(new URI(url + path), body, method, returnType);
}
protected RestResponseEntity exchange(URI uri, OUT body, HttpMethod method, Class returnType) {
return exchange(uri, body, method, returnType, false);
}
protected RestResponseEntity exchange(URI uri, OUT body, HttpMethod method, Class returnType,
boolean acceptAnyType) {
if (LOGGER.isDebugEnabled() && body != null) {
doLog("OUT", body, null);
}
try {
RequestEntity out = new RequestEntity<>(body, getHeaders(acceptAnyType), method, uri);
ResponseEntity in = restTemplate.exchange(out, returnType);
setCookies(in);
if (LOGGER.isDebugEnabled() && in.getBody() != null) {
doLog("IN", in.getBody(), in.getStatusCode());
}
return RestResponseEntity.of(in);
} catch (HttpServerErrorException e) {
ErrorModel errorModel = null;
try {
String bodyAsString = e.getResponseBodyAsString();
if (StringUtils.isNotBlank(bodyAsString)) {
errorModel = objectMapper.readerFor(ErrorModel.class).readValue(bodyAsString);
}
} catch (IOException ioe) {
LOGGER.error("could not read error from response", e);
}
if (null == errorModel) {
errorModel = new ErrorModel();
errorModel.setCode(e.getStatusCode().value());
errorModel.setMessage(e.getMessage());
}
return new RestResponseEntity<>(errorModel, e.getResponseHeaders(), e.getStatusCode());
}
}
/**
* Retrieves a REST-Resource with HTTP GET.
* Example:
*
*
* ResponseEntity result = restClient.retrieveResource("/application/rest/calculator/add/47/11", null, Integer.class, HttpMethod.GET)
*
*
* @param path
* the relative path to the resource, starting with the application's name
* @param returnType
* the type of the response
*
* @return the {@link RestResponseEntity}
*
* @throws URISyntaxException
*/
public RestResponseEntity getResource(String path, Class returnType) throws URISyntaxException {
return exchange(new URI(url + path), null, HttpMethod.GET, returnType, true);
}
/**
* Wraps paging and sorting
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Pageable {
private int page = 0;
private int pageSize = 10;
private Map fieldSorts = new HashMap<>();
private boolean reset = false;
/**
* Creates a new pageable without any sorting
*
* @param page
* the zero-indexed page number
* @param pageSize
* the size of a page
**/
public Pageable(int page, int pageSize) {
this(page, pageSize, false);
}
/**
* Creates a new pageable without any sorting
*
* @param page
* the zero-indexed page number
* @param pageSize
* the size of a page
* @param reset
* set to {@code true} to reset current sort criteria
**/
public Pageable(int page, int pageSize, boolean reset) {
this.page = page;
this.pageSize = pageSize;
this.reset = reset;
}
/**
* Creates a new pageable
*
* @param page
* the zero-indexed page number
* @param pageSize
* the size of a page
* @param field
* the field to sort
* @param order
* the direction to sort
**/
public Pageable(int page, int pageSize, String field, OrderEnum order) {
this(page, pageSize, field, order, false);
}
/**
* Creates a new pageable
*
* @param page
* the zero-indexed page number
* @param pageSize
* the size of a page
* @param field
* the field to sort
* @param order
* the direction to sort
* @param reset
* set to {@code true} to reset current sort criteria
*/
public Pageable(int page, int pageSize, String field, OrderEnum order, boolean reset) {
setPage(page);
setPageSize(pageSize);
addSort(field, order);
this.reset = reset;
}
/**
* Adds a sort criteria for the given field.
*
* @param field
* the field to sort
* @param direction
* the direction to sort
*
* @return this {@link Pageable}
*/
public Pageable addSort(String field, OrderEnum direction) {
fieldSorts.put(field, direction);
return this;
}
/**
* Creates a query string (matrix-parameter style) containing all the sort criteria
*
* @return the query string
*/
public String getSortQuery() {
StringBuilder sortBuilder = new StringBuilder();
sortBuilder.append("page:").append(page).append(";");
sortBuilder.append("pageSize:").append(pageSize).append(";");
if (fieldSorts != null) {
for (String field : fieldSorts.keySet()) {
sortBuilder.append(field).append(":").append(fieldSorts.get(field).getValue()).append(";");
}
}
if (reset) {
sortBuilder.append("reset");
}
return sortBuilder.toString();
}
}
}