package com.lob.client;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.joda.JodaModule;
import com.google.common.base.Optional;
import com.google.common.escape.Escaper;
import com.google.common.net.UrlEscapers;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.SettableFuture;
import com.lob.BooleanDeserializer;
import com.lob.Lob;
import com.lob.LobApiException;
import com.lob.MoneyDeserializer;
import com.lob.id.*;
import com.lob.protocol.request.*;
import com.lob.protocol.response.*;
import com.ning.http.client.AsyncCompletionHandler;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClient.BoundRequestBuilder;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.AsyncHttpClientConfig.Builder;
import com.ning.http.client.multipart.FilePart;
import com.ning.http.client.FluentStringsMap;
import com.ning.http.client.Realm;
import com.ning.http.client.Realm.AuthScheme;
import com.ning.http.client.Response;
import com.ning.http.client.multipart.StringPart;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import java.io.IOException;
import java.util.Collection;
import java.util.Map;
import java.util.Properties;
import static com.google.common.base.Preconditions.*;
public class AsyncLobClient implements LobClient {
private final static ObjectMapper MAPPER = new ObjectMapper()
.registerModule(new JodaModule())
.registerModule(new SimpleModule()
.addDeserializer(Money.class, new MoneyDeserializer(CurrencyUnit.USD))
.addDeserializer(Boolean.class, new BooleanDeserializer())
)
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private final AsyncHttpClient httpClient;
private final String baseUrl;
private final static String JAVA_VERSION = System.getProperty("java.version");
private AsyncLobClient(
final AsyncHttpClient httpClient,
final String baseUrl) {
this.httpClient = httpClient;
this.baseUrl = baseUrl;
}
private static AsyncHttpClientConfig commonSetup(final String apiKey, final AsyncHttpClientConfig.Builder configBuilder) {
final Realm realm = new Realm.RealmBuilder()
.setPrincipal(checkNotNull(apiKey))
.setUsePreemptiveAuth(true)
.setScheme(AuthScheme.BASIC)
.build();
configBuilder.setRealm(realm);
return configBuilder.build();
}
public static LobClient createDefault(final String apiKey) {
return new AsyncLobClient(
new AsyncHttpClient(commonSetup(apiKey, new Builder())),
Lob.getBaseUrl());
}
public static LobClient create(final String apiKey, final AsyncHttpClientConfig config) {
return new AsyncLobClient(
new AsyncHttpClient(commonSetup(apiKey, new Builder(config))),
Lob.getBaseUrl());
}
@Override
public void close() {
this.httpClient.close();
}
@Override
public void closeAsynchronously() {
this.httpClient.closeAsynchronously();
}
@Override
public ListenableFuture<AddressResponse> createAddress(final AddressRequest addressRequest) {
return execute(AddressResponse.class, post(Router.ADDRESSES, addressRequest));
}
@Override
public ListenableFuture<AddressResponse> getAddress(final AddressId id) {
return execute(AddressResponse.class, get(Router.ADDRESSES, id));
}
@Override
public ListenableFuture<AddressResponseList> getAddresses() {
return execute(AddressResponseList.class, get(Router.ADDRESSES));
}
@Override
public ListenableFuture<AddressResponseList> getAddresses(final int count) {
return execute(AddressResponseList.class, get(Router.ADDRESSES, count));
}
@Override
public ListenableFuture<AddressResponseList> getAddresses(final int count, final int offset) {
return execute(AddressResponseList.class, get(Router.ADDRESSES, count, offset));
}
@Override
public ListenableFuture<AddressResponseList> getAddresses(final Filter filter) {
return execute(AddressResponseList.class, get(Router.ADDRESSES, filter));
}
@Override
public ListenableFuture<AddressDeleteResponse> deleteAddress(final AddressId id) {
return execute(AddressDeleteResponse.class, delete(Router.ADDRESSES, id));
}
@Override
public ListenableFuture<LetterResponse> createLetter(final LetterRequest letterRequest) {
return execute(LetterResponse.class, post(Router.LETTERS, letterRequest));
}
@Override
public ListenableFuture<LetterResponse> getLetter(final LetterId id) {
return execute(LetterResponse.class, get(Router.LETTERS, id));
}
@Override
public ListenableFuture<LetterResponseList> getLetters() {
return execute(LetterResponseList.class, get(Router.LETTERS));
}
@Override
public ListenableFuture<LetterResponseList> getLetters(final int count) {
return execute(LetterResponseList.class, get(Router.LETTERS, count));
}
@Override
public ListenableFuture<LetterResponseList> getLetters(final int count, final int offset) {
return execute(LetterResponseList.class, get(Router.LETTERS, count, offset));
}
@Override
public ListenableFuture<LetterResponseList> getLetters(final Filter filter) {
return execute(LetterResponseList.class, get(Router.LETTERS, filter));
}
@Override
public ListenableFuture<LetterDeleteResponse> deleteLetter(final LetterId id) {
return execute(LetterDeleteResponse.class, delete(Router.LETTERS, id));
}
@Override
public ListenableFuture<PostcardResponse> createPostcard(final PostcardRequest postcardRequest) {
return execute(PostcardResponse.class, post(Router.POSTCARDS, postcardRequest));
}
@Override
public ListenableFuture<PostcardResponse> getPostcard(final PostcardId id) {
return execute(PostcardResponse.class, get(Router.POSTCARDS, id));
}
@Override
public ListenableFuture<PostcardResponseList> getPostcards() {
return execute(PostcardResponseList.class, get(Router.POSTCARDS));
}
@Override
public ListenableFuture<PostcardResponseList> getPostcards(final int count) {
return execute(PostcardResponseList.class, get(Router.POSTCARDS, count));
}
@Override
public ListenableFuture<PostcardResponseList> getPostcards(final int count, final int offset) {
return execute(PostcardResponseList.class, get(Router.POSTCARDS, count, offset));
}
@Override
public ListenableFuture<PostcardResponseList> getPostcards(final Filter filter) {
return execute(PostcardResponseList.class, get(Router.POSTCARDS, filter));
}
@Override
public ListenableFuture<PostcardDeleteResponse> deletePostcard(final PostcardId id) {
return execute(PostcardDeleteResponse.class, delete(Router.POSTCARDS, id));
}
@Override
public ListenableFuture<CheckResponse> createCheck(final CheckRequest checkRequest) {
return execute(CheckResponse.class, post(Router.CHECKS, checkRequest));
}
@Override
public ListenableFuture<CheckResponse> getCheck(final CheckId id) {
return execute(CheckResponse.class, get(Router.CHECKS, id));
}
@Override
public ListenableFuture<CheckResponseList> getChecks() {
return execute(CheckResponseList.class, get(Router.CHECKS));
}
@Override
public ListenableFuture<CheckResponseList> getChecks(final int count) {
return execute(CheckResponseList.class, get(Router.CHECKS, count));
}
@Override
public ListenableFuture<CheckResponseList> getChecks(final int count, final int offset) {
return execute(CheckResponseList.class, get(Router.CHECKS, count, offset));
}
@Override
public ListenableFuture<CheckResponseList> getChecks(final Filter filter) {
return execute(CheckResponseList.class, get(Router.CHECKS, filter));
}
@Override
public ListenableFuture<CheckDeleteResponse> deleteCheck(final CheckId id) {
return execute(CheckDeleteResponse.class, delete(Router.CHECKS, id));
}
@Override
public ListenableFuture<BankAccountResponse> createBankAccount(final BankAccountRequest bankAccountRequest) {
return execute(BankAccountResponse.class, post(Router.BANK_ACCOUNTS, bankAccountRequest));
}
@Override
public ListenableFuture<BankAccountResponse> getBankAccount(final BankAccountId id) {
return execute(BankAccountResponse.class, get(Router.BANK_ACCOUNTS, id));
}
@Override
public ListenableFuture<BankAccountResponseList> getBankAccounts() {
return execute(BankAccountResponseList.class, get(Router.BANK_ACCOUNTS));
}
@Override
public ListenableFuture<BankAccountResponseList> getBankAccounts(final int count) {
return execute(BankAccountResponseList.class, get(Router.BANK_ACCOUNTS, count));
}
@Override
public ListenableFuture<BankAccountResponseList> getBankAccounts(final int count, final int offset) {
return execute(BankAccountResponseList.class, get(Router.BANK_ACCOUNTS, count, offset));
}
@Override
public ListenableFuture<BankAccountResponseList> getBankAccounts(final Filter filter) {
return execute(BankAccountResponseList.class, get(Router.BANK_ACCOUNTS, filter));
}
@Override
public ListenableFuture<BankAccountDeleteResponse> deleteBankAccount(final BankAccountId id) {
return execute(BankAccountDeleteResponse.class, delete(Router.BANK_ACCOUNTS, id));
}
@Override
public ListenableFuture<BankAccountResponse> verifyBankAccount(final BankAccountVerifyRequest request) {
return execute(
BankAccountResponse.class,
post(Router.BANK_ACCOUNTS + "/" + request.getId().value() + "/verify", request));
}
@Override
public ListenableFuture<AreaMailResponse> createAreaMail(final AreaMailRequest areaMailRequest) {
return execute(AreaMailResponse.class, post(Router.AREA_MAIL, areaMailRequest));
}
@Override
public ListenableFuture<AreaMailResponse> getAreaMail(final AreaMailId id) {
return execute(AreaMailResponse.class, get(Router.AREA_MAIL, id));
}
@Override
public ListenableFuture<AreaMailResponseList> getAreaMails() {
return execute(AreaMailResponseList.class, get(Router.AREA_MAIL));
}
@Override
public ListenableFuture<AreaMailResponseList> getAreaMails(final int count) {
return execute(AreaMailResponseList.class, get(Router.AREA_MAIL, count));
}
@Override
public ListenableFuture<AreaMailResponseList> getAreaMails(final int count, final int offset) {
return execute(AreaMailResponseList.class, get(Router.AREA_MAIL, count, offset));
}
@Override
public ListenableFuture<AreaMailResponseList> getAreaMails(final Filter filter) {
return execute(AreaMailResponseList.class, get(Router.AREA_MAIL, filter));
}
@Override
public ListenableFuture<ZipCodeRouteResponseList> getZipCodeRoutes(final ZipCodeRouteRequest request) {
return execute(ZipCodeRouteResponseList.class, get(Router.ROUTES, request));
}
@Override
public ListenableFuture<USVerificationResponse> verifyUSAddress(final USVerificationRequest request) {
return execute(USVerificationResponse.class, post(Router.US_VERIFICATIONS, request));
}
@Override
public ListenableFuture<IntlVerificationResponse> verifyIntlAddress(final IntlVerificationRequest request) {
return execute(IntlVerificationResponse.class, post(Router.INTL_VERIFICATIONS, request));
}
@Override
public ListenableFuture<CountryResponseList> getCountries() {
return execute(CountryResponseList.class, get(Router.COUNTRIES));
}
@Override
public ListenableFuture<StateResponseList> getStates() {
return execute(StateResponseList.class, get(Router.STATES));
}
private BoundRequestBuilder delete(final String resourceUrl, final LobId id) {
return this.httpClient.prepareDelete(this.baseUrl + resourceUrl + "/" + id.value());
}
private BoundRequestBuilder get(final String resourceUrl) {
return get(resourceUrl, new FluentStringsMap());
}
private BoundRequestBuilder get(final String resourceUrl, final HasLobParams request) {
final FluentStringsMap map = new FluentStringsMap();
for (final LobParam param : request.getLobParams()) {
map.add(param.getName(), param.getStringParam());
}
return get(resourceUrl, new FluentStringsMap(map));
}
private BoundRequestBuilder get(final String resourceUrl, final LobId id) {
return get(resourceUrl + "/" + id.value(), new FluentStringsMap());
}
private BoundRequestBuilder get(final String resourceUrl, final StringValued id) {
return get(resourceUrl + "/" + id.value(), new FluentStringsMap());
}
private final static Escaper urlEscaper = UrlEscapers.urlFormParameterEscaper();
private BoundRequestBuilder get(final String resourceUrl, final Filter filter) {
final FluentStringsMap paramMap = new FluentStringsMap();
final Integer limit = filter.getLimit();
if (limit != null) {
paramMap.add("limit", Integer.toString(limit));
}
final Integer offset = filter.getOffset();
if (offset != null) {
paramMap.add("offset", Integer.toString(offset));
}
final Map<String, String> metadata = filter.getMetadata();
if (metadata != null) {
for (final Map.Entry<String, String> e : metadata.entrySet()) {
paramMap.add("metadata[" + urlEscaper.escape(e.getKey()) + "]", urlEscaper.escape(e.getValue()));
}
}
return get(resourceUrl, paramMap);
}
private BoundRequestBuilder get(final String resourceUrl, final int count) {
return get(resourceUrl, Filters.ofLimit(count));
}
private BoundRequestBuilder get(final String resourceUrl, final int count, final int offset) {
return get(resourceUrl, Filters.ofLimit(count).withOffset(offset));
}
private BoundRequestBuilder get(final String resourceUrl, final FluentStringsMap params) {
return this.httpClient.prepareGet(this.baseUrl + resourceUrl).setQueryParams(params);
}
private BoundRequestBuilder post(final String resourceUrl, final HasLobParams hasLobParams) {
final BoundRequestBuilder builder = this.httpClient.preparePost(this.baseUrl + resourceUrl);
final Collection<LobParam> params = hasLobParams.getLobParams();
boolean isMultipart = false;
for (final LobParam param : params) {
if (param.isFileParam()) {
isMultipart = true;
break;
}
}
if (isMultipart) {
for (final LobParam param : params) {
if (param.isStringParam()) {
for (final String s : param.getStringParam()) {
builder.addBodyPart(new StringPart(param.getName(), s));
}
}
else {
builder.addBodyPart(new FilePart(param.getName(), param.getFileParam(), null, null));
}
}
}
else {
for (final LobParam param : params) {
for (final String s : param.getStringParam()) {
builder.addFormParam(param.getName(), s);
}
}
}
return builder;
}
private static <T extends AbstractResponse> ListenableFuture<T> execute(
final Class<T> clazz,
final BoundRequestBuilder request) {
final SettableFuture<T> guavaFut = SettableFuture.create();
final Optional<String> apiVersionOpt = Lob.getApiVersion();
String wrapperVersion;
if (apiVersionOpt.isPresent()) {
request.addHeader(LobClient.LOB_VERSION_HEADER, apiVersionOpt.get());
}
try {
Properties prop = new Properties();
prop.load(AsyncLobClient.class.getClassLoader().getResourceAsStream("project.properties"));
wrapperVersion = prop.getProperty("version");
} catch (IOException e) {
wrapperVersion = "";
}
request.addHeader("user-agent", new StringBuilder().append("LobJava/").append(wrapperVersion).append(" JDK/").append(JAVA_VERSION).toString());
request.execute(new GuavaFutureConverter<T>(clazz, guavaFut));
return guavaFut;
}
private static class GuavaFutureConverter<T extends AbstractResponse> extends AsyncCompletionHandler<T> {
final Class<T> clazz;
final SettableFuture<T> guavaFut;
public GuavaFutureConverter(
final Class<T> clazz,
final SettableFuture<T> guavaFut) {
this.clazz = clazz;
this.guavaFut = guavaFut;
}
private static boolean isSuccess(final Response response) {
final int statusCode = response.getStatusCode();
return (statusCode > 199 && statusCode < 400);
}
@Override
public void onThrowable(final Throwable t) {
guavaFut.setException(t);
}
@Override
public T onCompleted(final Response response) throws Exception {
if (isSuccess(response)) {
final T value = MAPPER.readValue(response.getResponseBody(), clazz);
value.setStatusCode(response.getStatusCode());
value.setHeaders(response.getHeaders());
guavaFut.set(value);
return value;
}
else {
final ErrorResponse error = MAPPER.readValue(response.getResponseBody(), ErrorResponse.class);
final LobApiException exception = new LobApiException(response.getUri(), error, response.getStatusCode(), response.getHeaders());
guavaFut.setException(exception);
throw exception;
}
}
}
}