
com.ning.billing.recurly.RecurlyClient Maven / Gradle / Ivy
/*
* Copyright 2010-2014 Ning, Inc.
* Copyright 2014-2015 The Billing Project, LLC
*
* The Billing Project licenses this file to you under the Apache License, version 2.0
* (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package com.ning.billing.recurly;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Scanner;
import java.util.concurrent.ExecutionException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.xml.bind.DatatypeConverter;
import com.ning.billing.recurly.model.Account;
import com.ning.billing.recurly.model.Accounts;
import com.ning.billing.recurly.model.AddOn;
import com.ning.billing.recurly.model.AddOns;
import com.ning.billing.recurly.model.Adjustment;
import com.ning.billing.recurly.model.Adjustments;
import com.ning.billing.recurly.model.BillingInfo;
import com.ning.billing.recurly.model.Coupon;
import com.ning.billing.recurly.model.Coupons;
import com.ning.billing.recurly.model.Errors;
import com.ning.billing.recurly.model.Invoice;
import com.ning.billing.recurly.model.Invoices;
import com.ning.billing.recurly.model.Plan;
import com.ning.billing.recurly.model.Plans;
import com.ning.billing.recurly.model.RecurlyAPIError;
import com.ning.billing.recurly.model.RecurlyObject;
import com.ning.billing.recurly.model.RecurlyObjects;
import com.ning.billing.recurly.model.Redemption;
import com.ning.billing.recurly.model.Redemptions;
import com.ning.billing.recurly.model.RefundOption;
import com.ning.billing.recurly.model.Subscription;
import com.ning.billing.recurly.model.SubscriptionUpdate;
import com.ning.billing.recurly.model.SubscriptionNotes;
import com.ning.billing.recurly.model.Subscriptions;
import com.ning.billing.recurly.model.Transaction;
import com.ning.billing.recurly.model.Transactions;
import org.joda.time.DateTime;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.ning.http.client.AsyncHttpClient;
import com.ning.http.client.AsyncHttpClientConfig;
import com.ning.http.client.Response;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.StandardSystemProperty;
import com.google.common.io.CharSource;
import com.google.common.io.Resources;
import com.google.common.net.HttpHeaders;
public class RecurlyClient {
private static final Logger log = LoggerFactory.getLogger(RecurlyClient.class);
public static final String RECURLY_DEBUG_KEY = "recurly.debug";
public static final String RECURLY_PAGE_SIZE_KEY = "recurly.page.size";
public static final String RECURLY_API_VERSION = "2.1";
private static final Integer DEFAULT_PAGE_SIZE = 20;
private static final String PER_PAGE = "per_page=";
private static final String X_RECORDS_HEADER_NAME = "X-Records";
private static final String LINK_HEADER_NAME = "Link";
private static final String GIT_PROPERTIES_FILE = "com/ning/billing/recurly/git.properties";
@VisibleForTesting
static final String GIT_COMMIT_ID_DESCRIBE_SHORT = "git.commit.id.describe-short";
private static final Pattern TAG_FROM_GIT_DESCRIBE_PATTERN = Pattern.compile("recurly-java-library-([0-9]*\\.[0-9]*\\.[0-9]*)(-[0-9]*)?");
public static final String FETCH_RESOURCE = "/recurly_js/result";
/**
* Checks a system property to see if debugging output is
* required. Used internally by the client to decide whether to
* generate debug output
*/
private static boolean debug() {
return Boolean.getBoolean(RECURLY_DEBUG_KEY);
}
/**
* Returns the page Size to use when querying. The page size
* is set as System.property: recurly.page.size
*/
public static Integer getPageSize() {
Integer pageSize;
try {
pageSize = new Integer(System.getProperty(RECURLY_PAGE_SIZE_KEY));
} catch (NumberFormatException nfex) {
pageSize = DEFAULT_PAGE_SIZE;
}
return pageSize;
}
public static String getPageSizeGetParam() {
return PER_PAGE + getPageSize().toString();
}
// TODO: should we make it static?
private final XmlMapper xmlMapper;
private final String userAgent;
private final String key;
private final String baseUrl;
private AsyncHttpClient client;
public RecurlyClient(final String apiKey) {
this(apiKey, "api");
}
public RecurlyClient(final String apiKey, final String subDomain) {
this(apiKey, subDomain + ".recurly.com", 443, "v2");
}
public RecurlyClient(final String apiKey, final String host, final int port, final String version) {
this.key = DatatypeConverter.printBase64Binary(apiKey.getBytes());
this.baseUrl = String.format("https://%s:%d/%s", host, port, version);
this.xmlMapper = RecurlyObject.newXmlMapper();
this.userAgent = buildUserAgent();
}
/**
* Open the underlying http client
*/
public synchronized void open() {
client = createHttpClient();
}
/**
* Close the underlying http client
*/
public synchronized void close() {
if (client != null) {
client.close();
}
}
/**
* Create Account
*
* Creates a new account. You may optionally include billing information.
*
* @param account account object
* @return the newly created account object on success, null otherwise
*/
public Account createAccount(final Account account) {
return doPOST(Account.ACCOUNT_RESOURCE, account, Account.class);
}
/**
* Get Accounts
*
* Returns information about all accounts.
*
* @return account object on success, null otherwise
*/
public Accounts getAccounts() {
return doGET(Accounts.ACCOUNTS_RESOURCE, Accounts.class);
}
public Coupons getCoupons() {
return doGET(Coupons.COUPONS_RESOURCE, Coupons.class);
}
/**
* Get Account
*
* Returns information about a single account.
*
* @param accountCode recurly account id
* @return account object on success, null otherwise
*/
public Account getAccount(final String accountCode) {
return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode, Account.class);
}
/**
* Update Account
*
* Updates an existing account.
*
* @param accountCode recurly account id
* @param account account object
* @return the updated account object on success, null otherwise
*/
public Account updateAccount(final String accountCode, final Account account) {
return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode, account, Account.class);
}
/**
* Close Account
*
* Marks an account as closed and cancels any active subscriptions. Any saved billing information will also be
* permanently removed from the account.
*
* @param accountCode recurly account id
*/
public void closeAccount(final String accountCode) {
doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode);
}
////////////////////////////////////////////////////////////////////////////////////////
// Account adjustments
public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type) {
return getAccountAdjustments(accountCode, type, null);
}
public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type, final Adjustments.AdjustmentState state) {
String url = Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE;
if (type != null || state != null) {
url += "?";
}
if (type != null) {
url += "type=" + type.getType();
if (state != null) {
url += "&";
}
}
if (state != null) {
url += "state=" + state.getState();
}
return doGET(url, Adjustments.class);
}
public Adjustment createAccountAdjustment(final String accountCode, final Adjustment adjustment) {
return doPOST(Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE,
adjustment,
Adjustment.class);
}
public void deleteAccountAdjustment(final String accountCode) {
doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE);
}
////////////////////////////////////////////////////////////////////////////////////////
/**
* Create a subscription
*
* Creates a subscription for an account.
*
* @param subscription Subscription object
* @return the newly created Subscription object on success, null otherwise
*/
public Subscription createSubscription(final Subscription subscription) {
return doPOST(Subscription.SUBSCRIPTION_RESOURCE,
subscription, Subscription.class);
}
/**
* Preview a subscription
*
* Previews a subscription for an account.
*
* @param subscription Subscription object
* @return the newly created Subscription object on success, null otherwise
*/
public Subscription previewSubscription(final Subscription subscription) {
return doPOST(Subscription.SUBSCRIPTION_RESOURCE
+ "/preview",
subscription, Subscription.class);
}
/**
* Get a particular {@link Subscription} by it's UUID
*
* Returns information about a single subscription.
*
* @param uuid UUID of the subscription to lookup
* @return Subscription
*/
public Subscription getSubscription(final String uuid) {
return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE
+ "/" + uuid,
Subscription.class);
}
/**
* Cancel a subscription
*
* Cancel a subscription so it remains active and then expires at the end of the current bill cycle.
*
* @param subscription Subscription object
* @return -?-
*/
public Subscription cancelSubscription(final Subscription subscription) {
return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/cancel",
subscription, Subscription.class);
}
/**
* Postpone a subscription
*
* postpone a subscription, setting a new renewal date.
*
* @param subscription Subscription object
* @return -?-
*/
public Subscription postponeSubscription(final Subscription subscription, final DateTime renewaldate) {
return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/postpone?next_renewal_date=" + renewaldate,
subscription, Subscription.class);
}
/**
* Terminate a particular {@link Subscription} by it's UUID
*
* @param subscription Subscription to terminate
*/
public void terminateSubscription(final Subscription subscription, final RefundOption refund) {
doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/terminate?refund=" + refund,
subscription, Subscription.class);
}
/**
* Reactivating a canceled subscription
*
* Reactivate a canceled subscription so it renews at the end of the current bill cycle.
*
* @param subscription Subscription object
* @return -?-
*/
public Subscription reactivateSubscription(final Subscription subscription) {
return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/reactivate",
subscription, Subscription.class);
}
/**
* Update a particular {@link Subscription} by it's UUID
*
* Returns information about a single subscription.
*
* @param uuid UUID of the subscription to update
* @param subscriptionUpdate subscriptionUpdate object
* @return Subscription the updated subscription
*/
public Subscription updateSubscription(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
return doPUT(Subscriptions.SUBSCRIPTIONS_RESOURCE
+ "/" + uuid,
subscriptionUpdate,
Subscription.class);
}
/**
* Preview an update to a particular {@link Subscription} by it's UUID
*
* Returns information about a single subscription.
*
* @param uuid UUID of the subscription to preview an update for
* @return Subscription the updated subscription preview
*/
public Subscription updateSubscriptionPreview(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
return doPOST(Subscriptions.SUBSCRIPTIONS_RESOURCE
+ "/" + uuid + "/preview",
subscriptionUpdate,
Subscription.class);
}
/**
* Update to a particular {@link Subscription}'s notes by it's UUID
*
* Returns information about a single subscription.
*
* @param uuid UUID of the subscription to preview an update for
* @param subscriptionNotes SubscriptionNotes object
* @return Subscription the updated subscription
*/
public Subscription updateSubscriptionNotes(final String uuid, final SubscriptionNotes subscriptionNotes) {
return doPUT(SubscriptionNotes.SUBSCRIPTION_RESOURCE + "/" + uuid + "/notes",
subscriptionNotes, Subscription.class);
}
/**
* Get the subscriptions for an {@link Account}.
*
* Returns information about a single {@link Account}.
*
* @param accountCode recurly account id
* @return Subscriptions for the specified user
*/
public Subscriptions getAccountSubscriptions(final String accountCode) {
return doGET(Account.ACCOUNT_RESOURCE
+ "/" + accountCode
+ Subscriptions.SUBSCRIPTIONS_RESOURCE,
Subscriptions.class);
}
/**
* Get the subscriptions for an account.
*
* Returns information about a single account.
*
* @param accountCode recurly account id
* @param status Only accounts in this status will be returned
* @return Subscriptions for the specified user
*/
public Subscriptions getAccountSubscriptions(final String accountCode, final String status) {
return doGET(Account.ACCOUNT_RESOURCE
+ "/" + accountCode
+ Subscriptions.SUBSCRIPTIONS_RESOURCE
+ "?state="
+ status,
Subscriptions.class);
}
////////////////////////////////////////////////////////////////////////////////////////
/**
* Update an account's billing info
*
* When new or updated credit card information is updated, the billing information is only saved if the credit card
* is valid. If the account has a past due invoice, the outstanding balance will be collected to validate the
* billing information.
*
* If the account does not exist before the API request, the account will be created if the billing information
* is valid.
*
* Please note: this API end-point may be used to import billing information without security codes (CVV).
* Recurly recommends requiring CVV from your customers when collecting new or updated billing information.
*
* @param billingInfo billing info object to create or update
* @return the newly created or update billing info object on success, null otherwise
*/
public BillingInfo createOrUpdateBillingInfo(final BillingInfo billingInfo) {
final String accountCode = billingInfo.getAccount().getAccountCode();
// Unset it to avoid confusing Recurly
billingInfo.setAccount(null);
return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE,
billingInfo, BillingInfo.class);
}
/**
* Lookup an account's billing info
*
* Returns only the account's current billing information.
*
* @param accountCode recurly account id
* @return the current billing info object associated with this account on success, null otherwise
*/
public BillingInfo getBillingInfo(final String accountCode) {
return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE,
BillingInfo.class);
}
/**
* Clear an account's billing info
*
* You may remove any stored billing information for an account. If the account has a subscription, the renewal will
* go into past due unless you update the billing info before the renewal occurs
*
* @param accountCode recurly account id
*/
public void clearBillingInfo(final String accountCode) {
doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE);
}
///////////////////////////////////////////////////////////////////////////
// User transactions
/**
* Lookup an account's transactions history
*
* Returns the account's transaction history
*
* @param accountCode recurly account id
* @return the transaction history associated with this account on success, null otherwise
*/
public Transactions getAccountTransactions(final String accountCode) {
return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Transactions.TRANSACTIONS_RESOURCE,
Transactions.class);
}
/**
* Lookup a transaction
*
* @param transactionId recurly transaction id
* @return the transaction if found, null otherwise
*/
public Transaction getTransaction(final String transactionId) {
return doGET(Transactions.TRANSACTIONS_RESOURCE + "/" + transactionId,
Transaction.class);
}
/**
* Creates a {@link Transaction} through the Recurly API.
*
* @param trans The {@link Transaction} to create
* @return The created {@link Transaction} object
*/
public Transaction createTransaction(final Transaction trans) {
return doPOST(Transactions.TRANSACTIONS_RESOURCE, trans, Transaction.class);
}
/**
* Refund a transaction
*
* @param transactionId recurly transaction id
* @param amount amount to refund, null for full refund
*/
public void refundTransaction(final String transactionId, @Nullable final BigDecimal amount) {
String url = Transactions.TRANSACTIONS_RESOURCE + "/" + transactionId;
if (amount != null) {
url = url + "?amount_in_cents=" + (amount.intValue() * 100);
}
doDELETE(url);
}
///////////////////////////////////////////////////////////////////////////
// User invoices
/**
* Lookup an invoice
*
* Returns the invoice
*
* @param invoiceId Recurly Invoice ID
* @return the invoice
*/
public Invoice getInvoice(final Integer invoiceId) {
return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId, Invoice.class);
}
/**
* Lookup an account's invoices
*
* Returns the account's invoices
*
* @param accountCode recurly account id
* @return the invoices associated with this account on success, null otherwise
*/
public Invoices getAccountInvoices(final String accountCode) {
return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Invoices.INVOICES_RESOURCE,
Invoices.class);
}
/**
* Post an invoice: invoice pending charges on an account
*
* Returns an invoice
*
* @param accountCode
* @return the invoice that was generated on success, null otherwise
*/
public Invoice postAccountInvoice(final String accountCode, final Invoice invoice) {
return doPOST(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Invoices.INVOICES_RESOURCE, invoice, Invoice.class);
}
/**
* Mark an invoice as paid successfully - Recurly Enterprise Feature
*
* @param invoiceId Recurly Invoice ID
*/
public Invoice markInvoiceSuccessful(final Integer invoiceId) {
return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/mark_successful", null, Invoice.class);
}
/**
* Mark an invoice as failed collection
*
* @param invoiceId Recurly Invoice ID
*/
public Invoice markInvoiceFailed(final Integer invoiceId) {
return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/mark_failed", null, Invoice.class);
}
/**
* Enter an offline payment for a manual invoice (beta) - Recurly Enterprise Feature
*
* @param invoiceId Recurly Invoice ID
* @param payment The external payment
*/
public Transaction enterOfflinePayment(final Integer invoiceId, final Transaction payment) {
return doPOST(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/transactions", payment, Transaction.class);
}
///////////////////////////////////////////////////////////////////////////
/**
* Create a Plan's info
*
*
* @param plan The plan to create on recurly
* @return the plan object as identified by the passed in ID
*/
public Plan createPlan(final Plan plan) {
return doPOST(Plan.PLANS_RESOURCE, plan, Plan.class);
}
/**
* Get a Plan's details
*
*
* @param planCode recurly id of plan
* @return the plan object as identified by the passed in ID
*/
public Plan getPlan(final String planCode) {
return doGET(Plan.PLANS_RESOURCE + "/" + planCode, Plan.class);
}
/**
* Return all the plans
*
*
* @return the plan object as identified by the passed in ID
*/
public Plans getPlans() {
return doGET(Plans.PLANS_RESOURCE, Plans.class);
}
/**
* Deletes a {@link Plan}
*
*
* @param planCode The {@link Plan} object to delete.
*/
public void deletePlan(final String planCode) {
doDELETE(Plan.PLANS_RESOURCE +
"/" +
planCode);
}
///////////////////////////////////////////////////////////////////////////
/**
* Create an AddOn to a Plan
*
*
* @param planCode The planCode of the {@link Plan } to create within recurly
* @param addOn The {@link AddOn} to create within recurly
* @return the {@link AddOn} object as identified by the passed in object
*/
public AddOn createPlanAddOn(final String planCode, final AddOn addOn) {
return doPOST(Plan.PLANS_RESOURCE +
"/" +
planCode +
AddOn.ADDONS_RESOURCE,
addOn, AddOn.class);
}
/**
* Get an AddOn's details
*
*
* @param addOnCode recurly id of {@link AddOn}
* @param planCode recurly id of {@link Plan}
* @return the {@link AddOn} object as identified by the passed in plan and add-on IDs
*/
public AddOn getAddOn(final String planCode, final String addOnCode) {
return doGET(Plan.PLANS_RESOURCE +
"/" +
planCode +
AddOn.ADDONS_RESOURCE +
"/" +
addOnCode, AddOn.class);
}
/**
* Return all the {@link AddOn} for a {@link Plan}
*
*
* @return the {@link AddOn} objects as identified by the passed plan ID
*/
public AddOns getAddOns(final String planCode) {
return doGET(Plan.PLANS_RESOURCE +
"/" +
planCode +
AddOn.ADDONS_RESOURCE, AddOns.class);
}
/**
* Deletes a {@link AddOn} for a Plan
*
*
* @param planCode The {@link Plan} object.
* @param addOnCode The {@link AddOn} object to delete.
*/
public void deleteAddOn(final String planCode, final String addOnCode) {
doDELETE(Plan.PLANS_RESOURCE +
"/" +
planCode +
AddOn.ADDONS_RESOURCE +
"/" +
addOnCode);
}
///////////////////////////////////////////////////////////////////////////
/**
* Create a {@link Coupon}
*
*
* @param coupon The coupon to create on recurly
* @return the {@link Coupon} object
*/
public Coupon createCoupon(final Coupon coupon) {
return doPOST(Coupon.COUPON_RESOURCE, coupon, Coupon.class);
}
/**
* Get a Coupon
*
*
* @param couponCode The code for the {@link Coupon}
* @return The {@link Coupon} object as identified by the passed in code
*/
public Coupon getCoupon(final String couponCode) {
return doGET(Coupon.COUPON_RESOURCE + "/" + couponCode, Coupon.class);
}
/**
* Delete a {@link Coupon}
*
*
* @param couponCode The code for the {@link Coupon}
*/
public void deleteCoupon(final String couponCode) {
doDELETE(Coupon.COUPON_RESOURCE + "/" + couponCode);
}
///////////////////////////////////////////////////////////////////////////
/**
* Redeem a {@link Coupon} on an account.
*
* @param couponCode redeemed coupon id
* @return the {@link Coupon} object
*/
public Redemption redeemCoupon(final String couponCode, final Redemption redemption) {
return doPOST(Coupon.COUPON_RESOURCE + "/" + couponCode + Redemption.REDEEM_RESOURCE,
redemption, Redemption.class);
}
/**
* Lookup the first coupon redemption on an account.
*
* @param accountCode recurly account id
* @return the coupon redemption for this account on success, null otherwise
*/
public Redemption getCouponRedemptionByAccount(final String accountCode) {
return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTION_RESOURCE,
Redemption.class);
}
/**
* Lookup all coupon redemptions on an account.
*
* @param accountCode recurly account id
* @return the coupon redemptions for this account on success, null otherwise
*/
public Redemptions getCouponRedemptionsByAccount(final String accountCode) {
return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTIONS_RESOURCE,
Redemptions.class);
}
/**
* Lookup the first coupon redemption on an invoice.
*
* @param invoiceNumber invoice number
* @return the coupon redemption for this invoice on success, null otherwise
*/
public Redemption getCouponRedemptionByInvoice(final Integer invoiceNumber) {
return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceNumber + Redemption.REDEMPTION_RESOURCE,
Redemption.class);
}
/**
* Lookup all coupon redemptions on an invoice.
*
* @param invoiceNumber invoice number
* @return the coupon redemptions for this invoice on success, null otherwise
*/
public Redemptions getCouponRedemptionsByInvoice(final Integer invoiceNumber) {
return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceNumber + Redemption.REDEMPTION_RESOURCE,
Redemptions.class);
}
/**
* Deletes a coupon redemption from an account.
*
* @param accountCode recurly account id
*/
public void deleteCouponRedemption(final String accountCode) {
doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTION_RESOURCE);
}
/**
* Deletes a specific redemption.
*
* @param accountCode recurly account id
* @param redemptionUuid recurly coupon redemption uuid
*/
public void deleteCouponRedemption(final String accountCode, final String redemptionUuid) {
doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTIONS_RESOURCE + "/" + redemptionUuid);
}
///////////////////////////////////////////////////////////////////////////
//
// Recurly.js API
//
///////////////////////////////////////////////////////////////////////////
/**
* Fetch Subscription
*
* Returns subscription from a recurly.js token.
*
* @param recurlyToken token given by recurly.js
* @return subscription object on success, null otherwise
*/
public Subscription fetchSubscription(final String recurlyToken) {
return fetch(recurlyToken, Subscription.class);
}
/**
* Fetch BillingInfo
*
* Returns billing info from a recurly.js token.
*
* @param recurlyToken token given by recurly.js
* @return billing info object on success, null otherwise
*/
public BillingInfo fetchBillingInfo(final String recurlyToken) {
return fetch(recurlyToken, BillingInfo.class);
}
/**
* Fetch Invoice
*
* Returns invoice from a recurly.js token.
*
* @param recurlyToken token given by recurly.js
* @return invoice object on success, null otherwise
*/
public Invoice fetchInvoice(final String recurlyToken) {
return fetch(recurlyToken, Invoice.class);
}
private T fetch(final String recurlyToken, final Class clazz) {
return doGET(FETCH_RESOURCE + "/" + recurlyToken, clazz);
}
///////////////////////////////////////////////////////////////////////////
private T doGET(final String resource, final Class clazz) {
final StringBuffer url = new StringBuffer(baseUrl);
url.append(resource);
if (resource != null && !resource.contains("?")) {
url.append("?");
} else {
url.append("&");
url.append("&");
}
url.append(getPageSizeGetParam());
return doGETWithFullURL(clazz, url.toString());
}
public T doGETWithFullURL(final Class clazz, final String url) {
if (debug()) {
log.info("Msg to Recurly API [GET] :: URL : {}", url);
}
return callRecurlySafe(client.prepareGet(url), clazz);
}
private T doPOST(final String resource, final RecurlyObject payload, final Class clazz) {
final String xmlPayload;
try {
xmlPayload = xmlMapper.writeValueAsString(payload);
if (debug()) {
log.info("Msg to Recurly API [POST]:: URL : {}", baseUrl + resource);
log.info("Payload for [POST]:: {}", xmlPayload);
}
} catch (IOException e) {
log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
return null;
}
return callRecurlySafe(client.preparePost(baseUrl + resource).setBody(xmlPayload), clazz);
}
private T doPUT(final String resource, final RecurlyObject payload, final Class clazz) {
final String xmlPayload;
try {
if (payload != null) {
xmlPayload = xmlMapper.writeValueAsString(payload);
} else {
xmlPayload = null;
}
if (debug()) {
log.info("Msg to Recurly API [PUT]:: URL : {}", baseUrl + resource);
log.info("Payload for [PUT]:: {}", xmlPayload);
}
} catch (IOException e) {
log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
return null;
}
return callRecurlySafe(client.preparePut(baseUrl + resource).setBody(xmlPayload), clazz);
}
private void doDELETE(final String resource) {
callRecurlySafe(client.prepareDelete(baseUrl + resource), null);
}
private T callRecurlySafe(final AsyncHttpClient.BoundRequestBuilder builder, @Nullable final Class clazz) {
try {
return callRecurly(builder, clazz);
} catch (IOException e) {
log.warn("Error while calling Recurly", e);
return null;
} catch (ExecutionException e) {
// Extract the errors exception, if any
if (e.getCause() != null &&
e.getCause().getCause() != null &&
e.getCause().getCause() instanceof TransactionErrorException) {
throw (TransactionErrorException) e.getCause().getCause();
} else if (e.getCause() != null &&
e.getCause() instanceof TransactionErrorException) {
// See https://github.com/killbilling/recurly-java-library/issues/16
throw (TransactionErrorException) e.getCause();
}
log.error("Execution error", e);
return null;
} catch (InterruptedException e) {
log.error("Interrupted while calling Recurly", e);
return null;
}
}
private T callRecurly(final AsyncHttpClient.BoundRequestBuilder builder, @Nullable final Class clazz)
throws IOException, ExecutionException, InterruptedException {
final Response response = builder.addHeader("Authorization", "Basic " + key)
.addHeader("Accept", "application/xml")
.addHeader("Content-Type", "application/xml; charset=utf-8")
.addHeader("X-Api-Version", RECURLY_API_VERSION)
.addHeader(HttpHeaders.USER_AGENT, userAgent)
.setBodyEncoding("UTF-8")
.execute()
.get();
final InputStream in = response.getResponseBodyAsStream();
try {
final String payload = convertStreamToString(in);
if (debug()) {
log.info("Msg from Recurly API :: {}", payload);
}
// Handle errors payload
if (response.getStatusCode() >= 300) {
log.warn("Recurly error whilst calling: {}\n{}", response.getUri(), payload);
if (response.getStatusCode() == 422) {
final Errors errors;
try {
errors = xmlMapper.readValue(payload, Errors.class);
} catch (Exception e) {
// 422 is returned for transaction errors (see https://recurly.readme.io/v2.0/page/transaction-errors)
// as well as bad input payloads
log.debug("Unable to extract error", e);
return null;
}
throw new TransactionErrorException(errors);
} else {
RecurlyAPIError recurlyError = null;
try {
recurlyError = xmlMapper.readValue(payload, RecurlyAPIError.class);
} catch (Exception e) {
log.debug("Unable to extract error", e);
}
throw new RecurlyAPIException(recurlyError);
}
}
if (clazz == null) {
return null;
}
final T obj = xmlMapper.readValue(payload, clazz);
if (obj instanceof RecurlyObject) {
((RecurlyObject) obj).setRecurlyClient(this);
} else if (obj instanceof RecurlyObjects) {
final RecurlyObjects recurlyObjects = (RecurlyObjects) obj;
recurlyObjects.setRecurlyClient(this);
// Set the RecurlyClient on all objects for later use
for (final Object object : recurlyObjects) {
((RecurlyObject) object).setRecurlyClient(this);
}
// Set the total number of records
final String xRecords = response.getHeader(X_RECORDS_HEADER_NAME);
if (xRecords != null) {
recurlyObjects.setNbRecords(Integer.valueOf(xRecords));
}
// Set links for pagination
final String linkHeader = response.getHeader(LINK_HEADER_NAME);
if (linkHeader != null) {
final String[] links = PaginationUtils.getLinks(linkHeader);
recurlyObjects.setStartUrl(links[0]);
recurlyObjects.setPrevUrl(links[1]);
recurlyObjects.setNextUrl(links[2]);
}
}
return obj;
} finally {
closeStream(in);
}
}
private String convertStreamToString(final java.io.InputStream is) {
try {
return new Scanner(is).useDelimiter("\\A").next();
} catch (final NoSuchElementException e) {
return "";
}
}
private void closeStream(final InputStream in) {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.warn("Failed to close http-client - provided InputStream: {}", e.getLocalizedMessage());
}
}
}
private AsyncHttpClient createHttpClient() {
// Don't limit the number of connections per host
// See https://github.com/ning/async-http-client/issues/issue/28
final AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder();
builder.setMaxConnectionsPerHost(-1);
return new AsyncHttpClient(builder.build());
}
@VisibleForTesting
String getUserAgent() {
return userAgent;
}
private String buildUserAgent() {
final String defaultVersion = "0.0.0";
final String defaultJavaVersion = "0.0.0";
try {
final Properties gitRepositoryState = new Properties();
final URL resourceURL = Resources.getResource(GIT_PROPERTIES_FILE);
final CharSource charSource = Resources.asCharSource(resourceURL, Charset.forName("UTF-8"));
Reader reader = null;
try {
reader = charSource.openStream();
gitRepositoryState.load(reader);
} finally {
if (reader != null) {
reader.close();
}
}
final String version = Objects.firstNonNull(getVersionFromGitRepositoryState(gitRepositoryState), defaultVersion);
final String javaVersion = Objects.firstNonNull(StandardSystemProperty.JAVA_VERSION.value(), defaultJavaVersion);
return String.format("KillBill/%s; %s", version, javaVersion);
} catch (final Exception e) {
return String.format("KillBill/%s; %s", defaultVersion, defaultJavaVersion);
}
}
@VisibleForTesting
String getVersionFromGitRepositoryState(final Properties gitRepositoryState) {
final String gitDescribe = gitRepositoryState.getProperty(GIT_COMMIT_ID_DESCRIBE_SHORT);
if (gitDescribe == null) {
return null;
}
final Matcher matcher = TAG_FROM_GIT_DESCRIBE_PATTERN.matcher(gitDescribe);
return matcher.find() ? matcher.group(1) : null;
}
}