package oneapi.client.impl;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Map;
import java.util.Map.Entry;
import com.fasterxml.jackson.core.io.JsonStringEncoder;
import oneapi.config.Configuration;
import oneapi.exception.RequestException;
import oneapi.listener.ResponseListener;
import oneapi.model.Authentication;
import oneapi.model.Authentication.AuthType;
import oneapi.model.RequestData;
import oneapi.model.common.RequestError;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.RequestBuilder;
import com.ning.http.client.Response;
/**
* Client base class containing common methods and properties
* @author vavukovic
*
*/
public class OneAPIBaseClientImpl {
protected static final Logger LOGGER = LoggerFactory.getLogger(OneAPIBaseClientImpl.class);
protected static final String CHARSET = "UTF-8";
protected static final String URL_ENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded";
protected static final String JSON_CONTENT_TYPE = "application/json";
private Configuration configuration = null;
private ObjectMapper objectMapper = null;
private AsyncHttpClient asyncHttpClient = null;
/**
* Initialize OneAPIClientBase
*/
protected OneAPIBaseClientImpl(Configuration configuration) {
this.configuration = configuration;
}
/**
* Get Configuration object
* @return Configuration
*/
protected Configuration getConfiguration() {
return configuration;
}
/**
* Get asynchronous http client
*/
private AsyncHttpClient getAsyncHttpClient() {
if (asyncHttpClient == null) {
asyncHttpClient = new AsyncHttpClient();
}
return asyncHttpClient;
}
/**
* Get object mapper
* @return ObjectMapper
*/
private ObjectMapper getObjectMapper() {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
return objectMapper;
}
/**
* Execute method and deserialize response json
*/
protected <T> T executeMethod(RequestData requestData, Class<T> clazz) {
HttpURLConnection connection = sendOneAPIRequest(requestData);
return deserialize(connection, clazz, requestData.getRootElement());
}
/**
* Execute method and validate response
*/
protected void executeMethod(RequestData requestData)
{
HttpURLConnection connection = sendOneAPIRequest(requestData);
validateResponse(connection);
}
/**
* Execute method asynchronously and deserialize response json
*/
protected <T> void executeMethodAsync(RequestData requestData, Class<T> clazz, ResponseListener<T> responseListener) {
sendOneAPIRequestAsync(requestData, clazz, responseListener);
}
/**
* Convert json string to specific object
* @return T
*/
protected <T> T convertJSONToObject(byte[] jsonBytes, Class<T> clazz) {
return convertJSONToObject(jsonBytes, clazz, null);
}
/**
* Convert json string to specific object
* @return T
*/
protected <T> T convertJSONToObject(byte[] jsonBytes, Class<T> clazz, String rootElement) {
try {
if(null != rootElement && !"".equals(rootElement)) {
JsonNode node = getObjectMapper().reader().readTree(new ByteArrayInputStream(jsonBytes)).get(rootElement);
return getObjectMapper().readValue(node.toString(), clazz);
} else {
return getObjectMapper().readValue(jsonBytes, clazz);
}
} catch (Exception e) {
throw new RequestException(e);
}
}
/**
* Extract Id from resource url
*/
protected String getIdFromResourceUrl(String resourceUrl)
{
String id = "";
if (resourceUrl.contains("/"))
{
String[] arrResourceUrl = resourceUrl.split("/");
if (arrResourceUrl.length > 0)
{
id = arrResourceUrl[arrResourceUrl.length - 1];
}
}
return id;
}
/**
* Encode URL parameter
* @return String - encoded parameter
*/
protected String encodeURLParam(String param) {
try {
return URLEncoder.encode(param, CHARSET);
} catch (UnsupportedEncodingException e) {
throw new RequestException(e);
}
}
/**
* Send OneAPI request
* @throws RequestException
*/
private HttpURLConnection sendOneAPIRequest(RequestData requestData) {
HttpURLConnection connection = null;
try {
String apiUrl = appendMessagingBaseUrl(requestData.getResourcePath());
//setup connection with custom authorization
Authentication authentication = configuration.getAuthentication();
if (authentication.getType().equals(AuthType.BASIC)) {
connection = setupConnectionWithCustomAuthorization(apiUrl, "Basic", new String(Base64.encodeBase64((authentication.getUsername()+":"+authentication.getPassword()).getBytes("UTF-8")), "UTF-8"));
} else if (authentication.getType().equals(AuthType.OAUTH)) {
connection = setupConnectionWithCustomAuthorization(apiUrl, "OAuth", authentication.getAccessToken());
} else if (authentication.getType().equals(AuthType.IBSSO)) {
String ibssoToken = authentication.getIbssoToken();
connection = setupConnectionWithCustomAuthorization(apiUrl, "IBSSO", ibssoToken);
}
//Set Content Type
if ((requestData.getContentType() != null) && (requestData.getContentType().length() != 0)) {
if (connection != null) {
connection.setRequestProperty("Content-Type", requestData.getContentType());
}
}
if (connection != null) {
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("User-Agent", "OneApi-Java-".concat(SMSClient.VERSION));
}
//Set Request Method
connection.setRequestMethod(requestData.getRequestMethod().toString());
//Set Request Body
if (requestData.getFormParams() != null) {
connection.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
if (requestData.getContentType().equals(URL_ENCODED_CONTENT_TYPE)) {
out.write(formEncodeParams(requestData.getFormParams()));
} else if (requestData.getContentType().equals(JSON_CONTENT_TYPE)) {
String json = getObjectMapper().writeValueAsString(requestData.getFormParams());
out.write(json);
}
out.close();
}
return connection;
} catch (Exception e) {
throw new RequestException(e);
}
}
/**
* Send OneAPI request asynchronously
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private <T> void sendOneAPIRequestAsync(final RequestData requestData, final Class<T> clazz, final ResponseListener<T> responseListener) {
String apiUrl = appendMessagingBaseUrl(requestData.getResourcePath());
//Build request
RequestBuilder requestBuilder = new RequestBuilder()
.setUrl(apiUrl)
.setMethod(requestData.getRequestMethod().toString())
.addHeader("accept", "*/*")
.addHeader("User-Agent", "OneApi-Java-".concat(SMSClient.VERSION));
//Set content type
if (requestData.getContentType().length() != 0) {
requestBuilder.addHeader("Content-Type", requestData.getContentType());
}
try {
//Set Authorization header
Authentication authentication = configuration.getAuthentication();
if (authentication.getType().equals(AuthType.BASIC)) {
String basicCredentials = new String(Base64.encodeBase64((authentication.getUsername()+":"+authentication.getPassword()).getBytes("UTF-8")), "UTF-8");
requestBuilder.addHeader("Authorization", "Basic " + basicCredentials);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Authorization type Basic using " + basicCredentials);
}
} else if (authentication.getType().equals(AuthType.OAUTH)) {
String accessToken = authentication.getAccessToken();
requestBuilder.addHeader("Authorization", "OAuth " + authentication.getAccessToken());
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Authorization type OAuth using " + accessToken);
}
} else if (authentication.getType().equals(AuthType.IBSSO)) {
String ibssoToken = authentication.getIbssoToken();
requestBuilder.addHeader("Authorization", "IBSSO " + ibssoToken);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Authorization type IBSSO using " + ibssoToken);
}
}
//Set Request Body
if (requestData.getFormParams() != null) {
if (requestData.getContentType().equals(URL_ENCODED_CONTENT_TYPE)) {
requestBuilder.setBody(formEncodeParams(requestData.getFormParams()));
} else if (requestData.getContentType().equals(JSON_CONTENT_TYPE)) {
String json = getObjectMapper().writeValueAsString(requestData.getFormParams());
requestBuilder.setBody(JsonStringEncoder.getInstance().encodeAsUTF8(json));
}
}
//Execute async request
getAsyncHttpClient().executeRequest(requestBuilder.build(), new AsyncCompletionHandler() {
@Override
public Response onCompleted(Response response) {
try {
T jsonObject = deserialize(response, clazz, requestData.getRootElement());
responseListener.onGotResponse(jsonObject, null);
} catch (Exception e) {
responseListener.onGotResponse(null, e);
}
return response;
}
@Override
public void onThrowable(Throwable t){
responseListener.onGotResponse(null, t);
}
});
} catch (Exception e) {
throw new RequestException(e);
}
}
/**
* Setup http connection with custom authorization
* @throws MalformedURLException
* @throws IOException
*/
private HttpURLConnection setupConnectionWithCustomAuthorization(String url, String authorizationScheme, String authHeaderValue) throws MalformedURLException, IOException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Intitiating connection to URL: " + url);
}
HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection();
if (authHeaderValue!=null) {
con.setRequestProperty("Authorization", authorizationScheme + " " + authHeaderValue);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Authorization type " + authorizationScheme + " using " + authHeaderValue);
}
}
return con;
}
/**
* Deserialize response
* @return T
* @throws RequestException
*/
private <T> T deserialize(HttpURLConnection connection, Class<T> clazz, String rootElement) {
int responseCode = getResponseCode(connection);
String contentEncoding = getContentEncoding(connection.getContentType());
if (responseCode >= 200 && responseCode < 300) {
try {
return deserializeStream(connection.getInputStream(), contentEncoding, clazz, rootElement);
} catch (Exception e) {
throw new RequestException(e);
}
} else {
//Read RequestError from the response and throw the Exception
throw readRequestException(connection.getErrorStream(), responseCode, contentEncoding);
}
}
/**
* Deserialize response
* @return T
* @throws IOException
*/
private <T> T deserialize(Response response, Class<T> clazz, String rootElement) throws IOException {
int responseCode = response.getStatusCode();
InputStream inputStream = response.getResponseBodyAsStream();
if (responseCode >= 200 && responseCode < 300) {
T jsonObject = deserializeStream(inputStream, CHARSET, clazz, rootElement);
return jsonObject;
}
//Read RequestError from the response and throw the Exception
throw readRequestException(inputStream, responseCode, CHARSET);
}
/**
* Deserialize input stream
* @return T
* @throws IOException
*/
private <T> T deserializeStream(InputStream inputStream, String contentEncoding, Class<T> clazz, String rootElement) throws IOException {
LOGGER.debug("Processing JSON Response");
byte[] bytes = read(inputStream, contentEncoding);
return convertJSONToObject(bytes, clazz, rootElement);
}
/**
* Read connection input stream bytes
* @return byte[]
* @throws IOException
*/
private byte[] read(InputStream inputStream, String contentEncoding) throws IOException {
if (inputStream == null) {
throw new RequestException("Unexpected error occured. Response is empty.");
}
// Convert response body so it can be processed through JSON parser
ByteArrayOutputStream baOutputStream = new ByteArrayOutputStream();
int i;
while ((i = (byte) inputStream.read()) != -1) baOutputStream.write(i);
byte[] bytes = baOutputStream.toByteArray();
LOGGER.debug("Response data: " + new String(baOutputStream.toByteArray(), contentEncoding));
return bytes;
}
private RequestException readRequestException(InputStream errorStream, int responseCode, String contentEncoding) {
LOGGER.debug("Processing RequestError JSON Response");
String errorText = "Unexpected error occured.";
String messageId = "";
byte[] bytes;
try {
bytes = read(errorStream, contentEncoding);
RequestError errorResponse = convertJSONToObject(bytes, RequestError.class, "requestError");
if (errorResponse != null) {
if (errorResponse.getPolicyException() != null) {
errorText = errorResponse.getPolicyException().getText();
messageId = errorResponse.getPolicyException().getMessageId();
} else if (errorResponse.getServiceException() != null) {
errorText = errorResponse.getServiceException().getText();
messageId = errorResponse.getServiceException().getMessageId();
}
}
} catch (Exception e) {
return new RequestException(e, responseCode);
}
return new RequestException(errorText, messageId, responseCode);
}
/**
* Check if response status code is valid
*/
private void validateResponse(HttpURLConnection connection) {
int responseCode = getResponseCode(connection);
if (!(responseCode >= 200 && responseCode < 300)) {
String contentEncoding = getContentEncoding(connection.getContentType());
throw readRequestException(connection.getErrorStream(), responseCode, contentEncoding);
}
}
/**
* Encode specific object parameters
* @return String
* @throws IOException
*/
@SuppressWarnings("unchecked")
private String formEncodeParams(Object formParams) throws IOException {
Map<String, Object> formParamsMap = getObjectMapper().convertValue(formParams, Map.class);
if (formParamsMap == null) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("No request form parameters!");
}
return "";
}
StringBuilder sb = new StringBuilder();
int i = 0;
for (Entry<String, Object> entry : formParamsMap.entrySet()) {
if (entry.getValue() != null) {
if (entry.getValue() instanceof ArrayList) {
ArrayList<String> list = (ArrayList<String>) entry.getValue();
for (String listItem : list) {
if (listItem != null) {
appendEncodedParam(sb, entry.getKey(), listItem, i++);
}
}
} else {
appendEncodedParam(sb, entry.getKey(), entry.getValue(), i++);
}
}
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Request form parameters: " + sb.toString());
}
return sb.toString();
}
/**
* Encode specific parameter and append it to the string builder
* @throws UnsupportedEncodingException
*/
private void appendEncodedParam(StringBuilder sb, String key, Object value, int paramCounter) throws UnsupportedEncodingException {
if (paramCounter > 0) {
sb.append("&");
}
sb.append(URLEncoder.encode(key, CHARSET));
sb.append("=");
sb.append(URLEncoder.encode(String.valueOf(value), CHARSET));
}
/**
* Build url, append resourcePath to the apiUrl
* @param resourcePath Resource Path
* @return String
*/
private String appendMessagingBaseUrl(String resourcePath) {
StringBuilder urlBuilder = new StringBuilder(configuration.getApiUrl());
if (!configuration.getApiUrl().endsWith("/")) {
urlBuilder.append("/");
}
urlBuilder.append(encodeURLParam(configuration.getVersionOneAPISMS()));
if (!resourcePath.startsWith("/")) {
urlBuilder.append("/");
}
urlBuilder.append(resourcePath);
return urlBuilder.toString();
}
/**
* Get http connection response status code
*/
private int getResponseCode(HttpURLConnection connection) {
try {
return connection.getResponseCode();
} catch (IOException e) {
throw new RequestException(e);
}
}
/**
* Extract content encoding from the content type
*/
private String getContentEncoding(String contentType) {
String contentEncoding = "";
if (contentType != null) {
String[] contentTypeSplited = contentType.split(";");
for (String splitedItem : contentTypeSplited) {
if (splitedItem.toLowerCase().startsWith("charset=")) {
contentEncoding = splitedItem.substring("charset=".length());
}
}
}
if (contentEncoding.length() == 0) {
contentEncoding = CHARSET;
}
return contentEncoding;
}
}