com.mangopay.core.RestTool Maven / Gradle / Ivy
package com.mangopay.core;
import com.google.gson.*;
import com.mangopay.MangoPayApi;
import com.mangopay.core.enumerations.RequestType;
import com.mangopay.entities.RateLimit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
/**
* Class used to build HTTP request, call the request and handle response.
*/
public class RestTool {
// root/parent instance that holds the OAuthToken and Configuration instance
private MangoPayApi root;
// enable/disable debugging
private boolean debugMode;
// variable to flag that in request authentication data are required
private boolean authRequired;
private boolean clientIdRequired;
// array with HTTP header to send with request
private Map requestHttpHeaders;
// HTTP communication object
private HttpURLConnection connection;
// request type for current request
private String requestType;
// key-value collection pass in the request
private Map requestData;
// code get from response
private int responseCode;
// pagination object
private Pagination pagination;
// slf4j logger facade
private Logger logger;
/**
* Instantiates new RestTool object.
*
* @param root Root/parent instance that holds the OAuthToken and Configuration instance.
* @param authRequired Defines whether request authentication is required.
*/
public RestTool(MangoPayApi root, Boolean authRequired, Boolean clientIdRequired) {
this.root = root;
this.authRequired = authRequired;
this.clientIdRequired = clientIdRequired;
this.debugMode = this.root.getConfig().isDebugMode();
logger = LoggerFactory.getLogger(RestTool.class);
}
/**
* Adds HTTP headers as name/value pairs into the request.
*
* @param httpHeader Collection of headers name/value pairs.
*/
public void addRequestHttpHeader(Map httpHeader) {
if (this.requestHttpHeaders == null)
this.requestHttpHeaders = new HashMap<>();
this.requestHttpHeaders.putAll(httpHeader);
}
/**
* Adds HTTP header into the request.
*
* @param key Header name.
* @param value Header value.
*/
public void addRequestHttpHeader(final String key, final String value) {
addRequestHttpHeader(Collections.singletonMap(key, value));
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting single
* Dto
instances. In order to process collections of objects,
* use requestList
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term, one of the GET, PUT or POST.
* @param requestData Collection of key-value pairs of request
* parameters.
* @param pagination Pagination object.
* @param entity Instance of Dto class that is going to be
* sent in case of PUTting or POSTing.
* @return The Dto instance returned from API.
* @throws Exception
*/
public T request(Class classOfT, String urlMethod, String requestType, Map requestData, Pagination pagination, U entity) throws Exception {
return this.request(classOfT, null, urlMethod, requestType, requestData, pagination, entity);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting single
* Dto
instances. In order to process collections of objects,
* use requestList
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param idempotencyKey idempotency key for this request.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term, one of the GET, PUT or POST.
* @param requestData Collection of key-value pairs of request
* parameters.
* @param pagination Pagination object.
* @param entity Instance of Dto class that is going to be
* sent in case of PUTting or POSTing.
* @return The Dto instance returned from API.
* @throws Exception
*/
public T request(Class classOfT, String idempotencyKey, String urlMethod, String requestType, Map requestData, Pagination pagination, U entity) throws Exception {
this.requestType = requestType;
this.requestData = requestData;
return this.doRequest(classOfT, idempotencyKey, urlMethod, pagination, entity);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting single
* Dto
instances. In order to process collections of objects,
* use requestList
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term, one of the GET, PUT or POST.
* @return The Dto instance returned from API.
* @throws Exception
*/
public T request(Class classOfT, String idempotencyKey, String urlMethod, String requestType) throws Exception {
return request(classOfT, idempotencyKey, urlMethod, requestType, null, null, null);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting single
* Dto
instances. In order to process collections of objects,
* use requestList
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term, one of the GET, PUT or POST.
* @param requestData Collection of key-value pairs of request
* parameters.
* @return The Dto instance returned from API.
* @throws Exception
*/
public T request(Class classOfT, String idempotencyKey, String urlMethod, String requestType, Map requestData) throws Exception {
return request(classOfT, idempotencyKey, urlMethod, requestType, requestData, null, null);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting single
* Dto
instances. In order to process collections of objects,
* use requestList
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term, one of the GET, PUT or POST.
* @param requestData Collection of key-value pairs of request
* parameters.
* @param pagination Pagination object.
* @return The Dto instance returned from API.
* @throws Exception
*/
public T request(Class classOfT, String idempotencyKey, String urlMethod, String requestType, Map requestData, Pagination pagination) throws Exception {
return request(classOfT, idempotencyKey, urlMethod, requestType, requestData, pagination, null);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting collections of
* Dto
instances. In order to process single objects,
* use request
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param classOfTItem The class of single item in array.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term. For lists should be always GET.
* @param requestData Collection of key-value pairs of request
* parameters.
* @param pagination Pagination object.
* @param additionalUrlParams
* @return The collection of Dto instances returned from API.
* @throws Exception
*/
public List requestList(Class classOfT, Class classOfTItem, String urlMethod, String requestType, Map requestData, Pagination pagination, Map additionalUrlParams) throws Exception {
this.requestType = requestType;
this.requestData = requestData;
return this.doRequestList(classOfT, classOfTItem, urlMethod, pagination, additionalUrlParams);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting collections of
* Dto
instances. In order to process single objects,
* use request
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param classOfTItem The class of single item in array.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term. For lists should be always GET.
* @return The collection of Dto instances returned from API.
* @throws Exception
*/
public List requestList(Class classOfT, Class classOfTItem, String urlMethod, String requestType) throws Exception {
return requestList(classOfT, classOfTItem, urlMethod, requestType, null, null, null);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting collections of
* Dto
instances. In order to process single objects,
* use request
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param classOfTItem The class of single item in array.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term. For lists should be always GET.
* @param requestData Collection of key-value pairs of request
* parameters.
* @return The collection of Dto instances returned from API.
* @throws Exception
*/
public List requestList(Class classOfT, Class classOfTItem, String urlMethod, String requestType, Map requestData) throws Exception {
return requestList(classOfT, classOfTItem, urlMethod, requestType, requestData, null, null);
}
/**
* Makes a call to the MangoPay API.
*
* This generic method handles calls targeting collections of
* Dto
instances. In order to process single objects,
* use request
method instead.
*
* @param Type on behalf of which the request is being called.
* @param classOfT Type on behalf of which the request is being called.
* @param classOfTItem The class of single item in array.
* @param urlMethod Relevant method key.
* @param requestType HTTP request term. For lists should be always GET.
* @param requestData Collection of key-value pairs of request
* parameters.
* @param pagination Pagination object.
* @return The collection of Dto instances returned from API.
* @throws Exception
*/
public List requestList(Class classOfT, Class classOfTItem, String urlMethod, String requestType, Map requestData, Pagination pagination) throws Exception {
return requestList(classOfT, classOfTItem, urlMethod, requestType, requestData, pagination, null);
}
private T doRequest(Class classOfT, String idempotencyKey, String urlMethod, Pagination pagination, U entity) throws Exception {
T response = null;
try {
UrlTool urlTool = new UrlTool(root);
String restUrl = urlTool.getRestUrl(urlMethod, this.clientIdRequired, pagination, null);
URL url = new URL(urlTool.getFullUrl(restUrl));
if (this.debugMode) {
logger.info("FullUrl: {}", urlTool.getFullUrl(restUrl));
}
/* FOR WEB DEBUG PURPOSES
SocketAddress addr = new InetSocketAddress("localhost", 8888);
Proxy proxy = new Proxy(Proxy.Type.HTTP, addr);
connection = (HttpURLConnection)url.openConnection(proxy);
*/
connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
configureSslContext((HttpsURLConnection) connection);
}
// Get connection timeout from config
connection.setConnectTimeout(this.root.getConfig().getConnectTimeout());
// Get read timeout from config
connection.setReadTimeout(this.root.getConfig().getReadTimeout());
// set request method
connection.setRequestMethod(this.requestType);
// set headers
Map httpHeaders = this.getHttpHeaders(restUrl);
for (Entry entry : httpHeaders.entrySet()) {
connection.addRequestProperty(entry.getKey(), entry.getValue());
if (this.debugMode)
logger.info("HTTP Header: {}", entry.getKey() + ": " + entry.getValue());
}
if (idempotencyKey != null && !idempotencyKey.trim().isEmpty()) {
connection.addRequestProperty("Idempotency-Key", idempotencyKey);
}
// prepare to go
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
if (pagination != null) {
this.pagination = pagination;
}
if (this.debugMode)
logger.info("RequestType: {}", this.requestType);
if (this.requestData != null || entity != null || this.requestType.equals(RequestType.POST.toString())) {
String requestBody = "";
if (entity != null) {
requestBody = root.getGson().toJson(entity);
}
if (this.requestData != null) {
String params = "";
for (Entry entry : this.requestData.entrySet()) {
params += String.format("&%s=%s", URLEncoder.encode(entry.getKey(), "UTF-8"), URLEncoder.encode(entry.getValue(), "UTF-8"));
}
requestBody = params.replaceFirst("&", "");
}
if (this.debugMode) {
logger.info("RequestData: {}", this.requestData);
logger.info("RequestBody: {}", requestBody);
}
try (OutputStreamWriter osw = new OutputStreamWriter(connection.getOutputStream(), "UTF-8")) {
osw.write(requestBody);
osw.flush();
}
}
// get response
this.responseCode = connection.getResponseCode();
InputStream is;
if (this.responseCode != 200 && this.responseCode != 204) {
is = connection.getErrorStream();
} else {
is = connection.getInputStream();
}
checkApiConnection(is);
StringBuffer resp;
try (BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
String line;
resp = new StringBuffer();
while ((line = rd.readLine()) != null) {
resp.append(line);
}
}
String responseString = resp.toString();
if (this.debugMode) {
if (this.responseCode == 200 || this.responseCode == 204) {
logger.info("Response OK: {}", responseString);
} else {
logger.info("Response ERROR: {}", responseString);
}
}
if (this.responseCode == 200) {
this.readResponseHeaders(connection);
response = castResponseToEntity(classOfT, JsonParser.parseString(responseString).getAsJsonObject());
if (this.debugMode) logger.info("Response object: {}", response.toString());
}
this.checkResponseCode(responseString);
} catch (Exception ex) {
//ex.printStackTrace();
if (this.debugMode) logger.error("EXCEPTION: {}", Arrays.toString(ex.getStackTrace()));
throw ex;
}
return response;
}
private void configureSslContext(HttpsURLConnection connection) throws KeyManagementException, NoSuchAlgorithmException {
connection.setSSLSocketFactory(getSSLContext().getSocketFactory());
}
private SSLContext getSSLContext() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, null, new SecureRandom());
return sslContext;
}
private void readResponseHeaders(HttpURLConnection conn) {
List updatedRateLimits = null;
for (Map.Entry> k : conn.getHeaderFields().entrySet()) {
for (String v : k.getValue()) {
if (this.debugMode) logger.info("Response header: {}", k.getKey() + ":" + v);
if (k.getKey() == null) continue;
if (k.getKey().equals("X-RateLimit-Remaining") || k.getKey().equals("X-RateLimit-Remaining".toLowerCase())) {
if (updatedRateLimits == null) {
updatedRateLimits = initRateLimits();
}
List callsRemaining = k.getValue();
updatedRateLimits.get(0).setCallsRemaining(Integer.valueOf(callsRemaining.get(3)));
updatedRateLimits.get(1).setCallsRemaining(Integer.valueOf(callsRemaining.get(2)));
updatedRateLimits.get(2).setCallsRemaining(Integer.valueOf(callsRemaining.get(1)));
updatedRateLimits.get(3).setCallsRemaining(Integer.valueOf(callsRemaining.get(0)));
}
if (k.getKey().equals("X-RateLimit") || k.getKey().equals("X-RateLimit".toLowerCase())) {
if (updatedRateLimits == null) {
updatedRateLimits = initRateLimits();
}
List callsMade = k.getValue();
updatedRateLimits.get(0).setCallsMade(Integer.valueOf(callsMade.get(3)));
updatedRateLimits.get(1).setCallsMade(Integer.valueOf(callsMade.get(2)));
updatedRateLimits.get(2).setCallsMade(Integer.valueOf(callsMade.get(1)));
updatedRateLimits.get(3).setCallsMade(Integer.valueOf(callsMade.get(0)));
}
if (k.getKey().equals("X-RateLimit-Reset") || k.getKey().equals("X-RateLimit-Reset".toLowerCase())) {
if (updatedRateLimits == null) {
updatedRateLimits = initRateLimits();
}
List resetTimes = k.getValue();
updatedRateLimits.get(0).setResetTimeSeconds(Long.valueOf(resetTimes.get(3)));
updatedRateLimits.get(1).setResetTimeSeconds(Long.valueOf(resetTimes.get(2)));
updatedRateLimits.get(2).setResetTimeSeconds(Long.valueOf(resetTimes.get(1)));
updatedRateLimits.get(3).setResetTimeSeconds(Long.valueOf(resetTimes.get(0)));
}
if (k.getKey().equals("X-Number-Of-Pages") || k.getKey().equals("X-Number-Of-Pages".toLowerCase())) {
this.pagination.setTotalPages(Integer.parseInt(v));
}
if (k.getKey().equals("X-Number-Of-Items") || k.getKey().equals("X-Number-Of-Items".toLowerCase())) {
this.pagination.setTotalItems(Integer.parseInt(v));
}
if (k.getKey().equals("Link") || k.getKey().equals("Link".toLowerCase())) {
String linkValue = v;
String[] links = linkValue.split(",");
if (links != null && links.length > 0) {
for (String link : links) {
link = link.replaceAll(Matcher.quoteReplacement("<\""), "");
link = link.replaceAll(Matcher.quoteReplacement("\">"), "");
link = link.replaceAll(Matcher.quoteReplacement(" rel=\""), "");
link = link.replaceAll(Matcher.quoteReplacement("\""), "");
String[] oneLink = link.split(";");
if (oneLink != null && oneLink.length > 1) {
if (oneLink[0] != null && oneLink[1] != null) {
this.pagination.setLinks(oneLink);
}
}
}
}
}
}
}
if (updatedRateLimits != null) {
root.setRateLimits(updatedRateLimits);
}
}
private List initRateLimits() {
return Arrays.asList(
new RateLimit(15),
new RateLimit(30),
new RateLimit(60),
new RateLimit(24 * 60));
}
public T castResponseToEntity(Class classOfT, JsonObject response) {
return root.getGson().fromJson(response, classOfT);
}
private List doRequestList(Class classOfT, Class classOfTItem, String urlMethod, Pagination pagination) throws Exception {
return doRequestList(classOfT, classOfTItem, urlMethod, pagination, null);
}
private List doRequestList(Class classOfT, Class classOfTItem, String urlMethod, Pagination pagination, Map additionalUrlParams) throws Exception {
List response = new ArrayList<>();
try {
UrlTool urlTool = new UrlTool(root);
String restUrl = urlTool.getRestUrl(urlMethod, this.clientIdRequired, pagination, additionalUrlParams);
URL url = new URL(urlTool.getFullUrl(restUrl));
if (this.debugMode)
logger.info("FullUrl: {}", urlTool.getFullUrl(restUrl));
connection = (HttpURLConnection) url.openConnection();
if (connection instanceof HttpsURLConnection) {
configureSslContext((HttpsURLConnection) connection);
}
// set request method
connection.setRequestMethod(this.requestType);
// set headers
Map httpHeaders = this.getHttpHeaders(restUrl);
for (Entry entry : httpHeaders.entrySet()) {
connection.addRequestProperty(entry.getKey(), entry.getValue());
if (this.debugMode)
logger.info("HTTP Header: {}", entry.getKey() + ": " + entry.getValue());
}
// prepare to go
connection.setUseCaches(false);
connection.setDoInput(true);
connection.setDoOutput(true);
if (pagination != null) {
this.pagination = pagination;
}
if (this.debugMode)
logger.info("RequestType: {}", this.requestType);
if (this.requestData != null) {
String requestBody;
String params = "";
for (Entry entry : this.requestData.entrySet()) {
params += String.format("&%s=%s", URLEncoder.encode(entry.getKey(), "UTF-8"), URLEncoder.encode(entry.getValue(), "UTF-8"));
}
requestBody = params.replaceFirst("&", "");
writeRequestBody(connection, requestBody);
if (this.debugMode) {
logger.info("RequestData: {}", this.requestData);
logger.info("RequestBody: {}", requestBody);
}
} else if (restUrl.contains("consult")
&& (restUrl.contains("KYC/documents") || restUrl.contains("dispute-documents"))) {
writeRequestBody(connection, "");
}
//Get Response
this.responseCode = connection.getResponseCode();
InputStream is;
if (this.responseCode != 200) {
is = connection.getErrorStream();
} else {
is = connection.getInputStream();
}
checkApiConnection(is);
StringBuffer resp;
try (BufferedReader rd = new BufferedReader(new InputStreamReader(is))) {
String line;
resp = new StringBuffer();
while ((line = rd.readLine()) != null) {
resp.append(line);
resp.append('\r');
}
}
String responseString = resp.toString();
if (this.debugMode) {
if (this.responseCode == 200) {
logger.info("Response OK: {}", responseString);
} else {
logger.info("Response ERROR: {}", responseString);
}
}
if (this.responseCode == 200) {
this.readResponseHeaders(connection);
JsonArray ja = JsonParser.parseString(responseString).getAsJsonArray();
for (int x = 0; x < ja.size(); x++) {
JsonObject jo = ja.get(x).getAsJsonObject();
T toAdd = castResponseToEntity(classOfTItem, jo);
response.add(toAdd);
}
if (this.debugMode) {
logger.info("Response object: {}", response.toString());
logger.info("Elements count: {}", response.size());
}
}
this.checkResponseCode(responseString);
} catch (Exception ex) {
if (this.debugMode) logger.error("EXCEPTION: {}", Arrays.toString(ex.getStackTrace()));
throw ex;
}
return response;
}
private void writeRequestBody(HttpURLConnection connection, String body) throws IOException {
try (DataOutputStream wr = new DataOutputStream(connection.getOutputStream())) {
wr.writeBytes(body);
wr.flush();
}
}
/**
* Gets HTTP header to use in request.
*
* @param restUrl The REST API URL.
* @return Array containing HTTP headers.
*/
private Map getHttpHeaders(String restUrl) throws Exception {
// return if already created...
if (this.requestHttpHeaders != null)
return this.requestHttpHeaders;
// ...or initialize with default headers
Map httpHeaders = new HashMap<>();
// content type
httpHeaders.put("Content-Type", "application/json");
// AuthenticationHelper http header
if (this.authRequired) {
AuthenticationHelper authHlp = new AuthenticationHelper(root);
httpHeaders.putAll(authHlp.getHttpHeaderKey());
}
httpHeaders.put("User-Agent", String.format("MangoPay V2 SDK Java %s", root.getConfig().getVersion()));
if(this.root.getConfig().isUkHeaderFlag()) {
httpHeaders.put("x-tenant-id", "uk");
}
return httpHeaders;
}
private void checkApiConnection(InputStream is) throws ResponseException {
if (is == null) {
ResponseException responseException = new ResponseException("Connection to Mangopay API failed");
responseException.setResponseHttpCode(500);
responseException.setResponseHttpDescription("Internal Server Error");
responseException.setApiMessage("Connection to Mangopay API failed");
throw responseException;
}
}
/**
* Checks the HTTP response code and if it's neither 200 nor 204 throws a ResponseException.
*
* @param message Text response.
* @throws ResponseException If response code is other than 200 or 204.
*/
private void checkResponseCode(String message) throws ResponseException {
if (this.responseCode != 200 && this.responseCode != 204) {
HashMap responseCodes = new HashMap() {{
put(206, "PartialContent");
put(400, "Bad request");
put(401, "Unauthorized");
put(403, "Prohibition to use the method");
put(404, "Not found");
put(405, "Method not allowed");
put(413, "Request entity too large");
put(422, "Unprocessable entity");
put(500, "Internal server error");
put(501, "Not implemented");
}};
ResponseException responseException = new ResponseException(message);
responseException.setResponseHttpCode(this.responseCode);
if (responseCodes.containsKey(this.responseCode)) {
responseException.setResponseHttpDescription(responseCodes.get(this.responseCode));
} else {
responseException.setResponseHttpDescription("Unknown response error");
}
if (message != null) {
try {
JsonObject error = JsonParser.parseString(message).getAsJsonObject();
for (Entry entry : error.entrySet()) {
switch (entry.getKey().toLowerCase()) {
case "message":
responseException.setApiMessage(entry.getValue().getAsString());
break;
case "type":
responseException.setType(entry.getValue().getAsString());
break;
case "id":
responseException.setId(entry.getValue().getAsString());
break;
case "date":
responseException.setDate((int) entry.getValue().getAsDouble());
break;
case "errors":
if (entry.getValue() == null) break;
if (entry.getValue().isJsonNull()) break;
for (Entry errorEntry : entry.getValue().getAsJsonObject().entrySet()) {
if (!responseException.getErrors().containsKey(errorEntry.getKey()))
responseException.getErrors().put(errorEntry.getKey(), errorEntry.getValue().getAsString());
else {
String description = responseException.getErrors().get(errorEntry.getKey());
description = " | " + errorEntry.getValue().getAsString();
responseException.getErrors().put(errorEntry.getKey(), description);
}
}
break;
}
}
} catch (IllegalStateException | JsonSyntaxException ex) {
responseException.setType("Resource not found");
responseException.setApiMessage("API Endpoint not found");
}
}
throw responseException;
}
}
}