package net.billforward;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Type;
import java.net.URL;
import java.net.URLEncoder;
import java.net.URLStreamHandler;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Scanner;
import java.util.TimeZone;
import net.billforward.exception.APIConnectionException;
import net.billforward.exception.APIException;
import net.billforward.exception.AuthenticationException;
import net.billforward.exception.CardException;
import net.billforward.exception.InvalidRequestException;
import net.billforward.gson.typeadapters.RuntimeTypeAdapterFactory;
import net.billforward.model.APIResponse;
import net.billforward.model.amendments.Amendment;
import net.billforward.model.gateways.APIConfiguration;
import net.billforward.model.gateways.GatewayTypeMapping;
import net.billforward.model.notifications.Notification;
import net.billforward.net.BillForwardResponse;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.annotations.Expose;
@SuppressWarnings({ "unchecked", "rawtypes" })
public class BillForwardClient
{
public String apiKey;
public String apiUrl;
public final static String CHARSET = "UTF-8";
private static final String DNS_CACHE_TTL_PROPERTY_NAME = "networkaddress.cache.ttl";
private boolean verifySSL = false;
protected static BillForwardClient defaultClient;
public static BillForwardClient getClient() throws APIException {
if(defaultClient == null) {
throw new APIException("Please set a valid BillForward client using BillForwardClient.makeDefaultClient(...) before calling methods", null);
}
return defaultClient;
}
public static void setDefaultClient(BillForwardClient client_) {
defaultClient = client_;
}
public static BillForwardClient makeDefaultClient(String apiKey_, String apiUrl_) {
defaultClient = new BillForwardClient();
defaultClient.setAPIKey(apiKey_);
defaultClient.setAPIUrl(apiUrl_);
return defaultClient;
}
/**
* (FOR TESTING ONLY)
* Only disable SSL verification if you're using your own (mocked) server.
* Disabling verification on billforward.net is not supported
*/
public void setVerifySSL(boolean verify) {
verifySSL = verify;
}
public boolean getVerifySSL() {
return verifySSL;
}
public String getApiUrl() {
return apiUrl;
}
public static Gson GSON;
public static Gson GSON_NOTIFICATION_ENTITY;
static {
/*
* This is to support polymorphism in the different type of API Configurations
*/
RuntimeTypeAdapterFactory<APIConfiguration> apiConfigAdapter = RuntimeTypeAdapterFactory.of(APIConfiguration.class, "@type");
GatewayTypeMapping[] mappings = APIConfiguration.getTypeMappings();
for(GatewayTypeMapping mapping : mappings) {
apiConfigAdapter.registerSubtype((Class)mapping.getApiType(), mapping.getName());
}
/*
* This is to support polymorphism in the different type of Amendments from API
*/
RuntimeTypeAdapterFactory<Amendment> amendmentConfigAdapter = RuntimeTypeAdapterFactory.of(Amendment.class, "@type");
mappings = Amendment.getTypeMappings();
for(GatewayTypeMapping mapping : mappings) {
amendmentConfigAdapter.registerSubtype((Class)mapping.getApiType(), mapping.getName());
}
/*
* This is to support polymorphism in the different type of Amendments from Notifications
*/
RuntimeTypeAdapterFactory<Amendment> amendmentNotifcationConfigAdapter = RuntimeTypeAdapterFactory.of(Amendment.class, "type");
mappings = Amendment.getTypeMappings();
for(GatewayTypeMapping mapping : mappings) {
amendmentNotifcationConfigAdapter.registerSubtype((Class)mapping.getApiType(), mapping.getName());
}
/*
* This is to support polymorphism in the different type of Notifications
*/
RuntimeTypeAdapterFactory<Notification> notificationConfigAdapter = RuntimeTypeAdapterFactory.of(Notification.class, "domain");
mappings = Notification.getTypeMappings();
for(GatewayTypeMapping mapping : mappings) {
if(mapping.getName() == null) {
notificationConfigAdapter.registerSubtype((Class)mapping.getApiType(), "default_value_mapping");
} else {
notificationConfigAdapter.registerSubtype((Class)mapping.getApiType(), mapping.getName());
}
}
//2014-09-12T03:00:17Z
//.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
GSON = new GsonBuilder()
.registerTypeAdapter(Date.class, new DateTypeAdapter())
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapterFactory(apiConfigAdapter)
.registerTypeAdapterFactory(amendmentConfigAdapter)
.registerTypeAdapterFactory(notificationConfigAdapter)
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.create();
GSON_NOTIFICATION_ENTITY = new GsonBuilder()
.registerTypeAdapter(Date.class, new DateTypeAdapter())
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapterFactory(apiConfigAdapter)
.registerTypeAdapterFactory(amendmentNotifcationConfigAdapter)
.registerTypeAdapterFactory(notificationConfigAdapter)
.setFieldNamingPolicy(FieldNamingPolicy.IDENTITY)
.create();
}
private static class DateTypeAdapter implements JsonSerializer<Date>, JsonDeserializer<Date> {
private final DateFormat dateFormat;
private DateTypeAdapter() {
dateFormat = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
}
public Date deserialize(JsonElement jsonElement, Type arg1, JsonDeserializationContext arg2) throws JsonParseException {
Date date = null;
try {
date = dateFormat.parse(jsonElement.getAsString());
} catch (ParseException e) {
}
return date;
}
public JsonElement serialize(Date date, Type arg1, JsonSerializationContext arg2) {
String dateFormatAsString = dateFormat.format(date);
return new JsonPrimitive(dateFormatAsString);
}
}
/*
* Set this property to override your environment's default
* URLStreamHandler; Settings the property should not be needed in most
* environments.
*/
private static final String CUSTOM_URL_STREAM_HANDLER_PROPERTY_NAME = "net.billforward.net.customURLStreamHandler";
public enum RequestMethod {
GET, POST, DELETE, PUT
}
@SuppressWarnings("unused")
private static String urlEncode(String str) throws UnsupportedEncodingException {
// Preserve original behavior that passing null for an object id will lead
// to us actually making a request to /v1/foo/null
if (str == null) {
return null;
}
else {
return URLEncoder.encode(str, CHARSET);
}
}
private Map<String, String> getHeaders(String apiKey) {
Map<String, String> headers = new HashMap<String, String>();
headers.put("Accept-Charset", CHARSET);
headers.put("User-Agent",
String.format("BillForward/JavaBindings/%s", apiUrl));
headers.put("Authorization", String.format("Bearer %s", apiKey));
// debug headers
String[] propertyNames = { "os.name", "os.version", "os.arch",
"java.version", "java.vendor", "java.vm.version",
"java.vm.vendor" };
Map<String, String> propertyMap = new HashMap<String, String>();
for (String propertyName : propertyNames) {
propertyMap.put(propertyName, System.getProperty(propertyName));
}
propertyMap.put("lang", "Java");
propertyMap.put("publisher", "BillForward");
headers.put("X-BillForward-Client-User-Agent", GSON.toJson(propertyMap));
return headers;
}
private java.net.HttpURLConnection createBillForwardConnection(String url, String apiKey) throws IOException {
URL BillForwardURL = null;
String customURLStreamHandlerClassName = System.getProperty(CUSTOM_URL_STREAM_HANDLER_PROPERTY_NAME, null);
if (customURLStreamHandlerClassName != null) {
// instantiate the custom handler provided
try {
Class<URLStreamHandler> clazz = (Class<URLStreamHandler>) Class.forName(customURLStreamHandlerClassName);
Constructor<URLStreamHandler> constructor = clazz.getConstructor();
URLStreamHandler customHandler = constructor.newInstance();
BillForwardURL = new URL(null, url, customHandler);
} catch (ClassNotFoundException e) {
throw new IOException(e);
} catch (SecurityException e) {
throw new IOException(e);
} catch (NoSuchMethodException e) {
throw new IOException(e);
} catch (IllegalArgumentException e) {
throw new IOException(e);
} catch (InstantiationException e) {
throw new IOException(e);
} catch (IllegalAccessException e) {
throw new IOException(e);
} catch (InvocationTargetException e) {
throw new IOException(e);
}
} else {
BillForwardURL = new URL(url);
}
java.net.HttpURLConnection conn = (java.net.HttpURLConnection) BillForwardURL.openConnection();
conn.setConnectTimeout(30 * 1000);
conn.setReadTimeout(80 * 1000);
conn.setUseCaches(false);
for (Map.Entry<String, String> header : getHeaders(apiKey).entrySet()) {
conn.setRequestProperty(header.getKey(), header.getValue());
}
return conn;
}
private static String formatURL(String url, String query) {
if (query == null || query.isEmpty()) {
return url;
} else {
// In some cases, URL can already contain a question mark (eg, upcoming invoice lines)
String separator = url.contains("?") ? "&" : "?";
return String.format("%s%s%s", url, separator, query);
}
}
private java.net.HttpURLConnection createGetConnection(String url, String query, String apiKey) throws IOException, APIConnectionException {
String getURL = formatURL(url, query);
java.net.HttpURLConnection conn = createBillForwardConnection(getURL, apiKey);
conn.setRequestMethod("GET");
return conn;
}
private java.net.HttpURLConnection createPostConnection(String url, String query, String apiKey) throws IOException, APIConnectionException {
java.net.HttpURLConnection conn = createBillForwardConnection(url, apiKey);
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", String.format("application/json;charset=%s", CHARSET));
OutputStream output = null;
try {
output = conn.getOutputStream();
output.write(query.getBytes(CHARSET));
} finally {
if (output != null) {
output.close();
}
}
return conn;
}
private java.net.HttpURLConnection createPutConnection(String url, String query, String apiKey) throws IOException, APIConnectionException {
java.net.HttpURLConnection conn = createBillForwardConnection(url, apiKey);
conn.setDoOutput(true);
conn.setRequestMethod("PUT");
conn.setRequestProperty("Content-Type", String.format("application/json;charset=%s", CHARSET));
OutputStream output = null;
try {
output = conn.getOutputStream();
output.write(query.getBytes(CHARSET));
} finally {
if (output != null) {
output.close();
}
}
return conn;
}
private java.net.HttpURLConnection createDeleteConnection(String url, String query, String apiKey) throws IOException, APIConnectionException {
String deleteUrl = url; //formatURL(url, query);
java.net.HttpURLConnection conn = createBillForwardConnection(deleteUrl, apiKey);
//iansau: FYI: java < 1.8 does not support POST body on delete, sad times.
conn.setRequestMethod("DELETE");
return conn;
}
// represents Errors returned as JSON
private class Error {
@Expose String errorType;
@Expose String errorMessage;
public String getErrorParameters() {
return errorMessage;
}
}
private static String getResponseBody(InputStream responseStream)
throws IOException {
//\A is the beginning of
// the stream boundary
@SuppressWarnings("resource")
String rBody = new Scanner(responseStream, CHARSET).useDelimiter("\\A").next(); //
responseStream.close();
return rBody;
}
private BillForwardResponse makeURLConnectionRequest(RequestMethod method, String url, String query, String apiKey) throws APIConnectionException {
java.net.HttpURLConnection conn = null;
try {
switch (method) {
case GET:
conn = createGetConnection(url, query, apiKey);
break;
case POST:
conn = createPostConnection(url, query, apiKey);
break;
case PUT:
conn = createPutConnection(url, query, apiKey);
break;
case DELETE:
conn = createDeleteConnection(url, query, apiKey);
break;
default:
throw new APIConnectionException(
String.format(
"Unrecognized HTTP method %s. "
+ "This indicates a bug in the BillForward bindings. Please contact "
+ "support@BillForward.net for assistance.",
method));
}
// trigger the request
int rCode = conn.getResponseCode();
String rBody = null;
Map<String, List<String>> headers;
if (rCode >= 200 && rCode < 300) {
rBody = getResponseBody(conn.getInputStream());
} else {
rBody = getResponseBody(conn.getErrorStream());
}
headers = conn.getHeaderFields();
return new BillForwardResponse(rCode, rBody, headers);
} catch (IOException e) {
throw new APIConnectionException(
String.format(
"IOException during API request to BillForward (%s): %s "
+ "Please check your internet connection and try again. If this problem persists,"
+ "you should check BillForward's service status at https://twitter.com/BillForwardstatus,"
+ " or let us know at support@BillForward.net.",
getApiUrl(), e.getMessage()), e);
} finally {
if (conn != null) {
conn.disconnect();
}
}
}
public <TRequest, TResponse> TResponse requestUntyped(RequestMethod method, String url, TRequest obj, Type responseType) throws AuthenticationException,
InvalidRequestException,
APIConnectionException,
CardException,
APIException {
url = String.format("%s/%s", apiUrl, url);
String originalDNSCacheTTL = null;
Boolean allowedToSetTTL = true;
try {
originalDNSCacheTTL = java.security.Security.getProperty(DNS_CACHE_TTL_PROPERTY_NAME);
// disable DNS cache
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, "0");
} catch (SecurityException se) {
allowedToSetTTL = false;
}
try {
return _requestUntyped(responseType, method, url, obj, apiKey);
} finally {
if (allowedToSetTTL) {
if (originalDNSCacheTTL == null) {
// value unspecified by implementation
// DNS_CACHE_TTL_PROPERTY_NAME of -1 = cache forever
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, "-1");
} else {
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, originalDNSCacheTTL);
}
}
}
}
public <T> APIResponse<T> request(RequestMethod method, String url, T obj, Type responseType) throws AuthenticationException,
InvalidRequestException,
APIConnectionException,
CardException,
APIException {
url = String.format("%s/%s", apiUrl, url);
String originalDNSCacheTTL = null;
Boolean allowedToSetTTL = true;
try {
originalDNSCacheTTL = java.security.Security.getProperty(DNS_CACHE_TTL_PROPERTY_NAME);
// disable DNS cache
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, "0");
} catch (SecurityException se) {
allowedToSetTTL = false;
}
try {
return _request(responseType, method, url, obj, apiKey);
} finally {
if (allowedToSetTTL) {
if (originalDNSCacheTTL == null) {
// value unspecified by implementation
// DNS_CACHE_TTL_PROPERTY_NAME of -1 = cache forever
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, "-1");
} else {
java.security.Security.setProperty(DNS_CACHE_TTL_PROPERTY_NAME, originalDNSCacheTTL);
}
}
}
}
protected <TRequest, TResponse> TResponse _requestUntyped(Type responseType, RequestMethod method, String url, TRequest obj, String apiKey) throws AuthenticationException,
InvalidRequestException, APIConnectionException, CardException,
APIException {
if ((apiKey == null || apiKey.length() == 0) && (apiKey == null || apiKey.length() == 0)) {
throw new AuthenticationException(
"No API key provided. (HINT: set your API key using 'BillForward.apiKey = <API-KEY>'. "
+ "You can generate API keys from the BillForward web interface. "
+ "See https://BillForward.com/api for details or email support@BillForward.net if you have questions.");
}
String query = "";
if(obj != null) {
query = GSON.toJson(obj);
}
BillForwardResponse response = makeURLConnectionRequest(method, url, query, apiKey);
int rCode = response.getResponseCode();
String rBody = response.getResponseBody();
if (rCode < 200 || rCode >= 300) {
handleAPIError(rBody, rCode);
}
TResponse acc = GSON.fromJson(rBody, responseType);
return acc;
}
protected <T> APIResponse<T> _request(Type responseType, RequestMethod method, String url, T obj, String apiKey) throws AuthenticationException,
InvalidRequestException, APIConnectionException, CardException,
APIException {
if ((apiKey == null || apiKey.length() == 0) && (apiKey == null || apiKey.length() == 0)) {
throw new AuthenticationException(
"No API key provided. (HINT: set your API key using 'BillForward.apiKey = <API-KEY>'. "
+ "You can generate API keys from the BillForward web interface. "
+ "See https://BillForward.com/api for details or email support@BillForward.net if you have questions.");
}
String query = "";
if(obj != null) {
query = GSON.toJson(obj);
}
BillForwardResponse response = makeURLConnectionRequest(method, url, query, apiKey);
int rCode = response.getResponseCode();
String rBody = response.getResponseBody();
if (rCode < 200 || rCode >= 300) {
handleAPIError(rBody, rCode);
}
APIResponse<T> acc = GSON.fromJson(rBody, responseType);
return (APIResponse<T>)acc;
}
private void handleAPIError(String rBody, int rCode)
throws InvalidRequestException, AuthenticationException,
CardException, APIException {
Error error = GSON.fromJson(rBody, Error.class);
switch (rCode) {
case 400:
throw new InvalidRequestException(error.errorMessage, error.getErrorParameters(), null);
case 404:
throw new InvalidRequestException(error.errorMessage, error.getErrorParameters(), null);
case 401:
throw new AuthenticationException(error.errorMessage);
case 402:
throw new CardException(error.errorMessage, error.getErrorParameters(), error.getErrorParameters(), null);
default:
throw new APIException(error.getErrorParameters(), null);
}
}
public void setAPIKey(String apiKey_) {
apiKey = apiKey_;
}
public void setAPIUrl(String apiUrl_) {
apiUrl = apiUrl_;
}
public BillForwardClient() {
}
}