All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.messagebird.MessageBirdServiceImpl Maven / Gradle / Ivy

Go to download

The MessageBird API provides a API to the MessageBird SMS and voicemail services located at https://www.messagebird.com.

The newest version!
package com.messagebird;

import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.*;
import com.messagebird.exceptions.GeneralException;
import com.messagebird.exceptions.NotFoundException;
import com.messagebird.exceptions.UnauthorizedException;
import com.messagebird.objects.ErrorReport;
import com.messagebird.objects.PagedPaging;
import org.apache.maven.artifact.versioning.ComparableVersion;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;

/**
 * Implementation of MessageBirdService
 * Sends and receives JSON objects from the Messagebird platform
 * 

* Created by rvt on 1/5/15. */ public class MessageBirdServiceImpl implements MessageBirdService { private static final String NOT_AUTHORISED_MSG = "You are not authorised for the MessageBird service, please check your access key."; private static final String FAILED_DATA_RESPONSE_CODE = "Failed to retrieve data from MessageBird service with response code "; private static final String ACCESS_KEY_MUST_BE_SPECIFIED = "Access key must be specified"; private static final String SERVICE_URL_MUST_BE_SPECIFIED = "Service URL must be specified"; private static final String REQUEST_VALUE_MUST_BE_SPECIFIED = "Request value must be specified"; private static final String REQUEST_METHOD_NOT_ALLOWED = "Request method %s is not allowed."; private static final String CAN_NOT_ALLOW_PATCH = "Can not set HttpURLConnection.methods field to allow PATCH."; private static final String METHOD_DELETE = "DELETE"; private static final String METHOD_GET = "GET"; private static final String METHOD_PATCH = "PATCH"; private static final String METHOD_POST = "POST"; private static final String METHOD_PUT = "PUT"; private static final List REQUEST_METHODS = Arrays.asList(METHOD_DELETE, METHOD_GET, METHOD_PATCH, METHOD_POST, METHOD_PUT); private static final List REQUEST_METHODS_WITH_PAYLOAD = Arrays.asList(METHOD_PATCH, METHOD_POST, METHOD_PUT); private static final String[] PROTOCOL_LISTS = new String[]{"http://", "https://"}; private static final List PROTOCOLS = Arrays.asList(PROTOCOL_LISTS); private static final ComparableVersion JAVA_VERSION = getJavaVersion(); // Indicates whether we've overridden HttpURLConnection's behaviour to // allow PATCH requests yet. Also see docs on allowPatchRequestsIfNeeded(). private static boolean isPatchRequestAllowed = false; private static final int BUFFER_SIZE = 4096; private final String accessKey; private final String serviceUrl; private final String clientVersion = "6.1.7"; private final String userAgentString; private Proxy proxy = null; public MessageBirdServiceImpl(final String accessKey, final String serviceUrl) { if (accessKey == null) { throw new IllegalArgumentException(ACCESS_KEY_MUST_BE_SPECIFIED); } if (serviceUrl == null || serviceUrl.length() == 0) { throw new IllegalArgumentException(SERVICE_URL_MUST_BE_SPECIFIED); } this.accessKey = accessKey; this.serviceUrl = serviceUrl; this.userAgentString = determineUserAgentString(); } private static ComparableVersion getJavaVersion() { try { String version = System.getProperty("java.version"); return new ComparableVersion(version); } catch (IllegalArgumentException e) { return new ComparableVersion("0.0"); } } private String determineUserAgentString() { return String.format("MessageBird Java/%s ApiClient/%s", JAVA_VERSION, clientVersion); } /** * Initiate service with default serviceUrl. * * @param accessKey developer access key */ public MessageBirdServiceImpl(final String accessKey) { this(accessKey, "https://rest.messagebird.com"); } @Override public R request(String request, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { return getJsonData(request, null, "GET", clazz); } @Override public R requestByID(String request, String id, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { String path = ""; if (id != null) { path = "/" + id; } return getJsonData(request + path, null, "GET", clazz); } @Override public R requestByID(String request, String id, Map params, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { String path = ""; if (id != null) { path = "/" + id; } // Make rest of GET request String queryParams = ""; if (!params.isEmpty()) { queryParams = "?" + getPathVariables(params); } return getJsonData(request + path + queryParams, null, "GET", clazz); } @Override public List requestByIdAsList(String request, String id, Class elementClass) throws UnauthorizedException, GeneralException, NotFoundException { String path = ""; if (id != null) { path = "/" + id; } return getJsonDataAsList(request + path, null, "GET", elementClass); } @Override public void deleteByID(String request, String id) throws UnauthorizedException, GeneralException, NotFoundException { getJsonData(request + "/" + id, null, "DELETE", null); } @Override public R delete(String request, Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { return getJsonData(request, null, "DELETE", clazz); } @Override public R requestList(String request, Integer offset, Integer limit, Class clazz) throws UnauthorizedException, GeneralException { Map map = new LinkedHashMap<>(); if (offset != null) map.put("offset", String.valueOf(offset)); if (limit != null) map.put("limit", String.valueOf(limit)); try { return getJsonData(request + "?" + getPathVariables(map), null, "GET", clazz); } catch (NotFoundException e) { throw new GeneralException(e); } } @Override public R requestList(String request, Map params, Integer offset, Integer limit, Class clazz) throws UnauthorizedException, GeneralException { if (offset != null) params.put("offset", String.valueOf(offset)); if (limit != null) params.put("limit", String.valueOf(limit)); try { return getJsonData(request + "?" + getPathVariables(params), null, "GET", clazz); } catch (NotFoundException e) { throw new GeneralException(e); } } @Override public R requestList(String request, PagedPaging pagedPaging, Class clazz) throws UnauthorizedException, GeneralException { Map map = new LinkedHashMap<>(); if (pagedPaging.getPage() != null) map.put("page", String.valueOf(pagedPaging.getPage())); if (pagedPaging.getPageSize() != null) map.put("perPage", String.valueOf(pagedPaging.getPageSize())); try { return getJsonData(request + "?" + getPathVariables(map), null, "GET", clazz); } catch (NotFoundException e) { throw new GeneralException(e); } } @Override public R sendPayLoad(String request, P payload, Class clazz) throws UnauthorizedException, GeneralException { return this.sendPayLoad("POST", request, payload, clazz); } @Override public R sendPayLoad(String method, String request, P payload, Class clazz) throws UnauthorizedException, GeneralException { return sendPayLoad(method, request, new HashMap<>(), payload, clazz); } @Override public R sendPayLoad(String method, String request, Map headers, P payload, Class clazz) throws UnauthorizedException, GeneralException { if (!REQUEST_METHODS_WITH_PAYLOAD.contains(method)) { throw new IllegalArgumentException(String.format(REQUEST_METHOD_NOT_ALLOWED, method)); } try { return getJsonData(request, payload, method, headers, clazz); } catch (NotFoundException e) { throw new GeneralException(e); } } @Override public String getBinaryData(String request, String basePath, String fileName) throws GeneralException, UnauthorizedException, NotFoundException { if (basePath == null) { String homePath = System.getProperty("user.home"); //Home path is not existing if (homePath == null) { throw new IllegalArgumentException("BasePath must be specified."); } basePath = String.format("%s/%s",homePath,"Downloads"); } File file = new File(basePath); if(!file.exists()) { throw new IllegalArgumentException("basePath must be existed as directory."); } if(!file.isDirectory()) { throw new IllegalArgumentException("basePath must be a directory."); } String filePath = String.format("%s/%s", basePath, fileName); return doGetRequestForFileAndStore(request, filePath); } public T getJsonData(final String request, final P payload, final String requestType, final Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { return getJsonData(request, payload, requestType, new HashMap<>(), clazz); } public List getJsonDataAsList(final String request, final P payload, final String requestType, final Class elementClass) throws UnauthorizedException, GeneralException, NotFoundException { return getJsonDataAsList(request, payload, requestType, new HashMap<>(), elementClass); } public T getJsonData(final String request, final P payload, final String requestType, final Map headers, final Class clazz) throws UnauthorizedException, GeneralException, NotFoundException { if (request == null) { throw new IllegalArgumentException(REQUEST_VALUE_MUST_BE_SPECIFIED); } String url = request; if (!isURLAbsolute(url)) { url = serviceUrl + url; } final APIResponse apiResponse = doRequest(requestType, url, headers, payload); final String body = apiResponse.getBody(); final int status = apiResponse.getStatus(); if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED || status == HttpURLConnection.HTTP_ACCEPTED) { try { final ObjectMapper mapper = new ObjectMapper(); // If we as new properties, we don't want the system to fail, we rather want to ignore them mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // Enable case insensitivity to avoid parsing errors if parameters' case in api response doesn't match sdk's mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); // Prevents mismatched exception when clazz is null return clazz == null ? null : this.readValue(mapper, body, clazz); } catch (IOException ioe) { throw new GeneralException(ioe); } } else if (status == HttpURLConnection.HTTP_NO_CONTENT) { return null; // no content doesn't mean an error } handleHttpFailStatuses(status, body); return null; } // todo: need to refactor for duplicated code. public List getJsonDataAsList(final String request, final P payload, final String requestType, final Map headers, final Class elementClass) throws UnauthorizedException, GeneralException, NotFoundException { if (request == null) { throw new IllegalArgumentException(REQUEST_VALUE_MUST_BE_SPECIFIED); } String url = request; if (!isURLAbsolute(url)) { url = serviceUrl + url; } final APIResponse apiResponse = doRequest(requestType, url, headers, payload); final String body = apiResponse.getBody(); final int status = apiResponse.getStatus(); if (status == HttpURLConnection.HTTP_OK || status == HttpURLConnection.HTTP_CREATED || status == HttpURLConnection.HTTP_ACCEPTED) { try { final ObjectMapper mapper = new ObjectMapper(); // If we as new properties, we don't want the system to fail, we rather want to ignore them mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // Enable case insensitivity to avoid parsing errors if parameters' case in api response doesn't match sdk's mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS); // Prevents mismatched exception when clazz is null return this.readValueAsList(mapper, body, elementClass); } catch (IOException ioe) { throw new GeneralException(ioe); } } else if (status == HttpURLConnection.HTTP_NO_CONTENT) { return Collections.emptyList(); // no content doesn't mean an error } handleHttpFailStatuses(status, body); return Collections.emptyList(); } private T readValue(ObjectMapper mapper, String content, Class clazz) throws JsonProcessingException { return mapper.readValue(content, clazz); } private List readValueAsList(ObjectMapper mapper, String content, final Class elementClass) throws JsonProcessingException { return mapper.readValue(content, mapper.getTypeFactory().constructCollectionType(List.class, elementClass)); } private void handleHttpFailStatuses(final int status, String body) throws UnauthorizedException, NotFoundException, GeneralException { if (status == HttpURLConnection.HTTP_UNAUTHORIZED) { final List errorReport = getErrorReportOrNull(body); throw new UnauthorizedException(NOT_AUTHORISED_MSG, errorReport); } else if (status >= 400 && status < 500) { // Any code in the 400 range will have a list of error codes attached final List errorReport = getErrorReportOrNull(body); if (status == HttpURLConnection.HTTP_NOT_FOUND) { throw new NotFoundException(errorReport); } throw new GeneralException(FAILED_DATA_RESPONSE_CODE + status, status, errorReport); } else { throw new GeneralException(FAILED_DATA_RESPONSE_CODE + status, status); } } /** * Actually sends a HTTP request and returns its body and HTTP status code. * * @param method HTTP method. * @param url Absolute URL. * @param headers additional headers to set on the request. * @param payload Payload to JSON encode for the request body. May be null. * @param

Type of the payload. * @return APIResponse containing the response's body and status. */

APIResponse doRequest(final String method, final String url, final Map headers, final P payload) throws GeneralException { HttpURLConnection connection = null; InputStream inputStream = null; if (METHOD_PATCH.equalsIgnoreCase(method)) { // It'd perhaps be cleaner to call this in the constructor, but // we'd then need to throw GeneralExceptions from there. This means // it wouldn't be possible to declare AND initialize _instance_ // fields of MessageBirdServiceImpl at the same time. This method // already throws this exception, so now we don't have to pollute // our public API further. allowPatchRequestsIfNeeded(); } try { connection = getConnection(url, payload, method, headers); int status = connection.getResponseCode(); if (APIResponse.isSuccessStatus(status)) { inputStream = connection.getInputStream(); } else { inputStream = connection.getErrorStream(); } return new APIResponse(readToEnd(inputStream), status); } catch (IOException ioe) { throw new GeneralException(ioe); } finally { saveClose(inputStream); if (connection != null) { connection.disconnect(); } } } /** * * Do get request for file from input url and stores the file in filepath. * @param url Absolute URL. * @param filePath the path where the downloaded file is going to be stored. * @return if it succeed, it returns filepath otherwise null or exception. */ private String doGetRequestForFileAndStore(final String url, final String filePath) throws GeneralException, UnauthorizedException, NotFoundException { HttpURLConnection connection = null; InputStream inputStream = null; try { connection = getConnection(url, null, METHOD_GET); int status = connection.getResponseCode(); if (APIResponse.isSuccessStatus(status)) { inputStream = connection.getInputStream(); } else { inputStream = connection.getErrorStream(); if (inputStream == null) { throw new GeneralException("Error stream was empty"); } } if (status == HttpURLConnection.HTTP_OK) { return writeInputStreamToFile(inputStream, filePath); } String body = readToEnd(inputStream); handleHttpFailStatuses(status, body); } catch (IOException ioe) { throw new GeneralException(ioe); } finally { saveClose(inputStream); if (connection != null) { connection.disconnect(); } } return null; } /** * Writes input stream from IO to filepath. * @param inputStream stream that has been collected file input * @param filepath the storage path for the file * @return if it succeed, it returns filepath otherwise null or exception. * @throws IOException */ private String writeInputStreamToFile(InputStream inputStream, String filepath) throws IOException { // opens an output stream to save into file FileOutputStream outputStream = new FileOutputStream(filepath); int bytesRead = -1; byte[] buffer = new byte[BUFFER_SIZE]; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } outputStream.close(); return filepath; } /** * By default, HttpURLConnection does not support PATCH requests. We can * however work around this with reflection. Many thanks to okutane on * StackOverflow: https://stackoverflow.com/a/46323891/3521243. */ private synchronized static void allowPatchRequestsIfNeeded() throws GeneralException { if (isPatchRequestAllowed) { // Don't do anything if we've run this method before. We're in a // synchronized block, so return ASAP. return; } try { // Ensure we can access the fields we need to set. Field methodsField = HttpURLConnection.class.getDeclaredField("methods"); methodsField.setAccessible(true); Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL); Object noInstanceBecauseStaticField = null; // Determine what methods should be allowed. String[] existingMethods = (String[]) methodsField.get(noInstanceBecauseStaticField); String[] allowedMethods = getAllowedMethods(existingMethods); // Override the actual field to allow PATCH. methodsField.set(noInstanceBecauseStaticField, allowedMethods); // Set flag so we only have to run this once. isPatchRequestAllowed = true; } catch (IllegalAccessException | NoSuchFieldException e) { throw new GeneralException(CAN_NOT_ALLOW_PATCH); } } /** * Appends PATCH to the provided array. * * @param existingMethods Methods that are, and must be, allowed. * @return New array also containing PATCH. */ private static String[] getAllowedMethods(String[] existingMethods) { int listCapacity = existingMethods.length + 1; List allowedMethods = new ArrayList<>(listCapacity); allowedMethods.addAll(Arrays.asList(existingMethods)); allowedMethods.add(METHOD_PATCH); allowedMethods.add(METHOD_PUT); return allowedMethods.toArray(new String[0]); } /** * Reads the stream until it has no more bytes and returns a UTF-8 encoded * string representation. * * @param inputStream Stream to read from. * @return UTF-8 encoded string representation of stream's contents. */ private String readToEnd(InputStream inputStream) { Scanner scanner = new Scanner(inputStream).useDelimiter("\\A"); return scanner.hasNext() ? scanner.next() : ""; } /** * Attempts determining whether the provided URL is an absolute one, based on the scheme. * * @param url provided url * @return boolean */ private boolean isURLAbsolute(String url) { for (String protocol : PROTOCOLS) { if (url.startsWith(protocol)) { return true; } } return false; } /** * Create a HttpURLConnection connection object * * @param serviceUrl URL that needs to be requested * @param body body could not be empty for POST or PUT requests * @param requestType Request type POST requests without a payload will generate a exception * @return base class * @throws IOException io exception */ public

HttpURLConnection getConnection(final String serviceUrl, final P body, final String requestType) throws IOException { return getConnection(serviceUrl, body, requestType, new HashMap<>()); } /** * Create a HttpURLConnection connection object * * @param serviceUrl URL that needs to be requested * @param body body could not be empty for POST or PUT requests * @param requestType Request type POST requests without a payload will generate a exception * @param headers additional headers to set on the request * @return base class * @throws IOException io exception */ public

HttpURLConnection getConnection(final String serviceUrl, final P body, final String requestType, final Map headers) throws IOException { if (requestType == null || !REQUEST_METHODS.contains(requestType)) { throw new IllegalArgumentException(String.format(REQUEST_METHOD_NOT_ALLOWED, requestType)); } if (body == null && ("POST".equals(requestType) || "PUT".equals(requestType))) { throw new IllegalArgumentException("Empty body is not allowed for POST or PUT requests"); } final URL restService = new URL(serviceUrl); final HttpURLConnection connection; if (proxy != null) { connection = (HttpURLConnection) restService.openConnection(proxy); } else { connection = (HttpURLConnection) restService.openConnection(); } connection.setDoInput(true); connection.setRequestProperty("Accept", "application/json"); connection.setUseCaches(false); connection.setRequestProperty("charset", "utf-8"); connection.setRequestProperty("Connection", "close"); connection.setRequestProperty("Authorization", "AccessKey " + accessKey); connection.setRequestProperty("User-agent", userAgentString); if ("POST".equals(requestType) || "PUT".equals(requestType) || "PATCH".equals(requestType)) { connection.setRequestMethod(requestType); connection.setDoOutput(true); connection.setRequestProperty("Content-Type", "application/json"); ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(Include.NON_NULL); // Specifically set the date format for POST requests so scheduled // messages and other things relying on specific date formats don't // fail when sending. DateFormat df = getDateFormat(); mapper.setDateFormat(df); setAdditionalHeaders(connection, headers); byte[] bodyBytes; if (body instanceof byte[]) { bodyBytes = (byte[]) body; } else { final String json = mapper.writeValueAsString(body); bodyBytes = json.getBytes(StandardCharsets.UTF_8); } connection.getOutputStream().write(bodyBytes); } else if ("DELETE".equals(requestType)) { // could have just used rquestType as it is connection.setDoOutput(false); connection.setRequestMethod("DELETE"); connection.setRequestProperty("Content-Type", "text/plain"); setAdditionalHeaders(connection, headers); } else { connection.setDoOutput(false); connection.setRequestMethod("GET"); connection.setRequestProperty("Content-Type", "text/plain"); setAdditionalHeaders(connection, headers); } return connection; } private void setAdditionalHeaders(HttpURLConnection connection, Map headers) { for (Map.Entry header : headers.entrySet()) { connection.setRequestProperty(header.getKey(), header.getValue()); } } private DateFormat getDateFormat() { ComparableVersion java6 = new ComparableVersion("1.6"); if (JAVA_VERSION.compareTo(java6) > 0) { return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); } return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZZZ"); } /** * Get the MessageBird error report data. * * @param body Raw response body. * @return Error report, or null if the body can not be deserialized. */ private List getErrorReportOrNull(final String body) { ObjectMapper objectMapper = new ObjectMapper(); try { JsonNode jsonNode = objectMapper.readValue(body, JsonNode.class); if(!jsonNode.has("errors")) { return null; } ErrorReport[] errors = objectMapper.readValue(jsonNode.get("errors").toString(), ErrorReport[].class); List result = Arrays.asList(errors); if (result.isEmpty()) { return null; } return result; } catch (IOException e) { return null; } } /** * Get the used access key * * @return String */ public String getAccessKey() { return accessKey; } /** * get the used service URL * * @return String */ public String getServiceUrl() { return serviceUrl; } /** * Get the client version * * @return String */ public String getClientVersion() { return clientVersion; } /** * Get the user agent string * * @return String */ public String getUserAgentString() { return userAgentString; } /** * Enable proxy support * example: * Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.0.0.1", 8080)); * messageBirdService.setProxy(proxy); * * @param proxy proxy */ public void setProxy(Proxy proxy) { this.proxy = proxy; } /** * Get the proxy object if set * * @return Proxy */ public Proxy getProxy() { return proxy; } /** * Safe-close a input stream * * @param is input stream */ private void saveClose(final InputStream is) { if (is != null) { try { is.close(); } catch (IOException e) { // Do nothing } } } /** * Encodes a key/value pair with percent encoding. * * @param key the key name to be used * @param value the value to be assigned to that key * @return String */ private String encodeKeyValuePair(String key, Object value) throws UnsupportedEncodingException { return URLEncoder.encode(key, String.valueOf(StandardCharsets.UTF_8)) + "=" + URLEncoder.encode(String.valueOf(value), String.valueOf(StandardCharsets.UTF_8)); } /** * Build a path variable for GET requests * * @param map map for getting path variables * @return String */ private String getPathVariables(final Map map) { final StringBuilder bpath = new StringBuilder(); for (Map.Entry param : map.entrySet()) { if (bpath.length() > 1) { bpath.append("&"); } try { // Check to see if the value is a Collection if (param.getValue() instanceof Collection) { // If it is, cast the value as a Collection explicitly // so it can be iterated over. Its values should be // appended to the querystring parameters using the // original key provided (e.g., ?features=sms&features=mms) Collection col = (Collection) param.getValue(); Iterator iterator = col.iterator(); int count = 0; // While there are still remaining iterables while (iterator.hasNext()) { // Append & if not the first iterable if (count > 0) { bpath.append("&"); } // Append the encoded querystring key/value pair. // the value is returned from the next() call bpath.append(encodeKeyValuePair(param.getKey(), iterator.next())); count++; } } else { // If the value is not a collection, create the querystring value directly. bpath.append(encodeKeyValuePair(param.getKey(), param.getValue())); } } catch (UnsupportedEncodingException exception) { // Do nothing } } return bpath.toString(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy