com.plenigo.sdk.internal.util.RestClient Maven / Gradle / Ivy
package com.plenigo.sdk.internal.util;
import com.plenigo.sdk.PlenigoException;
import com.plenigo.sdk.internal.ErrorCode;
import com.plenigo.sdk.internal.exceptions.ApiExceptionTranslator;
import org.json.simple.JSONObject;
import org.json.simple.parser.ParseException;
import javax.xml.bind.DatatypeConverter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
*
* This class is a client used to communicate with plenigo's API services.
*
*
* IMPORTANT: This class is part of the internal API, please do not use it, because it can
* be removed in future versions of the SDK or access to such elements could
* be changed from 'public' to 'default' or less.
*
*
* Thread safety: This class is thread safe and can be injected.
*
*/
public class RestClient {
private static final Logger LOGGER = Logger.getLogger(RestClient.class.getName());
/**
* Default connection timeout.
*/
private static final int DEFAULT_CONN_TIMEOUT = 10000;
/**
* Default read timeout.
*/
private static final int DEFAULT_READ_TIMEOUT = 20000;
/**
* Get Method.
*/
private static final String GET_METHOD = "GET";
/**
* POST Method.
*/
private static final String POST_METHOD = "POST";
/**
* DELETE Method.
*/
private static final String DELETE_METHOD = "DELETE";
/**
* PUT Method.
*/
private static final String PUT_METHOD = "PUT";
/**
* Http Accept header name.
*/
private static final String ACCEPT_HEADER_NAME = "Accept";
/**
* JSON media type.
*/
private static final String JSON_MEDIA_TYPE = "application/json";
/**
* Authorization header name.
*/
private static final String AUTHORIZATION_HEADER_NAME = "Authorization";
/**
* Basic authentication prefix.
*/
private static final String BASIC_AUTH_PREFIX = "Basic ";
/**
* Question mark that separates the action from the
* query string.
*/
public static final String QUESTION_MARK = "?";
public static final int INITIAL_CORRECT_STATUS_CODE = 200;
public static final int FINAL_CORRECT_STATUS_CODE = 300;
/**
* Default headers used in every request.
*/
private Map defaultHeaders;
/**
* Connection timeout used by the client instance.
*/
private int connectionTimeout;
/**
* Connection timeout used by the client instance.
*
* @return The connectionTimeout
*/
public int getConnectionTimeout() {
return connectionTimeout;
}
/**
* Sets the connection timeout to use by the client.
*
* @param milliseconds the connectionTimeout to set
*/
public void setConnectionTimeout(final int milliseconds) {
this.connectionTimeout = milliseconds;
}
/**
* Returns the read timeout used by the client.
*
* @return The readTimeout
*/
public int getReadTimeout() {
return readTimeout;
}
/**
* @param milliseconds The milliseconds to set
*/
public void setReadTimeout(final int milliseconds) {
this.readTimeout = milliseconds;
}
/**
* Read timeout used by the client.
*/
private int readTimeout;
/**
* Default constructor.
*/
public RestClient() {
setConnectionTimeout(DEFAULT_CONN_TIMEOUT);
setReadTimeout(DEFAULT_READ_TIMEOUT);
defaultHeaders = new HashMap();
defaultHeaders.put(ACCEPT_HEADER_NAME, JSON_MEDIA_TYPE);
}
/**
* Constructor that creates an Authorization header.
*
* @param encodedusernameAndPassword The encoded basic authentication user name/password
*
* @throws UnsupportedEncodingException When the default encoding is not supported
*/
public RestClient(String encodedusernameAndPassword) throws UnsupportedEncodingException {
this();
String authorizationString = BASIC_AUTH_PREFIX + encodedusernameAndPassword;
defaultHeaders.put(AUTHORIZATION_HEADER_NAME, authorizationString);
}
/**
* Constructor that creates an Authorization header.
*
* @param username The basic authentication user name
* @param password The basic authentication password
*
* @throws UnsupportedEncodingException When the default encoding is not supported
*/
public RestClient(String username, String password) throws UnsupportedEncodingException {
this();
String usernamePassword = username + ":" + password;
String authorizationString = BASIC_AUTH_PREFIX + DatatypeConverter.printBase64Binary(
usernamePassword.getBytes(Charset.DEFAULT));
defaultHeaders.put(AUTHORIZATION_HEADER_NAME, authorizationString);
}
/**
* This method builds an URL based on the action and the query string.
*
* @param action The action to call
* @param query The query string to use
*
* @return a partial URL with the action and the query string
*/
private String buildURL(final String action, final String query) {
StringBuilder url = new StringBuilder(action);
if (query != null && !query.isEmpty()) {
if (!action.contains(QUESTION_MARK) && !query.contains(QUESTION_MARK)) {
url.append(QUESTION_MARK);
}
url.append(query);
}
return url.toString();
}
/**
* This method does a HTTP request and reads the response as a JSON object
* and puts it into a {@link java.util.Map}.
*
* @param apiUrl The URL of the method call
* @param referenceUrl The template url used as reference
* @param method The method to call e.g. (GET or POST)
* @param action The action to call
* @param query The query string to use
* @param body The json body to send
* @param headers The headers to send
*
* @return map of JSON based results
*
* @throws PlenigoException whenever an error happens
*/
@SuppressWarnings("unchecked")
private Map call(String apiUrl, String referenceUrl, final String method,
final String action, final String query, final Map body, final Map headers)
throws PlenigoException {
String restCallInfo = String.format("rest resource call: [apiUrl: %s, method: %s, action: %s]", apiUrl, method
, action);
LOGGER.log(Level.INFO, "Doing a " + restCallInfo);
try {
HttpURLConnection con = getHttpConnection(apiUrl, action, query, headers);
con.setRequestMethod(method);
addBodyToRequest(con, body);
return handleResponse(con, referenceUrl, action);
} catch (ConnectException e) {
throw new PlenigoException(ErrorCode.CONNECTION_ERROR, restCallInfo + " had a connection error", e);
} catch (UnknownHostException e) {
throw new PlenigoException(ErrorCode.UNKNOWN_HOST, restCallInfo + " is using an unknown host: ", e);
} catch (PlenigoException e) {
throw e;
} catch (Exception e) {
LOGGER.log(Level.SEVERE, restCallInfo + " had an unexpected error", e);
throw new PlenigoException(ErrorCode.SERVER, ErrorCode.SERVER.getMsg(), e);
}
}
/**
* Adds the body map as a json object to the http request.
*
* @param con The http connection
* @param body The json body to send
*
* @throws IOException if a write error occurs
*/
private void addBodyToRequest(HttpURLConnection con, Map body) throws IOException {
if (body != null) {
con.setRequestProperty("Content-Type", "application/json");
con.setDoOutput(true);
byte[] outputInBytes = new JSONObject(body).toJSONString().getBytes(Charset.DEFAULT);
OutputStream os = con.getOutputStream();
os.write(outputInBytes);
os.flush();
os.close();
}
}
/**
* This handles the response or throws a PlenigoException based on the response code.
*
* @param con The HTTPConnection
* @param referenceUrl The template url used as reference
* @param resource The api resource name used
*
* @return A Map that represents the json response
*
* @throws IOException if a communication error happened
* @throws ParseException If there was an error parsing the response given as a JSON
* @throws PlenigoException whenever an error happens
*/
private Map handleResponse(HttpURLConnection con, String referenceUrl, String resource) throws IOException, ParseException
, PlenigoException {
Map json;
InputStream in = null;
try {
LOGGER.log(Level.FINEST, "HTTP Response for resource {0}: {1}", new Object[]{resource, con.getResponseCode()});
if (con.getResponseCode() >= INITIAL_CORRECT_STATUS_CODE && con.getResponseCode() <= FINAL_CORRECT_STATUS_CODE) {
in = con.getInputStream();
try {
json = SdkUtils.
parseJSONObject(
new InputStreamReader(in, Charset.DEFAULT));
} catch (ParseException pe) {
//we will return the parse exceptions as null since there are calls that do not contain any data to read
//This will only be considered for OK responses
//Only case where this would also return null is if the response json is also invalid
return null;
}
LOGGER.log(Level.FINEST, "Parsed JSON Response for resource {0}: {1}", new Object[]{resource, json});
} else {
in = con.getErrorStream();
String urlToSend = resource;
if (referenceUrl != null && !referenceUrl.isEmpty()) {
urlToSend = referenceUrl;
}
throw ApiExceptionTranslator.get().translate(String.valueOf(con.getResponseCode()), urlToSend, in);
}
} finally {
if (in != null) {
try {
LOGGER.log(Level.FINEST, "Closing response stream...");
in.close();
LOGGER.log(Level.FINEST, "Response stream closed");
} catch (IOException e) {
LOGGER.log(Level.SEVERE, "Error while closing the response stream", e);
}
}
if (con != null) {
LOGGER.log(Level.FINEST, "Closing connection stream...");
con.disconnect();
LOGGER.log(Level.FINEST, "Connection stream closed");
}
}
return json;
}
/**
* Creates a HTTP connection with the base URL, the action
* that wants to be called and the query string if any.
*
* @param apiUrl The API url to be queried
* @param action Action to be used
* @param query Query string
* @param headers The headers to send
*
* @return {@link HttpURLConnection}
*
* @throws IOException When there is a read/write error
*/
private HttpURLConnection getHttpConnection(String apiUrl, String action,
String query, Map headers) throws IOException {
if (apiUrl == null || apiUrl.isEmpty()) {
LOGGER.log(Level.FINEST, "There api url is null or empty, not doing Http Request");
return null;
}
String fullURL;
fullURL = apiUrl + buildURL(action, query);
URL url = new URL(fullURL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setReadTimeout(getReadTimeout());
con.setConnectTimeout(getConnectionTimeout());
if (!defaultHeaders.isEmpty()) {
for (Map.Entry header : defaultHeaders.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
}
addHeadersToRequest(con, defaultHeaders);
addHeadersToRequest(con, headers);
return con;
}
/**
* Adds the headers to teh http request.
*
* @param con The connection to use
* @param headers Headers to add
*/
private void addHeadersToRequest(HttpURLConnection con, Map headers) {
if (headers != null && !headers.isEmpty()) {
for (Map.Entry header : headers.entrySet()) {
con.setRequestProperty(header.getKey(), header.getValue());
}
}
}
/**
* This does a GET HTTP call.
*
* @param apiUrl The base API url
* @param referenceUrl The template url used as reference
* @param resource The resource to call
* @param query The query string to use
* @param headers The headers to send
*
* @return The map result of the call
*
* @throws PlenigoException whenever an error happens
*/
public Map get(String apiUrl, String referenceUrl, String resource,
String query, Map headers) throws PlenigoException {
return call(apiUrl, referenceUrl, GET_METHOD, resource, query, null, headers);
}
/**
* This does a POST HTTP call and accepts a body to be sent as a json object.
*
* @param apiUrl The base API url
* @param referenceUrl The template url used as reference
* @param resource The resource to call
* @param query The query string to use
* @param body The body to use
* @param headers The headers to send
*
* @return The map result of the call
*
* @throws PlenigoException whenever an error happens
*/
public Map post(String apiUrl, String referenceUrl, String resource,
String query, Map body, Map headers) throws PlenigoException {
return call(apiUrl, referenceUrl, POST_METHOD, resource, query, body, headers);
}
/**
* This does a DELETE HTTP call.
*
* @param apiUrl The base API url
* @param referenceUrl The template url used as reference
* @param resource The resource to call
* @param query The query string to use
* @param headers The headers to send
*
* @return The map result of the call
*
* @throws PlenigoException whenever an error happens
*/
public Map delete(String apiUrl, String referenceUrl, String resource,
String query, Map headers) throws PlenigoException {
return call(apiUrl, referenceUrl, DELETE_METHOD, resource, query, null, headers);
}
/**
* This does a PUT HTTP call and accepts a body to be sent as a json object.
*
* @param apiUrl The base API url
* @param referenceUrl The template url used as reference
* @param resource The resource to call
* @param query The query string to use
* @param body The body to use
* @param headers The headers to send
*
* @return The map result of the call
*
* @throws PlenigoException whenever an error happens
*/
public Map put(String apiUrl, String referenceUrl, String resource,
String query, Map body, Map headers) throws PlenigoException {
return call(apiUrl, referenceUrl, PUT_METHOD, resource, query, body, headers);
}
}