Skip to content

Instantly share code, notes, and snippets.

@fabiankessler
Created October 26, 2015 23:04
Show Gist options
  • Save fabiankessler/a6cb51e247a8c450d4aa to your computer and use it in GitHub Desktop.
Save fabiankessler/a6cb51e247a8c450d4aa to your computer and use it in GitHub Desktop.
RestApiClient thread safe
/**
* Thread safe.
*/
public class RestApiClient {
/**
* Creation of these Client instances is expensive, and according to its Javadoc, they
* should be shared.
*/
private static ConcurrentMap<String, Client> hostMap = new ConcurrentHashMap<>();
public static class Builder {
private String basePath;
private Map<String, String> defaultHeaderMap = new HashMap<>();
private boolean debug = false;
private JSON json = new JSON();
private DateFormat dateFormat;
public Builder basePath(String basePath) {
this.basePath = basePath;
return this;
}
public Builder defaultHeader(String key, String value) {
defaultHeaderMap.put(key, value);
return this;
}
/**
* Set the User-Agent header's value (by adding to the default header map).
*/
public Builder userAgent(String userAgent) {
defaultHeader("User-Agent", userAgent);
return this;
}
public Builder debug(boolean debug) {
this.debug = debug;
return this;
}
public Builder json(JSON json) {
this.json = json;
return this;
}
public Builder dateFormat(DateFormat dateFormat) {
this.dateFormat = dateFormat;
return this;
}
public RestApiClient build() {
if (dateFormat==null) {
// Use ISO 8601 format for date and datetime.
// See https://en.wikipedia.org/wiki/ISO_8601
dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
// Use UTC as the default time zone.
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
return new RestApiClient(
basePath,
defaultHeaderMap,
debug,
json,
dateFormat
);
}
}
private final String basePath;
private final Map<String, String> defaultHeaderMap;
private final boolean debug;
private final JSON json;
private final DateFormat dateFormat;
private RestApiClient(String basePath, Map<String, String> defaultHeaderMap, boolean debug, JSON json, DateFormat dateFormat) {
this.basePath = basePath;
this.defaultHeaderMap = defaultHeaderMap;
this.debug = debug;
this.json = json;
this.dateFormat = dateFormat;
}
public String getBasePath() {
return basePath;
}
/**
* Check that whether debugging is enabled for this API client.
*/
public boolean isDebug() {
return debug;
}
/**
* Get the date format used to parse/format date parameters.
*/
public DateFormat getDateFormat() {
return dateFormat;
}
/**
* Parse the given string into Date object.
*/
public Date parseDate(String str) {
try {
return dateFormat.parse(str);
} catch (ParseException e) {
throw new RuntimeException(e);
}
}
/**
* Format the given Date object into string.
*/
public String formatDate(Date date) {
return dateFormat.format(date);
}
/**
* Format the given parameter object into string.
*/
public String parameterToString(Object param) {
if (param == null) {
return "";
} else if (param instanceof Date) {
return formatDate((Date) param);
} else if (param instanceof Collection) {
StringBuilder b = new StringBuilder();
for (Object o : (Collection) param) {
if (b.length() > 0) {
b.append(",");
}
b.append(String.valueOf(o));
}
return b.toString();
} else {
return String.valueOf(param);
}
}
/*
Format to {@code Pair} objects.
*/
public List<Pair> parameterToPairs(String collectionFormat, String name, Object value) {
List<Pair> params = new ArrayList<>();
// preconditions
if (name == null || name.isEmpty() || value == null) return params;
Collection valueCollection;
if (value instanceof Collection) {
valueCollection = (Collection) value;
} else {
params.add(new Pair(name, parameterToString(value)));
return params;
}
if (valueCollection.isEmpty()) {
return params;
}
// get the collection format
collectionFormat = (collectionFormat == null || collectionFormat.isEmpty() ? "csv" : collectionFormat); // default: csv
// create the params based on the collection format
if (collectionFormat.equals("multi")) {
for (Object item : valueCollection) {
params.add(new Pair(name, parameterToString(item)));
}
return params;
}
String delimiter = ",";
if (collectionFormat.equals("csv")) {
delimiter = ",";
} else if (collectionFormat.equals("ssv")) {
delimiter = " ";
} else if (collectionFormat.equals("tsv")) {
delimiter = "\t";
} else if (collectionFormat.equals("pipes")) {
delimiter = "|";
}
StringBuilder sb = new StringBuilder();
for (Object item : valueCollection) {
sb.append(delimiter);
sb.append(parameterToString(item));
}
params.add(new Pair(name, sb.substring(1)));
return params;
}
/**
* Select the Accept header's value from the given accepts array:
* if JSON exists in the given array, use it;
* otherwise use all of them (joining into a string)
*
* @param accepts The accepts array to select from
* @return The Accept header to use. If the given array is empty,
* null will be returned (not to set the Accept header explicitly).
*/
public String selectHeaderAccept(String[] accepts) {
if (accepts.length == 0) return null;
if (StringUtil.containsIgnoreCase(accepts, "application/json")) return "application/json";
return StringUtil.join(accepts, ",");
}
/**
* Select the Content-Type header's value from the given array:
* if JSON exists in the given array, use it;
* otherwise use the first one of the array.
*
* @param contentTypes The Content-Type array to select from
* @return The Content-Type header to use. If the given array is empty,
* JSON will be used.
*/
public String selectHeaderContentType(String[] contentTypes) {
if (contentTypes.length == 0) return "application/json";
if (StringUtil.containsIgnoreCase(contentTypes, "application/json")) return "application/json";
return contentTypes[0];
}
/**
* Escape the given string to be used as URL query value.
*/
public String escapeString(String str) {
try {
return URLEncoder.encode(str, "utf8").replaceAll("\\+", "%20");
} catch (UnsupportedEncodingException e) {
return str;
}
}
/**
* Serialize the given Java object into string according the given
* Content-Type (only JSON is supported for now).
*/
public String serialize(Object obj, String contentType) throws RestApiException {
if (contentType.startsWith("application/json")) {
return json.serialize(obj);
} else {
throw new RestApiException(400, "can not serialize object into Content-Type: " + contentType);
}
}
/**
* Deserialize response body to Java object according to the Content-Type.
*/
public <T> T deserialize(ClientResponse response, TypeRef returnType) throws RestApiException {
String contentType = null;
List<String> contentTypes = response.getHeaders().get("Content-Type");
if (contentTypes != null && !contentTypes.isEmpty())
contentType = contentTypes.get(0);
if (contentType == null)
throw new RestApiException(500, "missing Content-Type in response");
String body;
if (response.hasEntity())
body = (String) response.getEntity(String.class);
else
body = "";
if (contentType.startsWith("application/json")) {
return json.deserialize(body, returnType);
} else if (returnType.getType().equals(String.class)) {
// Expecting string, return the raw response body.
return (T) body;
} else {
throw new RestApiException(
500,
"Content type \"" + contentType + "\" is not supported for type: "
+ returnType.getType()
);
}
}
private ClientResponse getAPIResponse(String path, String method, List<Pair> queryParams, Object body, byte[] binaryBody, Map<String, String> headerParams, Map<String, Object> formParams, String accept, String contentType, String[] authNames) throws RestApiException {
if (body != null && binaryBody != null) {
throw new RestApiException(500, "Either body or binaryBody must be null");
}
Client client = getClient();
String querystring = makeQueryString(queryParams);
WebResource.Builder builder;
if (accept == null)
builder = client.resource(basePath + path + querystring).getRequestBuilder();
else
builder = client.resource(basePath + path + querystring).accept(accept);
for (String key : headerParams.keySet()) {
builder = builder.header(key, headerParams.get(key));
}
for (String key : defaultHeaderMap.keySet()) {
if (!headerParams.containsKey(key)) {
builder = builder.header(key, defaultHeaderMap.get(key));
}
}
String encodedFormParams = null;
if (contentType.startsWith("multipart/form-data")) {
FormDataMultiPart mp = new FormDataMultiPart();
for (Entry<String, Object> param : formParams.entrySet()) {
if (param.getValue() instanceof File) {
File file = (File) param.getValue();
mp.field(param.getKey(), file.getName());
mp.bodyPart(new FileDataBodyPart(param.getKey(), file, MediaType.MULTIPART_FORM_DATA_TYPE));
} else {
mp.field(param.getKey(), parameterToString(param.getValue()), MediaType.MULTIPART_FORM_DATA_TYPE);
}
}
body = mp;
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
encodedFormParams = this.getXWWWFormUrlencodedParams(formParams);
}
ClientResponse response;
if ("GET".equals(method)) {
response = builder.get(ClientResponse.class);
} else if ("POST".equals(method)) {
if (encodedFormParams != null) {
response = builder.type(contentType).post(ClientResponse.class, encodedFormParams);
} else if (body == null) {
if (binaryBody == null)
response = builder.post(ClientResponse.class, null);
else
response = builder.type(contentType).post(ClientResponse.class, binaryBody);
} else if (body instanceof FormDataMultiPart) {
response = builder.type(contentType).post(ClientResponse.class, body);
} else {
response = builder.type(contentType).post(ClientResponse.class, serialize(body, contentType));
}
} else if ("PUT".equals(method)) {
if (encodedFormParams != null) {
response = builder.type(contentType).put(ClientResponse.class, encodedFormParams);
} else if (body == null) {
if (binaryBody == null)
response = builder.put(ClientResponse.class, null);
else
response = builder.type(contentType).put(ClientResponse.class, binaryBody);
} else {
response = builder.type(contentType).put(ClientResponse.class, serialize(body, contentType));
}
} else if ("DELETE".equals(method)) {
if (encodedFormParams != null) {
response = builder.type(contentType).delete(ClientResponse.class, encodedFormParams);
} else if (body == null) {
if (binaryBody == null)
response = builder.delete(ClientResponse.class);
else
response = builder.type(contentType).delete(ClientResponse.class, binaryBody);
} else {
response = builder.type(contentType).delete(ClientResponse.class, serialize(body, contentType));
}
} else {
throw new RestApiException(500, "unknown method type " + method);
}
return response;
}
private String makeQueryString(List<Pair> queryParams) {
if (queryParams == null || queryParams.isEmpty()) {
return "";
}
StringBuilder b = new StringBuilder();
b.append("?");
for (Pair queryParam : queryParams) {
if (!queryParam.getName().isEmpty()) {
b.append(escapeString(queryParam.getName()));
b.append("=");
b.append(escapeString(queryParam.getValue()));
b.append("&");
}
}
return b.substring(0, b.length() - 1); // -1 to remove the last ampersand.
}
/**
* Invoke API by sending HTTP request with the given options.
*
* @param path The sub-path of the HTTP URL
* @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
* @param queryParams The query parameters
* @param body The request body object - if it is not binary, otherwise null
* @param binaryBody The request body object - if it is binary, otherwise null
* @param headerParams The header parameters
* @param formParams The form parameters
* @param accept The request's Accept header
* @param contentType The request's Content-Type header
* @param authNames The authentications to apply
* @return The response body in type of string
*/
public <T> RestApiClientResponse<T> invokeAPI(String path, String method, List<Pair> queryParams, Object body, byte[] binaryBody, Map<String, String> headerParams, Map<String, Object> formParams, String accept, String contentType, String[] authNames, TypeRef returnType) throws RestApiException {
ClientResponse response = getAPIResponse(path, method, queryParams, body, binaryBody, headerParams, formParams, accept, contentType, authNames);
if (response.getStatusInfo() == ClientResponse.Status.NO_CONTENT) {
return new RestApiClientResponse<>(response, Optional.<T>absent());
} else if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
if (returnType == null)
return new RestApiClientResponse<>(response, Optional.<T>absent());
else {
T deserialized = deserialize(response, returnType);
return new RestApiClientResponse<>(response, Optional.fromNullable(deserialized));
}
} else {
String message = "error";
String respBody = null;
if (response.hasEntity()) {
try {
respBody = String.valueOf(response.getEntity(String.class));
message = respBody;
} catch (RuntimeException e) {
// e.printStackTrace();
}
}
throw new RestApiException(
response.getStatusInfo().getStatusCode(),
message,
response.getHeaders(),
respBody);
}
}
/**
* Invoke API by sending HTTP request with the given options - return binary result
*
* @param path The sub-path of the HTTP URL
* @param method The request method, one of "GET", "POST", "PUT", and "DELETE"
* @param queryParams The query parameters
* @param body The request body object - if it is not binary, otherwise null
* @param binaryBody The request body object - if it is binary, otherwise null
* @param headerParams The header parameters
* @param formParams The form parameters
* @param accept The request's Accept header
* @param contentType The request's Content-Type header
* @param authNames The authentications to apply
* @return The response body in type of string
*/
public byte[] invokeBinaryAPI(String path, String method, List<Pair> queryParams, Object body, byte[] binaryBody, Map<String, String> headerParams, Map<String, Object> formParams, String accept, String contentType, String[] authNames) throws RestApiException {
ClientResponse response = getAPIResponse(path, method, queryParams, body, binaryBody, headerParams, formParams, accept, contentType, authNames);
if (response.getStatusInfo() == ClientResponse.Status.NO_CONTENT) {
return null;
} else if (response.getStatusInfo().getFamily() == Family.SUCCESSFUL) {
if (response.hasEntity()) {
DataInputStream stream = new DataInputStream(response.getEntityInputStream());
byte[] data = new byte[response.getLength()];
try {
stream.readFully(data);
} catch (IOException ex) {
throw new RestApiException(500, "Error obtaining binary response data");
}
return data;
} else {
return new byte[0];
}
} else {
String message = "error";
if (response.hasEntity()) {
try {
message = String.valueOf(response.getEntity(String.class));
} catch (RuntimeException e) {
// e.printStackTrace();
}
}
throw new RestApiException(
response.getStatusInfo().getStatusCode(),
message);
}
}
/**
* Encode the given form parameters as request body.
*/
private String getXWWWFormUrlencodedParams(Map<String, Object> formParams) {
StringBuilder formParamBuilder = new StringBuilder();
for (Entry<String, Object> param : formParams.entrySet()) {
String keyStr = param.getKey();
String valueStr = parameterToString(param.getValue());
try {
formParamBuilder.append(URLEncoder.encode(param.getKey(), "utf8"))
.append("=")
.append(URLEncoder.encode(valueStr, "utf8"));
formParamBuilder.append("&");
} catch (UnsupportedEncodingException e) {
// move on to next
}
}
String encodedFormParams = formParamBuilder.toString();
if (encodedFormParams.endsWith("&")) {
encodedFormParams = encodedFormParams.substring(0, encodedFormParams.length() - 1);
}
return encodedFormParams;
}
/**
* Get an existing client or create a new client to handle HTTP request.
*/
private Client getClient() {
Client client = hostMap.get(basePath);
if (client!=null) return client;
client = Client.create();
if (debug) {
client.addFilter(new LoggingFilter());
}
hostMap.putIfAbsent(basePath, client);
return hostMap.get(basePath); //be sure to use the same in case of a race condition.
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment