001/*
002 * Copyright 2010-2014 Ning, Inc.
003 * Copyright 2014-2015 The Billing Project, LLC
004 *
005 * The Billing Project licenses this file to you under the Apache License, version 2.0
006 * (the "License"); you may not use this file except in compliance with the
007 * License.  You may obtain a copy of the License at:
008 *
009 *    http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package com.ning.billing.recurly;
019
020import com.ning.billing.recurly.model.Account;
021import com.ning.billing.recurly.model.AccountBalance;
022import com.ning.billing.recurly.model.AccountNotes;
023import com.ning.billing.recurly.model.Accounts;
024import com.ning.billing.recurly.model.AddOn;
025import com.ning.billing.recurly.model.AddOns;
026import com.ning.billing.recurly.model.Adjustment;
027import com.ning.billing.recurly.model.AdjustmentRefund;
028import com.ning.billing.recurly.model.Adjustments;
029import com.ning.billing.recurly.model.BillingInfo;
030import com.ning.billing.recurly.model.Coupon;
031import com.ning.billing.recurly.model.Coupons;
032import com.ning.billing.recurly.model.CreditPayments;
033import com.ning.billing.recurly.model.Errors;
034import com.ning.billing.recurly.model.GiftCard;
035import com.ning.billing.recurly.model.GiftCards;
036import com.ning.billing.recurly.model.Invoice;
037import com.ning.billing.recurly.model.InvoiceCollection;
038import com.ning.billing.recurly.model.InvoiceRefund;
039import com.ning.billing.recurly.model.InvoiceState;
040import com.ning.billing.recurly.model.Invoices;
041import com.ning.billing.recurly.model.Item;
042import com.ning.billing.recurly.model.Items;
043import com.ning.billing.recurly.model.Plan;
044import com.ning.billing.recurly.model.Plans;
045import com.ning.billing.recurly.model.Purchase;
046import com.ning.billing.recurly.model.RecurlyAPIError;
047import com.ning.billing.recurly.model.RecurlyObject;
048import com.ning.billing.recurly.model.RecurlyObjects;
049import com.ning.billing.recurly.model.Redemption;
050import com.ning.billing.recurly.model.Redemptions;
051import com.ning.billing.recurly.model.RefundMethod;
052import com.ning.billing.recurly.model.RefundOption;
053import com.ning.billing.recurly.model.ResponseMetadata;
054import com.ning.billing.recurly.model.ShippingAddress;
055import com.ning.billing.recurly.model.ShippingAddresses;
056import com.ning.billing.recurly.model.Subscription;
057import com.ning.billing.recurly.model.SubscriptionState;
058import com.ning.billing.recurly.model.SubscriptionUpdate;
059import com.ning.billing.recurly.model.SubscriptionNotes;
060import com.ning.billing.recurly.model.Subscriptions;
061import com.ning.billing.recurly.model.Transaction;
062import com.ning.billing.recurly.model.TransactionState;
063import com.ning.billing.recurly.model.TransactionType;
064import com.ning.billing.recurly.model.Transactions;
065import com.ning.billing.recurly.model.Usage;
066import com.ning.billing.recurly.model.Usages;
067import com.ning.billing.recurly.model.MeasuredUnit;
068import com.ning.billing.recurly.model.MeasuredUnits;
069import com.ning.billing.recurly.model.AccountAcquisition;
070import com.ning.billing.recurly.model.ShippingMethod;
071import com.ning.billing.recurly.model.ShippingMethods;
072import com.fasterxml.jackson.annotation.JsonTypeInfo.None;
073import com.fasterxml.jackson.dataformat.xml.XmlMapper;
074import com.google.common.annotations.VisibleForTesting;
075import com.google.common.base.MoreObjects;
076import com.google.common.base.StandardSystemProperty;
077import com.google.common.io.CharSource;
078import com.google.common.io.Resources;
079import com.google.common.net.HttpHeaders;
080
081import com.ning.billing.recurly.util.http.SslUtils;
082import com.ning.http.client.AsyncHttpClient;
083import com.ning.http.client.AsyncHttpClientConfig;
084import com.ning.http.client.FluentCaseInsensitiveStringsMap;
085import com.ning.http.client.Response;
086import org.joda.time.DateTime;
087import org.slf4j.Logger;
088import org.slf4j.LoggerFactory;
089
090import javax.annotation.Nullable;
091import javax.xml.bind.DatatypeConverter;
092import java.io.IOException;
093import java.io.InputStream;
094import java.io.Reader;
095import java.math.BigDecimal;
096import java.net.ConnectException;
097import java.net.URI;
098import java.net.URL;
099import java.nio.charset.Charset;
100import java.security.KeyManagementException;
101import java.security.NoSuchAlgorithmException;
102import java.util.NoSuchElementException;
103import java.util.Properties;
104import java.util.Scanner;
105import java.util.concurrent.ExecutionException;
106import java.util.regex.Matcher;
107import java.util.regex.Pattern;
108import java.util.List;
109import java.util.Arrays;
110
111public class RecurlyClient {
112
113    private static final Logger log = LoggerFactory.getLogger(RecurlyClient.class);
114
115    public static final String RECURLY_DEBUG_KEY = "recurly.debug";
116    public static final String RECURLY_API_VERSION = "2.25";
117
118    private static final String X_RATELIMIT_REMAINING_HEADER_NAME = "X-RateLimit-Remaining";
119    private static final String X_RECORDS_HEADER_NAME = "X-Records";
120    private static final String LINK_HEADER_NAME = "Link";
121
122    private static final String GIT_PROPERTIES_FILE = "com/ning/billing/recurly/git.properties";
123    @VisibleForTesting
124    static final String GIT_COMMIT_ID_DESCRIBE_SHORT = "git.commit.id.describe-short";
125    private static final Pattern TAG_FROM_GIT_DESCRIBE_PATTERN = Pattern.compile("recurly-java-library-([0-9]*\\.[0-9]*\\.[0-9]*)(-[0-9]*)?");
126
127    public static final String FETCH_RESOURCE = "/recurly_js/result";
128
129    private static final List<String> validHosts = Arrays.asList("recurly.com");
130
131    /**
132     * Checks a system property to see if debugging output is
133     * required. Used internally by the client to decide whether to
134     * generate debug output
135     */
136    private static boolean debug() {
137        return Boolean.getBoolean(RECURLY_DEBUG_KEY);
138    }
139
140    /**
141     * Warns the user about logging PII in production environments
142     */
143    private static void loggerWarning() {
144        if (debug())
145        {
146            log.warn("[WARNING] Logger enabled. The logger has the potential to leak " +
147            "PII and should never be used in production environments.");
148        }
149    }
150
151    // TODO: should we make it static?
152    private final XmlMapper xmlMapper;
153    private final String userAgent;
154
155    private final String key;
156    private final String baseUrl;
157    private AsyncHttpClient client;
158
159    // Allows error messages to be returned in a specified language
160    private String acceptLanguage = "en-US";
161
162    // Stores the number of requests remaining before rate limiting takes effect
163    private int rateLimitRemaining;
164
165    public RecurlyClient(final String apiKey) {
166        this(apiKey, "api");
167        loggerWarning();
168    }
169
170    public RecurlyClient(final String apiKey, final String subDomain) {
171        this(apiKey, subDomain + ".recurly.com", 443, "v2");
172        loggerWarning();
173    }
174
175    public RecurlyClient(final String apiKey, final String host, final int port, final String version) {
176        this(apiKey, "https", host, port, version);
177        loggerWarning();
178    }
179
180    public RecurlyClient(final String apiKey, final String scheme, final String host, final int port, final String version) {
181        this.key = DatatypeConverter.printBase64Binary(apiKey.getBytes());
182        this.baseUrl = String.format("%s://%s:%d/%s", scheme, host, port, version);
183        this.xmlMapper = RecurlyObject.newXmlMapper();
184        this.userAgent = buildUserAgent();
185        this.rateLimitRemaining = -1;
186        loggerWarning();
187    }
188
189    /**
190     * Open the underlying http client
191     */
192    public synchronized void open() throws NoSuchAlgorithmException, KeyManagementException {
193        client = createHttpClient();
194    }
195
196    /**
197     * Close the underlying http client
198     */
199    public synchronized void close() {
200        if (client != null) {
201            client.close();
202        }
203    }
204
205    /**
206     * Set the Accept-Language header
207     * <p>
208     * Sets the Accept-Language header for all requests made by this client. Note: this is not thread-safe!
209     * See https://github.com/killbilling/recurly-java-library/pull/298 for more details about thread safety.
210     *
211     * @param language The language to set in the header. E.g., "en-US"
212     */
213    public void setAcceptLanguage(String language) {
214        this.acceptLanguage = language;
215    }
216
217    /**
218     * Returns the number of requests remaining until requests will be denied by rate limiting.
219     * @return Number of requests remaining. Value is valid (> -1) after a successful API call.
220     */
221    public int getRateLimitRemaining() {
222        return rateLimitRemaining;
223    }
224
225    /**
226     * Create Account
227     * <p>
228     * Creates a new account. You may optionally include billing information.
229     *
230     * @param account account object
231     * @return the newly created account object on success, null otherwise
232     */
233    public Account createAccount(final Account account) {
234        return doPOST(Account.ACCOUNT_RESOURCE, account, Account.class);
235    }
236
237    /**
238     * Get Accounts
239     * <p>
240     * Returns information about all accounts.
241     *
242     * @return Accounts on success, null otherwise
243     */
244    public Accounts getAccounts() {
245        return doGET(Accounts.ACCOUNTS_RESOURCE, Accounts.class, new QueryParams());
246    }
247
248    /**
249     * Get Accounts given query params
250     * <p>
251     * Returns information about all accounts.
252     *
253     * @param params {@link QueryParams}
254     * @return Accounts on success, null otherwise
255     */
256    public Accounts getAccounts(final QueryParams params) {
257        return doGET(Accounts.ACCOUNTS_RESOURCE, Accounts.class, params);
258    }
259
260    /**
261     * Get number of Accounts matching the query params
262     *
263     * @param params {@link QueryParams}
264     * @return Integer on success, null otherwise
265     */
266    public Integer getAccountsCount(final QueryParams params) {
267        FluentCaseInsensitiveStringsMap map = doHEAD(Accounts.ACCOUNTS_RESOURCE, params);
268        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
269    }
270
271    /**
272     * Get Coupons
273     * <p>
274     * Returns information about all accounts.
275     *
276     * @return Coupons on success, null otherwise
277     */
278    public Coupons getCoupons() {
279        return doGET(Coupons.COUPONS_RESOURCE, Coupons.class, new QueryParams());
280    }
281
282    /**
283     * Get Coupons given query params
284     * <p>
285     * Returns information about all accounts.
286     *
287     * @param params {@link QueryParams}
288     * @return Coupons on success, null otherwise
289     */
290    public Coupons getCoupons(final QueryParams params) {
291        return doGET(Coupons.COUPONS_RESOURCE, Coupons.class, params);
292    }
293
294    /**
295     * Get number of Coupons matching the query params
296     *
297     * @param params {@link QueryParams}
298     * @return Integer on success, null otherwise
299     */
300    public Integer getCouponsCount(final QueryParams params) {
301        FluentCaseInsensitiveStringsMap map = doHEAD(Coupons.COUPONS_RESOURCE, params);
302        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
303    }
304
305    /**
306     * Get Account
307     * <p>
308     * Returns information about a single account.
309     *
310     * @param accountCode recurly account id
311     * @return account object on success, null otherwise
312     */
313    public Account getAccount(final String accountCode) {
314        if (accountCode == null || accountCode.isEmpty())
315            throw new RuntimeException("accountCode cannot be empty!");
316
317        return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode, Account.class);
318    }
319
320    /**
321     * Update Account
322     * <p>
323     * Updates an existing account.
324     *
325     * @param accountCode recurly account id
326     * @param account     account object
327     * @return the updated account object on success, null otherwise
328     */
329    public Account updateAccount(final String accountCode, final Account account) {
330        return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode, account, Account.class);
331    }
332
333    /**
334     * Get Account Balance
335     * <p>
336     * Retrieves the remaining balance on the account
337     *
338     * @param accountCode recurly account id
339     * @return the updated AccountBalance if success, null otherwise
340     */
341    public AccountBalance getAccountBalance(final String accountCode) {
342        return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode + AccountBalance.ACCOUNT_BALANCE_RESOURCE, AccountBalance.class);
343    }
344
345    /**
346     * Close Account
347     * <p>
348     * Marks an account as closed and cancels any active subscriptions. Any saved billing information will also be
349     * permanently removed from the account.
350     *
351     * @param accountCode recurly account id
352     */
353    public void closeAccount(final String accountCode) {
354        doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode);
355    }
356
357    /**
358     * Reopen Account
359     * <p>
360     * Transitions a closed account back to active.
361     *
362     * @param accountCode recurly account id
363     */
364    public Account reopenAccount(final String accountCode) {
365        return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode + "/reopen",
366                     null, Account.class);
367    }
368
369
370    /**
371     * Get Child Accounts
372     * <p>
373     * Returns information about a the child accounts of an account.
374     *
375     * @param accountCode recurly account id
376     * @return Accounts on success, null otherwise
377     */
378    public Accounts getChildAccounts(final String accountCode) {
379        return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode + "/child_accounts", Accounts.class, new QueryParams());
380    }
381
382    ////////////////////////////////////////////////////////////////////////////////////////
383    // Account adjustments
384
385    /**
386     * Get Account Adjustments
387     * <p>
388     *
389     * @param accountCode recurly account id
390     * @return the adjustments on the account
391     */
392    public Adjustments getAccountAdjustments(final String accountCode) {
393        return getAccountAdjustments(accountCode, null, null, new QueryParams());
394    }
395
396    /**
397     * Get Account Adjustments
398     * <p>
399     *
400     * @param accountCode recurly account id
401     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
402     * @return the adjustments on the account
403     */
404    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type) {
405        return getAccountAdjustments(accountCode, type, null, new QueryParams());
406    }
407
408    /**
409     * Get Account Adjustments
410     * <p>
411     *
412     * @param accountCode recurly account id
413     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
414     * @param state {@link com.ning.billing.recurly.model.Adjustments.AdjustmentState}
415     * @return the adjustments on the account
416     */
417    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type, final Adjustments.AdjustmentState state) {
418        return getAccountAdjustments(accountCode, type, state, new QueryParams());
419    }
420
421    /**
422     * Get Account Adjustments
423     * <p>
424     *
425     * @param accountCode recurly account id
426     * @param type {@link com.ning.billing.recurly.model.Adjustments.AdjustmentType}
427     * @param state {@link com.ning.billing.recurly.model.Adjustments.AdjustmentState}
428     * @param params {@link QueryParams}
429     * @return the adjustments on the account
430     */
431    public Adjustments getAccountAdjustments(final String accountCode, final Adjustments.AdjustmentType type, final Adjustments.AdjustmentState state, final QueryParams params) {
432        final String url = Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE;
433
434        if (type != null) params.put("type", type.getType());
435        if (state != null) params.put("state", state.getState());
436
437        return doGET(url, Adjustments.class, params);
438    }
439
440    public Adjustment getAdjustment(final String adjustmentUuid) {
441        if (adjustmentUuid == null || adjustmentUuid.isEmpty())
442            throw new RuntimeException("adjustmentUuid cannot be empty!");
443
444        return doGET(Adjustments.ADJUSTMENTS_RESOURCE + "/" + adjustmentUuid, Adjustment.class);
445    }
446
447    public Adjustment createAccountAdjustment(final String accountCode, final Adjustment adjustment) {
448        return doPOST(Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE,
449                      adjustment,
450                      Adjustment.class);
451    }
452
453    public void deleteAccountAdjustment(final String accountCode) {
454        doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode + Adjustments.ADJUSTMENTS_RESOURCE);
455    }
456
457    public void deleteAdjustment(final String adjustmentUuid) {
458        doDELETE(Adjustments.ADJUSTMENTS_RESOURCE + "/" + adjustmentUuid);
459    }
460
461    ////////////////////////////////////////////////////////////////////////////////////////
462
463    /**
464     * Create a subscription
465     * <p>
466     * Creates a subscription for an account.
467     *
468     * @param subscription Subscription object
469     * @return the newly created Subscription object on success, null otherwise
470     */
471    public Subscription createSubscription(final Subscription subscription) {
472        return doPOST(Subscription.SUBSCRIPTION_RESOURCE,
473                      subscription, Subscription.class);
474    }
475
476    /**
477     * Preview a subscription
478     * <p>
479     * Previews a subscription for an account.
480     *
481     * @param subscription Subscription object
482     * @return the newly created Subscription object on success, null otherwise
483     */
484    public Subscription previewSubscription(final Subscription subscription) {
485        return doPOST(Subscription.SUBSCRIPTION_RESOURCE
486                      + "/preview",
487                      subscription, Subscription.class);
488    }
489
490    /**
491     * Get a particular {@link Subscription} by it's UUID
492     * <p>
493     * Returns information about a single subscription.
494     *
495     * @param uuid UUID of the subscription to lookup
496     * @return Subscription
497     */
498    public Subscription getSubscription(final String uuid) {
499        if (uuid == null || uuid.isEmpty())
500            throw new RuntimeException("uuid cannot be empty!");
501
502        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE
503                     + "/" + uuid,
504                     Subscription.class);
505    }
506
507    /**
508     * Cancel a subscription
509     * <p>
510     * Cancel a subscription so it remains active and then expires at the end of the current bill cycle.
511     *
512     * @param subscription Subscription object
513     * @return Subscription
514     */
515    public Subscription cancelSubscription(final Subscription subscription) {
516        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/cancel",
517                     subscription, Subscription.class);
518    }
519
520    /**
521     * Cancel a subscription
522     * <p>
523     * Cancel a subscription so it remains active and then expires at the end of the current bill cycle.
524     *
525     * @param subscriptionUuid String uuid of the subscription to cancel
526     * @param timeframe SubscriptionUpdate.TimeFrame the timeframe in which to cancel. Only accepts bill_date or term_end
527     * @return Subscription
528     */
529    public Subscription cancelSubscription(final String subscriptionUuid, final SubscriptionUpdate.Timeframe timeframe) {
530        final QueryParams qp = new QueryParams();
531        if (timeframe != null) qp.put("timeframe", timeframe.toString());
532        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + "/cancel",
533                     null, Subscription.class, qp);
534    }
535
536    /**
537     * Pause a subscription or cancel a scheduled pause on a subscription.
538     * <p>
539     * * For an active subscription without a pause scheduled already, this will
540     *   schedule a pause period to begin at the next renewal date for the specified
541     *   number of billing cycles (remaining_pause_cycles).
542     * * When a scheduled pause already exists, this will update the remaining pause
543     *   cycles with the new value sent. When zero (0) remaining_pause_cycles is sent
544     *   for a subscription with a scheduled pause, the pause will be canceled.
545     * * For a paused subscription, the remaining_pause_cycles will adjust the
546     *   length of the current pause period. Sending zero (0) in the remaining_pause_cycles
547     *   field will cause the subscription to be resumed at the next renewal date.
548     *
549     * @param subscriptionUuid The uuid for the subscription you wish to pause.
550     * @param remainingPauseCycles The number of billing cycles that the subscription will be paused.
551     * @return Subscription
552     */
553    public Subscription pauseSubscription(final String subscriptionUuid, final int remainingPauseCycles) {
554        Subscription request = new Subscription();
555        request.setRemainingPauseCycles(remainingPauseCycles);
556        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + "/pause",
557                     request, Subscription.class);
558    }
559
560    /**
561     * Convert trial to paid subscription when TransactionType = "moto".
562     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
563     * @return Subscription
564     */
565    public Subscription convertTrialMoto(final String subscriptionUuid) {
566        Subscription request = new Subscription();
567        request.setTransactionType("moto");
568        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + "/convert_trial",
569            request, Subscription.class);
570    }
571
572    /**
573     * Convert trial to paid subscription without 3DS token
574     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
575     * @return Subscription
576     */
577    public Subscription convertTrial(final String subscriptionUuid) {
578        return convertTrial(subscriptionUuid, null);
579    }
580
581    /**
582     * Convert trial to paid subscription with 3DS token
583     * @param subscriptionUuid The uuid for the subscription you want to convert from trial to paid.
584     * @param ThreeDSecureActionResultTokenId 3DS secure action result token id in billing info.
585     * @return Subscription
586     */
587    public Subscription convertTrial(final String subscriptionUuid, final String ThreeDSecureActionResultTokenId) {
588        Subscription request;
589        if (ThreeDSecureActionResultTokenId == null) {
590            request = null;
591        } else {
592            request = new Subscription();
593            Account account = new Account();
594            BillingInfo billingInfo = new BillingInfo();
595            billingInfo.setThreeDSecureActionResultTokenId(ThreeDSecureActionResultTokenId); 
596            account.setBillingInfo(billingInfo);
597            request.setAccount(account);   
598        }
599        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + "/convert_trial",
600            request, Subscription.class);
601    }
602
603    /**
604     * Immediately resumes a currently paused subscription.
605     * <p>
606     * For a paused subscription, this will immediately resume the subscription
607     * from the pause, produce an invoice, and return the newly resumed subscription.
608     * Any at-renewal subscription changes will be immediately applied when
609     * the subscription resumes.
610     *
611     * @param subscriptionUuid The uuid for the subscription you wish to pause.
612     * @return Subscription
613     */
614    public Subscription resumeSubscription(final String subscriptionUuid) {
615        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + "/resume",
616                null, Subscription.class);
617    }
618
619    /**
620     * Postpone a subscription
621     * <p>
622     * postpone a subscription, setting a new renewal date.
623     *
624     * @param subscription Subscription object
625     * @return Subscription
626     */
627    public Subscription postponeSubscription(final Subscription subscription, final DateTime renewaldate) {
628        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/postpone?next_renewal_date=" + renewaldate,
629                     subscription, Subscription.class);
630    }
631
632    /**
633     * Terminate a particular {@link Subscription} by it's UUID
634     *
635     * @param subscription Subscription to terminate
636     */
637    public void terminateSubscription(final Subscription subscription, final RefundOption refund) {
638        doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/terminate?refund=" + refund,
639              subscription, Subscription.class);
640    }
641
642    /**
643     * Reactivating a canceled subscription
644     * <p>
645     * Reactivate a canceled subscription so it renews at the end of the current bill cycle.
646     *
647     * @param subscription Subscription object
648     * @return Subscription
649     */
650    public Subscription reactivateSubscription(final Subscription subscription) {
651        return doPUT(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscription.getUuid() + "/reactivate",
652                     subscription, Subscription.class);
653    }
654
655    /**
656     * Update a particular {@link Subscription} by it's UUID
657     * <p>
658     * Returns information about a single subscription.
659     *
660     * @param uuid               UUID of the subscription to update
661     * @param subscriptionUpdate subscriptionUpdate object
662     * @return Subscription the updated subscription
663     */
664    public Subscription updateSubscription(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
665        return doPUT(Subscriptions.SUBSCRIPTIONS_RESOURCE
666                     + "/" + uuid,
667                     subscriptionUpdate,
668                     Subscription.class);
669    }
670
671    /**
672     * Preview an update to a particular {@link Subscription} by it's UUID
673     * <p>
674     * Returns information about a single subscription.
675     *
676     * @param uuid UUID of the subscription to preview an update for
677     * @return Subscription the updated subscription preview
678     */
679    public Subscription updateSubscriptionPreview(final String uuid, final SubscriptionUpdate subscriptionUpdate) {
680        return doPOST(Subscriptions.SUBSCRIPTIONS_RESOURCE
681                      + "/" + uuid + "/preview",
682                      subscriptionUpdate,
683                      Subscription.class);
684    }
685
686
687    /**
688     * Update to a particular {@link Subscription}'s notes by it's UUID
689     * <p>
690     * Returns information about a single subscription.
691     *
692     * @param uuid UUID of the subscription to preview an update for
693     * @param subscriptionNotes SubscriptionNotes object
694     * @return Subscription the updated subscription
695     */
696    public Subscription updateSubscriptionNotes(final String uuid, final SubscriptionNotes subscriptionNotes) {
697      return doPUT(SubscriptionNotes.SUBSCRIPTION_RESOURCE + "/" + uuid + "/notes",
698                   subscriptionNotes, Subscription.class);
699    }
700
701    /**
702     * Get the subscriptions for an {@link Account}.
703     * <p>
704     * Returns subscriptions associated with an account
705     *
706     * @param accountCode recurly account id
707     * @return Subscriptions on the account
708     */
709    public Subscriptions getAccountSubscriptions(final String accountCode) {
710        return doGET(Account.ACCOUNT_RESOURCE
711                     + "/" + accountCode
712                     + Subscriptions.SUBSCRIPTIONS_RESOURCE,
713                     Subscriptions.class,
714                     new QueryParams());
715    }
716
717    /**
718     * Get all the subscriptions on the site
719     * <p>
720     * Returns all the subscriptions on the site
721     *
722     * @return Subscriptions on the site
723     */
724    public Subscriptions getSubscriptions() {
725        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE,
726                Subscriptions.class, new QueryParams());
727    }
728
729    /**
730     * Get all the subscriptions on the site given some sort and filter params.
731     * <p>
732     * Returns all the subscriptions on the site
733     *
734     * @param state {@link SubscriptionState}
735     * @param params {@link QueryParams}
736     * @return Subscriptions on the site
737     */
738    public Subscriptions getSubscriptions(final SubscriptionState state, final QueryParams params) {
739        if (state != null) { params.put("state", state.getType()); }
740
741        return doGET(Subscriptions.SUBSCRIPTIONS_RESOURCE,
742                Subscriptions.class, params);
743    }
744
745    /**
746     * Get number of Subscriptions matching the query params
747     *
748     * @param params {@link QueryParams}
749     * @return Integer on success, null otherwise
750     */
751    public Integer getSubscriptionsCount(final QueryParams params) {
752        FluentCaseInsensitiveStringsMap map = doHEAD(Subscription.SUBSCRIPTION_RESOURCE,  params);
753        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
754    }
755
756    /**
757     * Get the subscriptions for an {@link Account} given query params
758     * <p>
759     * Returns subscriptions associated with an account
760     *
761     * @param accountCode recurly account id
762     * @param state {@link SubscriptionState}
763     * @param params {@link QueryParams}
764     * @return Subscriptions on the account
765     */
766    public Subscriptions getAccountSubscriptions(final String accountCode, final SubscriptionState state, final QueryParams params) {
767        if (state != null) params.put("state", state.getType());
768
769        return doGET(Account.ACCOUNT_RESOURCE
770                        + "/" + accountCode
771                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
772                Subscriptions.class,
773                params);
774    }
775
776    /**
777     * Return all the subscriptions on an invoice.
778     *
779     * @param invoiceId String Recurly Invoice ID
780     * @return all the subscriptions on the invoice
781     */
782    public Subscriptions getInvoiceSubscriptions(final String invoiceId) {
783        return getInvoiceSubscriptions(invoiceId, new QueryParams());
784    }
785
786    /**
787     * Return all the subscriptions on an invoice given query params.
788     *
789     * @param invoiceId String Recurly Invoice ID
790     * @param params {@link QueryParams}
791     * @return all the subscriptions on the invoice
792     */
793    public Subscriptions getInvoiceSubscriptions(final String invoiceId, final QueryParams params) {
794        return doGET(Invoices.INVOICES_RESOURCE
795                        + "/" + invoiceId 
796                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
797                Subscriptions.class, 
798                params);
799    }
800
801    /**
802     * Post usage to subscription
803     * <p>
804     *
805     * @param subscriptionCode The recurly id of the {@link Subscription }
806     * @param addOnCode recurly id of {@link AddOn}
807     * @param usage the usage to post on recurly
808     * @return the {@link Usage} object as identified by the passed in object
809     */
810    public Usage postSubscriptionUsage(final String subscriptionCode, final String addOnCode, final Usage usage) {
811        return doPOST(Subscription.SUBSCRIPTION_RESOURCE +
812                        "/" +
813                        subscriptionCode +
814                        AddOn.ADDONS_RESOURCE +
815                        "/" +
816                        addOnCode +
817                        Usage.USAGE_RESOURCE,
818                usage, Usage.class);
819    }
820
821    /**
822     * Get Subscription Addon Usages
823     * <p>
824     *
825     * @param subscriptionCode The recurly id of the {@link Subscription }
826     * @param addOnCode recurly id of {@link AddOn}
827     * @return {@link Usages} for the specified subscription and addOn
828     */
829    public Usages getSubscriptionUsages(final String subscriptionCode, final String addOnCode, final QueryParams params) {
830       return doGET(Subscription.SUBSCRIPTION_RESOURCE +
831                        "/" +
832                        subscriptionCode +
833                        AddOn.ADDONS_RESOURCE +
834                        "/" +
835                        addOnCode +
836                        Usage.USAGE_RESOURCE, Usages.class, params );
837    }
838
839
840    /**
841     * Get the subscriptions for an account.
842     * This is deprecated. Please use getAccountSubscriptions(String, Subscriptions.State, QueryParams)
843     * <p>
844     * Returns information about a single account.
845     *
846     * @param accountCode recurly account id
847     * @param status      Only accounts in this status will be returned
848     * @return Subscriptions on the account
849     */
850    @Deprecated
851    public Subscriptions getAccountSubscriptions(final String accountCode, final String status) {
852        final QueryParams params = new QueryParams();
853        if (status != null) params.put("state", status);
854
855        return doGET(Account.ACCOUNT_RESOURCE
856                        + "/" + accountCode
857                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
858                Subscriptions.class, params);
859    }
860
861    ////////////////////////////////////////////////////////////////////////////////////////
862
863    /**
864     * Update an account's billing info
865     * <p>
866     * When new or updated credit card information is updated, the billing information is only saved if the credit card
867     * is valid. If the account has a past due invoice, the outstanding balance will be collected to validate the
868     * billing information.
869     * <p>
870     * If the account does not exist before the API request, the account will be created if the billing information
871     * is valid.
872     * <p>
873     * Please note: this API end-point may be used to import billing information without security codes (CVV).
874     * Recurly recommends requiring CVV from your customers when collecting new or updated billing information.
875     *
876     * @param accountCode recurly account id
877     * @param billingInfo billing info object to create or update
878     * @return the newly created or update billing info object on success, null otherwise
879     */
880    public BillingInfo createOrUpdateBillingInfo(final String accountCode, final BillingInfo billingInfo) {
881        return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE,
882                     billingInfo, BillingInfo.class);
883    }
884
885    /**
886     * Update an account's billing info
887     * <p>
888     * When new or updated credit card information is updated, the billing information is only saved if the credit card
889     * is valid. If the account has a past due invoice, the outstanding balance will be collected to validate the
890     * billing information.
891     * <p>
892     * If the account does not exist before the API request, the account will be created if the billing information
893     * is valid.
894     * <p>
895     * Please note: this API end-point may be used to import billing information without security codes (CVV).
896     * Recurly recommends requiring CVV from your customers when collecting new or updated billing information.
897     *
898     * @deprecated Replaced by {@link #createOrUpdateBillingInfo(String, BillingInfo)} Please pass in the account code rather than setting the account on the BillingInfo object
899     *
900     * @param billingInfo billing info object to create or update
901     * @return the newly created or update billing info object on success, null otherwise
902     */
903    @Deprecated
904    public BillingInfo createOrUpdateBillingInfo(final BillingInfo billingInfo) {
905        final String accountCode = billingInfo.getAccount().getAccountCode();
906        // Unset it to avoid confusing Recurly
907        billingInfo.setAccount(null);
908        return doPUT(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE,
909                     billingInfo, BillingInfo.class);
910    }
911
912    /**
913     * Lookup an account's billing info
914     * <p>
915     * Returns only the account's current billing information.
916     *
917     * @param accountCode recurly account id
918     * @return the current billing info object associated with this account on success, null otherwise
919     */
920    public BillingInfo getBillingInfo(final String accountCode) {
921        return doGET(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE,
922                     BillingInfo.class);
923    }
924
925    /**
926     * Clear an account's billing info
927     * <p>
928     * You may remove any stored billing information for an account. If the account has a subscription, the renewal will
929     * go into past due unless you update the billing info before the renewal occurs
930     *
931     * @param accountCode recurly account id
932     */
933    public void clearBillingInfo(final String accountCode) {
934        doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode + BillingInfo.BILLING_INFO_RESOURCE);
935    }
936
937    ///////////////////////////////////////////////////////////////////////////
938    // Account Notes
939
940    /**
941     * List an account's notes
942     * <p>
943     * Returns the account's notes
944     *
945     * @param accountCode recurly account id
946     * @return the notes associated with this account on success, null otherwise
947     */
948    public AccountNotes getAccountNotes(final String accountCode) {
949        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + AccountNotes.ACCOUNT_NOTES_RESOURCE,
950                     AccountNotes.class, new QueryParams());
951    }
952
953    ///////////////////////////////////////////////////////////////////////////
954    // User transactions
955
956    /**
957     * Lookup an account's transactions history
958     * <p>
959     * Returns the account's transaction history
960     *
961     * @param accountCode recurly account id
962     * @return the transaction history associated with this account on success, null otherwise
963     */
964    public Transactions getAccountTransactions(final String accountCode) {
965        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Transactions.TRANSACTIONS_RESOURCE,
966                     Transactions.class, new QueryParams());
967    }
968
969    /**
970     * Lookup an account's transactions history given query params
971     * <p>
972     * Returns the account's transaction history
973     *
974     * @param accountCode recurly account id
975     * @param state {@link TransactionState}
976     * @param type {@link TransactionType}
977     * @param params {@link QueryParams}
978     * @return the transaction history associated with this account on success, null otherwise
979     */
980    public Transactions getAccountTransactions(final String accountCode, final TransactionState state, final TransactionType type, final QueryParams params) {
981        if (state != null) params.put("state", state.getType());
982        if (type != null) params.put("type", type.getType());
983
984        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Transactions.TRANSACTIONS_RESOURCE,
985                Transactions.class, params);
986    }
987
988    /**
989     * Get site's transaction history
990     * <p>
991     * All transactions on the site
992     *
993     * @return the transaction history of the site on success, null otherwise
994     */
995    public Transactions getTransactions() {
996        return doGET(Transactions.TRANSACTIONS_RESOURCE, Transactions.class, new QueryParams());
997    }
998
999    /**
1000     * Get site's transaction history
1001     * <p>
1002     * All transactions on the site
1003     *
1004     * @param state {@link TransactionState}
1005     * @param type {@link TransactionType}
1006     * @param params {@link QueryParams}
1007     * @return the transaction history of the site on success, null otherwise
1008     */
1009    public Transactions getTransactions(final TransactionState state, final TransactionType type, final QueryParams params) {
1010        if (state != null) params.put("state", state.getType());
1011        if (type != null) params.put("type", type.getType());
1012
1013        return doGET(Transactions.TRANSACTIONS_RESOURCE, Transactions.class, params);
1014    }
1015
1016    /**
1017     * Get number of Transactions matching the query params
1018     *
1019     * @param params {@link QueryParams}
1020     * @return Integer on success, null otherwise
1021     */
1022    public Integer getTransactionsCount(final QueryParams params) {
1023        FluentCaseInsensitiveStringsMap map = doHEAD(Transactions.TRANSACTIONS_RESOURCE, params);
1024        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
1025    }
1026
1027    /**
1028     * Lookup a transaction
1029     *
1030     * @param transactionId recurly transaction id
1031     * @return the transaction if found, null otherwise
1032     */
1033    public Transaction getTransaction(final String transactionId) {
1034        if (transactionId == null || transactionId.isEmpty())
1035            throw new RuntimeException("transactionId cannot be empty!");
1036
1037        return doGET(Transactions.TRANSACTIONS_RESOURCE + "/" + transactionId,
1038                     Transaction.class);
1039    }
1040
1041    /**
1042     * Creates a {@link Transaction} through the Recurly API.
1043     *
1044     * @param trans The {@link Transaction} to create
1045     * @return The created {@link Transaction} object
1046     */
1047    public Transaction createTransaction(final Transaction trans) {
1048        return doPOST(Transactions.TRANSACTIONS_RESOURCE, trans, Transaction.class);
1049    }
1050
1051    /**
1052     * Refund a transaction
1053     *
1054     * @param transactionId recurly transaction id
1055     * @param amount        amount to refund, null for full refund
1056     */
1057    public void refundTransaction(final String transactionId, @Nullable final BigDecimal amount) {
1058        String url = Transactions.TRANSACTIONS_RESOURCE + "/" + transactionId;
1059        if (amount != null) {
1060            url = url + "?amount_in_cents=" + (amount.intValue() * 100);
1061        }
1062        doDELETE(url);
1063    }
1064
1065    /**
1066     * Get the subscriptions for a {@link Transaction}.
1067     * <p>
1068     * Returns subscriptions associated with a transaction
1069     *
1070     * @param transactionId recurly transaction id
1071     * @return Subscriptions on the transaction
1072     */
1073    public Subscriptions getTransactionSubscriptions(final String transactionId) {
1074        return doGET(Transactions.TRANSACTIONS_RESOURCE
1075                        + "/" + transactionId
1076                        + Subscriptions.SUBSCRIPTIONS_RESOURCE,
1077                Subscriptions.class,
1078                new QueryParams());
1079    }
1080
1081    ///////////////////////////////////////////////////////////////////////////
1082    // User invoices
1083
1084    /**
1085     * Lookup an invoice
1086     * <p>
1087     * Returns the invoice given an integer id
1088     *
1089     * @deprecated Please switch to using a string for invoice ids
1090     *
1091     * @param invoiceId Recurly Invoice ID
1092     * @return the invoice
1093     */
1094    @Deprecated
1095    public Invoice getInvoice(final Integer invoiceId) {
1096        return getInvoice(invoiceId.toString());
1097    }
1098
1099    /**
1100     * Lookup an invoice given an invoice id
1101     *
1102     * <p>
1103     * Returns the invoice given a string id.
1104     * The invoice may or may not have acountry code prefix (ex: IE1023).
1105     * For more information on invoicing and prefixes, see:
1106     * https://docs.recurly.com/docs/site-settings#section-invoice-prefixing
1107     *
1108     * @param invoiceId String Recurly Invoice ID
1109     * @return the invoice
1110     */
1111    public Invoice getInvoice(final String invoiceId) {
1112        if (invoiceId == null || invoiceId.isEmpty())
1113            throw new RuntimeException("invoiceId cannot be empty!");
1114
1115        return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId, Invoice.class);
1116    }
1117
1118    /**
1119     * Update an invoice
1120     * <p>
1121     * Updates an existing invoice.
1122     *
1123     * @param invoiceId String Recurly Invoice ID
1124     * @return the updated invoice object on success, null otherwise
1125     */
1126    public Invoice updateInvoice(final String invoiceId, final Invoice invoice) {
1127        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId, invoice, Invoice.class);
1128    }
1129
1130    /**
1131     * Fetch invoice pdf
1132     * <p>
1133     * Returns the invoice pdf as an inputStream
1134     *
1135     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1136     *
1137     * @param invoiceId Recurly Invoice ID
1138     * @return the invoice pdf as an inputStream
1139     */
1140    @Deprecated
1141    public InputStream getInvoicePdf(final Integer invoiceId) {
1142        return getInvoicePdf(invoiceId.toString());
1143    }
1144
1145    /**
1146     * Fetch invoice pdf
1147     * <p>
1148     * Returns the invoice pdf as an inputStream
1149     *
1150     * @param invoiceId String Recurly Invoice ID
1151     * @return the invoice pdf as an inputStream
1152     */
1153    public InputStream getInvoicePdf(final String invoiceId) {
1154        if (invoiceId == null || invoiceId.isEmpty())
1155            throw new RuntimeException("invoiceId cannot be empty!");
1156
1157        return doGETPdf(Invoices.INVOICES_RESOURCE + "/" + invoiceId);
1158    }
1159
1160    /**
1161     * Lookup all invoices
1162     * <p>
1163     * Returns all invoices on the site
1164     *
1165     * @return the invoices associated with this site on success, null otherwise
1166     */
1167    public Invoices getInvoices() {
1168        return doGET(Invoices.INVOICES_RESOURCE, Invoices.class, new QueryParams());
1169    }
1170
1171    /**
1172     * Return all the invoices given query params
1173     * <p>
1174     *
1175     * @param params {@link QueryParams}
1176     * @return all invoices matching the query
1177     */
1178    public Invoices getInvoices(final QueryParams params) {
1179        return doGET(Invoices.INVOICES_RESOURCE, Invoices.class, params);
1180    }
1181
1182    /**
1183     * Return all the invoices given query params
1184     * <p>
1185     *
1186     * @param params {@link QueryParams}
1187     * @return the count of invoices matching the query
1188     */
1189    public int getInvoicesCount(final QueryParams params) {
1190        FluentCaseInsensitiveStringsMap map = doHEAD(Invoices.INVOICES_RESOURCE, params);
1191        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
1192    }
1193
1194    /**
1195     * Return all the transactions on an invoice. Only use this endpoint
1196     * if you have more than 500 transactions on an invoice.
1197     * <p>
1198     *
1199     * @param invoiceId String Recurly Invoice ID
1200     * @return all the transactions on the invoice
1201     */
1202    public Transactions getInvoiceTransactions(final String invoiceId) {
1203        return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId + Transactions.TRANSACTIONS_RESOURCE,
1204                     Transactions.class, new QueryParams());
1205    }
1206    
1207    /**
1208     * Lookup an account's invoices
1209     * <p>
1210     * Returns the account's invoices
1211     *
1212     * @param accountCode recurly account id
1213     * @return the invoices associated with this account on success, null otherwise
1214     */
1215    public Invoices getAccountInvoices(final String accountCode) {
1216        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Invoices.INVOICES_RESOURCE,
1217                     Invoices.class, new QueryParams());
1218    }
1219
1220    /**
1221     * Lookup an invoice's original invoices (e.g. a refund invoice has original_invoices)
1222     * <p>
1223     * Returns the invoice's original invoices
1224     *
1225     * @param invoiceId the invoice id
1226     * @return the original invoices associated with this invoice on success. Throws RecurlyAPIError if not found
1227     */
1228    public Invoices getOriginalInvoices(final String invoiceId) {
1229        return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/original_invoices",
1230                    Invoices.class, new QueryParams());
1231    }
1232
1233    /**
1234     * Refund an invoice given an open amount
1235     * <p/>
1236     * Returns the refunded invoice
1237     *
1238     * @deprecated Please use refundInvoice(String, InvoiceRefund)
1239     *
1240     * @param invoiceId The id of the invoice to refund
1241     * @param amountInCents The open amount to refund
1242     * @param method If credit line items exist on the invoice, this parameter specifies which refund method to use first
1243     * @return the refunded invoice
1244     */
1245    @Deprecated
1246    public Invoice refundInvoice(final String invoiceId, final Integer amountInCents, final RefundMethod method) {
1247        final InvoiceRefund invoiceRefund = new InvoiceRefund();
1248        invoiceRefund.setRefundMethod(method);
1249        invoiceRefund.setAmountInCents(amountInCents);
1250
1251        return refundInvoice(invoiceId, invoiceRefund);
1252    }
1253
1254    /**
1255     * Refund an invoice given some line items
1256     * <p/>
1257     * Returns the refunded invoice
1258     *
1259     * @deprecated Please use refundInvoice(String, InvoiceRefund)
1260     *
1261     * @param invoiceId The id of the invoice to refund
1262     * @param lineItems The list of adjustment refund objects
1263     * @param method If credit line items exist on the invoice, this parameter specifies which refund method to use first
1264     * @return the refunded invoice
1265     */
1266    @Deprecated
1267    public Invoice refundInvoice(final String invoiceId, List<AdjustmentRefund> lineItems, final RefundMethod method) {
1268        final InvoiceRefund invoiceRefund = new InvoiceRefund();
1269        invoiceRefund.setRefundMethod(method);
1270        invoiceRefund.setLineItems(lineItems);
1271
1272        return refundInvoice(invoiceId, invoiceRefund);
1273    }
1274
1275    /**
1276     * Refund an invoice given some options
1277     * <p/>
1278     * Returns the refunded invoice
1279     *
1280     * @param invoiceId The id of the invoice to refund
1281     * @param refundOptions The options for the refund
1282     * @return the refunded invoice
1283     */
1284    public Invoice refundInvoice(final String invoiceId, final InvoiceRefund refundOptions) {
1285        return doPOST(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/refund", refundOptions, Invoice.class);
1286    }
1287
1288    /**
1289     * Lookup an account's shipping addresses
1290     * <p>
1291     * Returns the account's shipping addresses
1292     *
1293     * @param accountCode recurly account id
1294     * @return the shipping addresses associated with this account on success, null otherwise
1295     */
1296    public ShippingAddresses getAccountShippingAddresses(final String accountCode) {
1297        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE,
1298                ShippingAddresses.class, new QueryParams());
1299    }
1300
1301    /**
1302     * Get an existing shipping address
1303     * <p>
1304     *
1305     * @param accountCode recurly account id
1306     * @param shippingAddressId the shipping address id to fetch
1307     * @return the newly created shipping address on success
1308     */
1309    public ShippingAddress getShippingAddress(final String accountCode, final long shippingAddressId) {
1310        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId,
1311                ShippingAddress.class);
1312    }
1313
1314    /**
1315     * Create a shipping address on an existing account
1316     * <p>
1317     *
1318     * @param accountCode recurly account id
1319     * @param shippingAddress the shipping address request data
1320     * @return the newly created shipping address on success
1321     */
1322    public ShippingAddress createShippingAddress(final String accountCode, final ShippingAddress shippingAddress) {
1323        return doPOST(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE, shippingAddress,
1324                ShippingAddress.class);
1325    }
1326
1327    /**
1328     * Update an existing shipping address
1329     * <p>
1330     *
1331     * @param accountCode recurly account id
1332     * @param shippingAddressId the shipping address id to update
1333     * @param shippingAddress the shipping address request data
1334     * @return the updated shipping address on success
1335     */
1336    public ShippingAddress updateShippingAddress(final String accountCode, final long shippingAddressId, ShippingAddress shippingAddress) {
1337        return doPUT(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId, shippingAddress,
1338                ShippingAddress.class);
1339    }
1340
1341    /**
1342     * Delete an existing shipping address
1343     * <p>
1344     *
1345     * @param accountCode recurly account id
1346     * @param shippingAddressId the shipping address id to delete
1347     */
1348    public void deleteShippingAddress(final String accountCode, final long shippingAddressId) {
1349        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + ShippingAddresses.SHIPPING_ADDRESSES_RESOURCE + "/" + shippingAddressId);
1350    }
1351
1352    /**
1353     * Lookup an account's invoices given query params
1354     * <p>
1355     * Returns the account's invoices
1356     *
1357     * @param accountCode recurly account id
1358     * @param state {@link InvoiceState} state of the invoices
1359     * @param params {@link QueryParams}
1360     * @return the invoices associated with this account on success, null otherwise
1361     */
1362    public Invoices getAccountInvoices(final String accountCode, final InvoiceState state, final QueryParams params) {
1363        if (state != null) params.put("state", state.getType());
1364        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Invoices.INVOICES_RESOURCE,
1365                Invoices.class, params);
1366    }
1367
1368    /**
1369     * Post an invoice: invoice pending charges on an account
1370     * <p>
1371     * Returns an invoice collection
1372     *
1373     * @param accountCode
1374     * @return the invoice collection that was generated on success, null otherwise
1375     */
1376    public InvoiceCollection postAccountInvoice(final String accountCode, final Invoice invoice) {
1377        return doPOST(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Invoices.INVOICES_RESOURCE, invoice, InvoiceCollection.class);
1378    }
1379
1380    /**
1381     * Mark an invoice as paid successfully - Recurly Enterprise Feature
1382     *
1383     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1384     *
1385     * @param invoiceId Recurly Invoice ID
1386     */
1387    @Deprecated
1388    public Invoice markInvoiceSuccessful(final Integer invoiceId) {
1389        return markInvoiceSuccessful(invoiceId.toString());
1390    }
1391
1392    /**
1393     * Mark an invoice as paid successfully - Recurly Enterprise Feature
1394     *
1395     * @param invoiceId String Recurly Invoice ID
1396     */
1397    public Invoice markInvoiceSuccessful(final String invoiceId) {
1398        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/mark_successful", null, Invoice.class);
1399    }
1400
1401    /**
1402     * Mark an invoice as failed collection
1403     *
1404     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1405     *
1406     * @param invoiceId Recurly Invoice ID
1407     */
1408    @Deprecated
1409    public InvoiceCollection markInvoiceFailed(final Integer invoiceId) {
1410        return markInvoiceFailed(invoiceId.toString());
1411    }
1412
1413    /**
1414     * Mark an invoice as failed collection
1415     *
1416     * @param invoiceId String Recurly Invoice ID
1417     */
1418    public InvoiceCollection markInvoiceFailed(final String invoiceId) {
1419        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/mark_failed", null, InvoiceCollection.class);
1420    }
1421
1422    /**
1423     * Force collect an invoice
1424     *
1425     * @param invoiceId String Recurly Invoice ID
1426     */
1427    public Invoice forceCollectInvoice(final String invoiceId) {
1428        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/collect", null, Invoice.class);
1429    }
1430
1431    /**
1432     * Force collect an invoice
1433     *
1434     * @param transactionType String The gateway transaction type. Currency accepts value "moto".
1435     * @param invoiceId String Recurly Invoice ID
1436     */
1437    public Invoice forceCollectInvoice(final String invoiceId, final String transactionType) {
1438        Invoice request = new Invoice();
1439        request.setTransactionType(transactionType);
1440        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/collect", request, Invoice.class);
1441    }
1442
1443    /**
1444     * Void Invoice
1445     *
1446     * @param invoiceId String Recurly Invoice ID
1447     */
1448    public Invoice voidInvoice(final String invoiceId) {
1449        return doPUT(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/void", null, Invoice.class);
1450    }
1451
1452    /**
1453     * Enter an offline payment for a manual invoice (beta) - Recurly Enterprise Feature
1454     *
1455     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1456     *
1457     * @param invoiceId Recurly Invoice ID
1458     * @param payment   The external payment
1459     */
1460    @Deprecated
1461    public Transaction enterOfflinePayment(final Integer invoiceId, final Transaction payment) {
1462        return enterOfflinePayment(invoiceId.toString(), payment);
1463    }
1464
1465    /**
1466     * Enter an offline payment for a manual invoice (beta) - Recurly Enterprise Feature
1467     *
1468     * @param invoiceId String Recurly Invoice ID
1469     * @param payment   The external payment
1470     */
1471    public Transaction enterOfflinePayment(final String invoiceId, final Transaction payment) {
1472        return doPOST(Invoices.INVOICES_RESOURCE + "/" + invoiceId + "/transactions", payment, Transaction.class);
1473    }
1474
1475    ///////////////////////////////////////////////////////////////////////////
1476
1477    /**
1478     * Create an Item's info
1479     * <p>
1480     *
1481     * @param item The item to create on recurly
1482     * @return the item object as identified by the passed in ID
1483     */
1484    public Item createItem(final Item item) {
1485        return doPOST(Item.ITEMS_RESOURCE, item, Item.class);
1486    }
1487
1488    /**
1489     * Update an Item's info
1490     * <p>
1491     *
1492     * @param item The Item to update on recurly
1493     * @return the updated item object
1494     */
1495    public Item updateItem(final String itemCode, final Item item) {
1496        return doPUT(Item.ITEMS_RESOURCE + "/" + itemCode, item, Item.class);
1497    }
1498
1499    /**
1500     * Get a Item's details
1501     * <p>
1502     *
1503     * @param itemCode recurly id of item
1504     * @return the item object as identified by the passed in ID
1505     */
1506    public Item getItem(final String itemCode) {
1507        if (itemCode == null || itemCode.isEmpty())
1508            throw new RuntimeException("itemCode cannot be empty!");
1509
1510        return doGET(Item.ITEMS_RESOURCE + "/" + itemCode, Item.class);
1511    }
1512
1513    /**
1514     * Return all the items
1515     * <p>
1516     *
1517     * @return the item object as identified by the passed in ID
1518     */
1519    public Items getItems() {
1520        return doGET(Items.ITEMS_RESOURCE, Items.class, new QueryParams());
1521    }
1522
1523    /**
1524     * Deletes a {@link Item}
1525     * <p>
1526     *
1527     * @param itemCode The {@link Item} object to delete.
1528     */
1529    public void deleteItem(final String itemCode) {
1530        doDELETE(Item.ITEMS_RESOURCE +
1531                "/" +
1532                itemCode);
1533    }
1534
1535    /**
1536     * Reactivating a canceled item
1537     * <p>
1538     * Reactivate a canceled item.
1539     *
1540     * @param item Item object
1541     * @return Item
1542     */
1543    public Item reactivateItem(final String itemCode) {
1544        return doPUT(Item.ITEMS_RESOURCE + "/" + itemCode + "/reactivate",
1545                null, Item.class);
1546    }
1547
1548    ///////////////////////////////////////////////////////////////////////////
1549
1550    /**
1551     * Create a Plan's info
1552     * <p>
1553     *
1554     * @param plan The plan to create on recurly
1555     * @return the plan object as identified by the passed in ID
1556     */
1557    public Plan createPlan(final Plan plan) {
1558        return doPOST(Plan.PLANS_RESOURCE, plan, Plan.class);
1559    }
1560
1561    /**
1562     * Update a Plan's info
1563     * <p>
1564     *
1565     * @param plan The plan to update on recurly
1566     * @return the updated plan object
1567     */
1568    public Plan updatePlan(final Plan plan) {
1569        return doPUT(Plan.PLANS_RESOURCE + "/" + plan.getPlanCode(), plan, Plan.class);
1570    }
1571
1572    /**
1573     * Get a Plan's details
1574     * <p>
1575     *
1576     * @param planCode recurly id of plan
1577     * @return the plan object as identified by the passed in ID
1578     */
1579    public Plan getPlan(final String planCode) {
1580        if (planCode == null || planCode.isEmpty())
1581            throw new RuntimeException("planCode cannot be empty!");
1582
1583        return doGET(Plan.PLANS_RESOURCE + "/" + planCode, Plan.class);
1584    }
1585
1586    /**
1587     * Return all the plans
1588     * <p>
1589     *
1590     * @return the plan object as identified by the passed in ID
1591     */
1592    public Plans getPlans() {
1593        return doGET(Plans.PLANS_RESOURCE, Plans.class, new QueryParams());
1594    }
1595
1596    /**
1597     * Return all the plans given query params
1598     * <p>
1599     *
1600     * @param params {@link QueryParams}
1601     * @return the plan object as identified by the passed in ID
1602     */
1603    public Plans getPlans(final QueryParams params) {
1604        return doGET(Plans.PLANS_RESOURCE, Plans.class, params);
1605    }
1606
1607    /**
1608     * Get number of Plans matching the query params
1609     *
1610     * @param params {@link QueryParams}
1611     * @return Integer on success, null otherwise
1612     */
1613    public Integer getPlansCount(final QueryParams params) {
1614        FluentCaseInsensitiveStringsMap map = doHEAD(Plans.PLANS_RESOURCE, params);
1615        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
1616    }
1617
1618    /**
1619     * Deletes a {@link Plan}
1620     * <p>
1621     *
1622     * @param planCode The {@link Plan} object to delete.
1623     */
1624    public void deletePlan(final String planCode) {
1625        doDELETE(Plan.PLANS_RESOURCE +
1626                 "/" +
1627                 planCode);
1628    }
1629
1630    ///////////////////////////////////////////////////////////////////////////
1631
1632    /**
1633     * Create an AddOn to a Plan
1634     * <p>
1635     *
1636     * @param planCode The planCode of the {@link Plan } to create within recurly
1637     * @param addOn    The {@link AddOn} to create within recurly
1638     * @return the {@link AddOn} object as identified by the passed in object
1639     */
1640    public AddOn createPlanAddOn(final String planCode, final AddOn addOn) {
1641        return doPOST(Plan.PLANS_RESOURCE +
1642                      "/" +
1643                      planCode +
1644                      AddOn.ADDONS_RESOURCE,
1645                      addOn, AddOn.class);
1646    }
1647
1648    /**
1649     * Get an AddOn's details
1650     * <p>
1651     *
1652     * @param addOnCode recurly id of {@link AddOn}
1653     * @param planCode  recurly id of {@link Plan}
1654     * @return the {@link AddOn} object as identified by the passed in plan and add-on IDs
1655     */
1656    public AddOn getAddOn(final String planCode, final String addOnCode) {
1657        if (addOnCode == null || addOnCode.isEmpty())
1658            throw new RuntimeException("addOnCode cannot be empty!");
1659
1660        return doGET(Plan.PLANS_RESOURCE +
1661                     "/" +
1662                     planCode +
1663                     AddOn.ADDONS_RESOURCE +
1664                     "/" +
1665                     addOnCode, AddOn.class);
1666    }
1667
1668    /**
1669     * Return all the {@link AddOn} for a {@link Plan}
1670     * <p>
1671     *
1672     * @param planCode
1673     * @return the {@link AddOn} objects as identified by the passed plan ID
1674     */
1675    public AddOns getAddOns(final String planCode) {
1676        return doGET(Plan.PLANS_RESOURCE +
1677                "/" +
1678                planCode +
1679                AddOn.ADDONS_RESOURCE,
1680                AddOns.class,
1681                new QueryParams());
1682    }
1683
1684    /**
1685     * Return all the {@link AddOn} for a {@link Plan}
1686     * <p>
1687     *
1688     * @param planCode
1689     * @param params {@link QueryParams}
1690     * @return the {@link AddOn} objects as identified by the passed plan ID
1691     */
1692    public AddOns getAddOns(final String planCode, final QueryParams params) {
1693        return doGET(Plan.PLANS_RESOURCE +
1694                "/" +
1695                planCode +
1696                AddOn.ADDONS_RESOURCE,
1697                AddOns.class,
1698                params);
1699    }
1700
1701    /**
1702     * Deletes an {@link AddOn} for a Plan
1703     * <p>
1704     *
1705     * @param planCode  The {@link Plan} object.
1706     * @param addOnCode The {@link AddOn} object to delete.
1707     */
1708    public void deleteAddOn(final String planCode, final String addOnCode) {
1709        doDELETE(Plan.PLANS_RESOURCE +
1710                 "/" +
1711                 planCode +
1712                 AddOn.ADDONS_RESOURCE +
1713                 "/" +
1714                 addOnCode);
1715    }
1716
1717    /**
1718     * Updates an {@link AddOn} for a Plan
1719     * <p>
1720     *
1721     * @param planCode  The {@link Plan} object.
1722     * @param addOnCode The {@link AddOn} object to update.
1723     * @param addOn The updated {@link AddOn} data.
1724     *
1725     * @return the updated {@link AddOn} object.
1726     */
1727    public AddOn updateAddOn(final String planCode, final String addOnCode, final AddOn addOn) {
1728        return doPUT(Plan.PLANS_RESOURCE +
1729                "/" +
1730                planCode +
1731                AddOn.ADDONS_RESOURCE +
1732                "/" +
1733                addOnCode,
1734                addOn,
1735                AddOn.class);
1736    }
1737
1738    ///////////////////////////////////////////////////////////////////////////
1739
1740    /**
1741     * Create a {@link Coupon}
1742     * <p>
1743     *
1744     * @param coupon The coupon to create on recurly
1745     * @return the {@link Coupon} object
1746     */
1747    public Coupon createCoupon(final Coupon coupon) {
1748        return doPOST(Coupon.COUPON_RESOURCE, coupon, Coupon.class);
1749    }
1750
1751    /**
1752     * Get a Coupon
1753     * <p>
1754     *
1755     * @param couponCode The code for the {@link Coupon}
1756     * @return The {@link Coupon} object as identified by the passed in code
1757     */
1758    public Coupon getCoupon(final String couponCode) {
1759        if (couponCode == null || couponCode.isEmpty())
1760            throw new RuntimeException("couponCode cannot be empty!");
1761
1762        return doGET(Coupon.COUPON_RESOURCE + "/" + couponCode, Coupon.class);
1763    }
1764
1765    /**
1766     * Delete a {@link Coupon}
1767     * <p>
1768     *
1769     * @param couponCode The code for the {@link Coupon}
1770     */
1771    public void deleteCoupon(final String couponCode) {
1772        doDELETE(Coupon.COUPON_RESOURCE + "/" + couponCode);
1773    }
1774
1775    ///////////////////////////////////////////////////////////////////////////
1776
1777    /**
1778     * Redeem a {@link Coupon} on an account.
1779     *
1780     * @param couponCode redeemed coupon id
1781     * @return the {@link Coupon} object
1782     */
1783    public Redemption redeemCoupon(final String couponCode, final Redemption redemption) {
1784        return doPOST(Coupon.COUPON_RESOURCE + "/" + couponCode + Redemption.REDEEM_RESOURCE,
1785                      redemption, Redemption.class);
1786    }
1787
1788    /**
1789     * Lookup the first coupon redemption on an account.
1790     *
1791     * @param accountCode recurly account id
1792     * @return the coupon redemption for this account on success, null otherwise
1793     */
1794    public Redemption getCouponRedemptionByAccount(final String accountCode) {
1795        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTION_RESOURCE,
1796                     Redemption.class);
1797    }
1798
1799    /**
1800     * Lookup all coupon redemptions on an account.
1801     *
1802     * @param accountCode recurly account id
1803     * @return the coupon redemptions for this account on success, null otherwise
1804     */
1805    public Redemptions getCouponRedemptionsByAccount(final String accountCode) {
1806        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTIONS_RESOURCE,
1807                Redemptions.class, new QueryParams());
1808    }
1809
1810    /**
1811     * Lookup all coupon redemptions on an account given query params.
1812     *
1813     * @param accountCode recurly account id
1814     * @param params {@link QueryParams}
1815     * @return the coupon redemptions for this account on success, null otherwise
1816     */
1817    public Redemptions getCouponRedemptionsByAccount(final String accountCode, final QueryParams params) {
1818        return doGET(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTIONS_RESOURCE,
1819                Redemptions.class, params);
1820    }
1821
1822    /**
1823     * Lookup the first coupon redemption on an invoice.
1824     *
1825     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1826     *
1827     * @param invoiceNumber invoice number
1828     * @return the coupon redemption for this invoice on success, null otherwise
1829     */
1830    @Deprecated
1831    public Redemption getCouponRedemptionByInvoice(final Integer invoiceNumber) {
1832        return getCouponRedemptionByInvoice(invoiceNumber.toString());
1833    }
1834
1835    /**
1836     * Lookup the first coupon redemption on an invoice.
1837     *
1838     * @param invoiceId String invoice id
1839     * @return the coupon redemption for this invoice on success, null otherwise
1840     */
1841    public Redemption getCouponRedemptionByInvoice(final String invoiceId) {
1842        return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId + Redemption.REDEMPTION_RESOURCE,
1843                Redemption.class);
1844    }
1845
1846
1847    /**
1848     * Lookup all coupon redemptions on an invoice.
1849     *
1850     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1851     *
1852     * @param invoiceNumber invoice number
1853     * @return the coupon redemptions for this invoice on success, null otherwise
1854     */
1855    @Deprecated
1856    public Redemptions getCouponRedemptionsByInvoice(final Integer invoiceNumber) {
1857        return getCouponRedemptionsByInvoice(invoiceNumber.toString(), new QueryParams());
1858    }
1859
1860    /**
1861     * Lookup all coupon redemptions on an invoice.
1862     *
1863     * @param invoiceId String invoice id
1864     * @return the coupon redemptions for this invoice on success, null otherwise
1865     */
1866    public Redemptions getCouponRedemptionsByInvoice(final String invoiceId) {
1867        return getCouponRedemptionsByInvoice(invoiceId, new QueryParams());
1868    }
1869
1870    /**
1871     * Lookup all coupon redemptions on an invoice given query params.
1872     *
1873     * @deprecated Prefer using Invoice#getId() as the id param (which is a String)
1874     *
1875     * @param invoiceNumber invoice number
1876     * @param params {@link QueryParams}
1877     * @return the coupon redemptions for this invoice on success, null otherwise
1878     */
1879    @Deprecated
1880    public Redemptions getCouponRedemptionsByInvoice(final Integer invoiceNumber, final QueryParams params) {
1881        return getCouponRedemptionsByInvoice(invoiceNumber.toString(), params);
1882    }
1883
1884    /**
1885     * Lookup all coupon redemptions on an invoice given query params.
1886     *
1887     * @param invoiceId String invoice id
1888     * @param params {@link QueryParams}
1889     * @return the coupon redemptions for this invoice on success, null otherwise
1890     */
1891    public Redemptions getCouponRedemptionsByInvoice(final String invoiceId, final QueryParams params) {
1892        return doGET(Invoices.INVOICES_RESOURCE + "/" + invoiceId + Redemption.REDEMPTIONS_RESOURCE,
1893                Redemptions.class, params);
1894    }
1895
1896    /**
1897     * Lookup all coupon redemptions on a subscription given query params.
1898     *
1899     * @param subscriptionUuid String subscription uuid
1900     * @param params {@link QueryParams}
1901     * @return the coupon redemptions for this subscription on success, null otherwise
1902     */
1903    public Redemptions getCouponRedemptionsBySubscription(final String subscriptionUuid, final QueryParams params) {
1904        return doGET(Subscription.SUBSCRIPTION_RESOURCE + "/" + subscriptionUuid + Redemptions.REDEMPTIONS_RESOURCE,
1905                Redemptions.class, params);
1906    }
1907
1908    /**
1909     * Deletes a coupon redemption from an account.
1910     *
1911     * @param accountCode recurly account id
1912     */
1913    public void deleteCouponRedemption(final String accountCode) {
1914        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTION_RESOURCE);
1915    }
1916
1917    /**
1918     * Deletes a specific redemption.
1919     *
1920     * @param accountCode recurly account id
1921     * @param redemptionUuid recurly coupon redemption uuid
1922     */
1923    public void deleteCouponRedemption(final String accountCode, final String redemptionUuid) {
1924        doDELETE(Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + Redemption.REDEMPTIONS_RESOURCE + "/" + redemptionUuid);
1925    }
1926
1927    /**
1928     * Generates unique codes for a bulk coupon.
1929     *
1930     * @param couponCode recurly coupon code (must have been created as type: bulk)
1931     * @param coupon A coupon with number of unique codes set
1932     */
1933    public Coupons generateUniqueCodes(final String couponCode, final Coupon coupon) {
1934        Coupons coupons = doPOST(Coupon.COUPON_RESOURCE + "/" + couponCode + Coupon.GENERATE_RESOURCE, coupon, Coupons.class);
1935        return coupons.getStart();
1936    }
1937
1938    /**
1939     * Lookup all unique codes for a bulk coupon given query params.
1940     *
1941     * @param couponCode String coupon code
1942     * @param params {@link QueryParams}
1943     * @return the unique coupon codes for the coupon code on success, null otherwise
1944     */
1945    public Coupons getUniqueCouponCodes(final String couponCode, final QueryParams params) {
1946        return doGET(Coupon.COUPON_RESOURCE + "/" + couponCode + Coupon.UNIQUE_CODES_RESOURCE,
1947                Coupons.class, params);
1948    }
1949
1950    ///////////////////////////////////////////////////////////////////////////
1951    //
1952    // Recurly.js API
1953    //
1954    ///////////////////////////////////////////////////////////////////////////
1955
1956    /**
1957     * Fetch Subscription
1958     * <p>
1959     * Returns subscription from a recurly.js token.
1960     *
1961     * @param recurlyToken token given by recurly.js
1962     * @return subscription object on success, null otherwise
1963     */
1964    public Subscription fetchSubscription(final String recurlyToken) {
1965        return fetch(recurlyToken, Subscription.class);
1966    }
1967
1968    /**
1969     * Fetch BillingInfo
1970     * <p>
1971     * Returns billing info from a recurly.js token.
1972     *
1973     * @param recurlyToken token given by recurly.js
1974     * @return billing info object on success, null otherwise
1975     */
1976    public BillingInfo fetchBillingInfo(final String recurlyToken) {
1977        return fetch(recurlyToken, BillingInfo.class);
1978    }
1979
1980    /**
1981     * Fetch Invoice
1982     * <p>
1983     * Returns invoice from a recurly.js token.
1984     *
1985     * @param recurlyToken token given by recurly.js
1986     * @return invoice object on success, null otherwise
1987     */
1988    public Invoice fetchInvoice(final String recurlyToken) {
1989        return fetch(recurlyToken, Invoice.class);
1990    }
1991
1992    /**
1993     * Get Gift Cards given query params
1994     * <p>
1995     * Returns information about all gift cards.
1996     *
1997     * @param params {@link QueryParams}
1998     * @return gitfcards object on success, null otherwise
1999     */
2000    public GiftCards getGiftCards(final QueryParams params) {
2001        return doGET(GiftCards.GIFT_CARDS_RESOURCE, GiftCards.class, params);
2002    }
2003
2004    /**
2005     * Get Gift Cards
2006     * <p>
2007     * Returns information about all gift cards.
2008     *
2009     * @return gitfcards object on success, null otherwise
2010     */
2011    public GiftCards getGiftCards() {
2012        return doGET(GiftCards.GIFT_CARDS_RESOURCE, GiftCards.class, new QueryParams());
2013    }
2014
2015    /**
2016     * Get number of GiftCards matching the query params
2017     *
2018     * @param params {@link QueryParams}
2019     * @return Integer on success, null otherwise
2020     */
2021    public Integer getGiftCardsCount(final QueryParams params) {
2022        FluentCaseInsensitiveStringsMap map = doHEAD(GiftCards.GIFT_CARDS_RESOURCE, params);
2023        return Integer.parseInt(map.getFirstValue(X_RECORDS_HEADER_NAME));
2024    }
2025
2026    /**
2027     * Get a Gift Card
2028     * <p>
2029     *
2030     * @param giftCardId The id for the {@link GiftCard}
2031     * @return The {@link GiftCard} object as identified by the passed in id
2032     */
2033    public GiftCard getGiftCard(final Long giftCardId) {
2034        return doGET(GiftCards.GIFT_CARDS_RESOURCE + "/" + Long.toString(giftCardId), GiftCard.class);
2035    }
2036
2037    /**
2038     * Redeem a Gift Card
2039     * <p>
2040     *
2041     * @param redemptionCode The redemption code the {@link GiftCard}
2042     * @param accountCode The account code for the {@link Account}
2043     * @return The updated {@link GiftCard} object as identified by the passed in id
2044     */
2045    public GiftCard redeemGiftCard(final String redemptionCode, final String accountCode) {
2046        final GiftCard.Redemption redemptionData = GiftCard.createRedemption(accountCode);
2047        final String url = GiftCards.GIFT_CARDS_RESOURCE + "/" + redemptionCode + "/redeem";
2048
2049        return doPOST(url, redemptionData, GiftCard.class);
2050    }
2051
2052    /**
2053     * Purchase a GiftCard
2054     * <p>
2055     *
2056     * @param giftCard The giftCard data
2057     * @return the giftCard object
2058     */
2059    public GiftCard purchaseGiftCard(final GiftCard giftCard) {
2060        return doPOST(GiftCards.GIFT_CARDS_RESOURCE, giftCard, GiftCard.class);
2061    }
2062
2063    /**
2064     * Preview a GiftCard
2065     * <p>
2066     *
2067     * @param giftCard The giftCard data
2068     * @return the giftCard object
2069     */
2070    public GiftCard previewGiftCard(final GiftCard giftCard) {
2071        return doPOST(GiftCards.GIFT_CARDS_RESOURCE + "/preview", giftCard, GiftCard.class);
2072    }
2073
2074    /**
2075     * Return all the MeasuredUnits
2076     * <p>
2077     *
2078     * @return the MeasuredUnits object as identified by the passed in ID
2079     */
2080    public MeasuredUnits getMeasuredUnits() {
2081        return doGET(MeasuredUnits.MEASURED_UNITS_RESOURCE, MeasuredUnits.class, new QueryParams());
2082    }
2083
2084    /**
2085     * Create a MeasuredUnit's info
2086     * <p>
2087     *
2088     * @param measuredUnit The measuredUnit to create on recurly
2089     * @return the measuredUnit object as identified by the passed in ID
2090     */
2091    public MeasuredUnit createMeasuredUnit(final MeasuredUnit measuredUnit) {
2092        return doPOST(MeasuredUnit.MEASURED_UNITS_RESOURCE, measuredUnit, MeasuredUnit.class);
2093    }
2094
2095    /**
2096     * Purchases endpoint
2097     * <p>
2098     * https://dev.recurly.com/docs/create-purchase
2099     *
2100     * @param purchase The purchase data
2101     * @return The created invoice collection
2102     */
2103    public InvoiceCollection purchase(final Purchase purchase) {
2104        return doPOST(Purchase.PURCHASES_ENDPOINT, purchase, InvoiceCollection.class);
2105    }
2106
2107    /**
2108     * Purchases preview endpoint
2109     * <p>
2110     * https://dev.recurly.com/docs/preview-purchase
2111     *
2112     * @param purchase The purchase data
2113     * @return The preview invoice collection
2114     */
2115    public InvoiceCollection previewPurchase(final Purchase purchase) {
2116        return doPOST(Purchase.PURCHASES_ENDPOINT + "/preview", purchase, InvoiceCollection.class);
2117    }
2118
2119    /**
2120     * Purchases authorize endpoint.
2121     *
2122     * Generate an authorized invoice for the purchase. Runs validations
2123     + but does not run any transactions. This endpoint will create a
2124     + pending purchase that can be activated at a later time once payment
2125     + has been completed on an external source (e.g. Adyen's Hosted
2126     + Payment Pages).
2127     *
2128     * <p>
2129     * https://dev.recurly.com/docs/authorize-purchase
2130     *
2131     * @param purchase The purchase data
2132     * @return The authorized invoice collection
2133     */
2134    public InvoiceCollection authorizePurchase(final Purchase purchase) {
2135        return doPOST(Purchase.PURCHASES_ENDPOINT + "/authorize", purchase, InvoiceCollection.class);
2136    }
2137
2138    /**
2139     * Purchases pending endpoint.
2140     *
2141     * Use for Adyen HPP transaction requests. Runs validations
2142     + but does not run any transactions.
2143     *
2144     * <p>
2145     * https://dev.recurly.com/docs/pending-purchase
2146     *
2147     * @param purchase The purchase data
2148     * @return The authorized invoice collection
2149     */
2150    public InvoiceCollection pendingPurchase(final Purchase purchase) {
2151        return doPOST(Purchase.PURCHASES_ENDPOINT + "/pending", purchase, InvoiceCollection.class);
2152    }
2153
2154    /**
2155     * Sets the acquisition details for an account
2156     * <p>
2157     * https://dev.recurly.com/docs/create-account-acquisition
2158     *
2159     * @param accountCode The account's account code
2160     * @param acquisition The AccountAcquisition data
2161     * @return The created AccountAcquisition object
2162     */
2163    public AccountAcquisition createAccountAcquisition(final String accountCode, final AccountAcquisition acquisition) {
2164        final String path = Account.ACCOUNT_RESOURCE + "/" + accountCode + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2165        return doPOST(path, acquisition, AccountAcquisition.class);
2166    }
2167
2168    /**
2169     * Gets the acquisition details for an account
2170     * <p>
2171     * https://dev.recurly.com/docs/create-account-acquisition
2172     *
2173     * @param accountCode The account's account code
2174     * @return The created AccountAcquisition object
2175     */
2176    public AccountAcquisition getAccountAcquisition(final String accountCode) {
2177        final String path = Account.ACCOUNT_RESOURCE + "/" + accountCode + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2178        return doGET(path, AccountAcquisition.class);
2179    }
2180
2181    /**
2182     * Updates the acquisition details for an account
2183     * <p>
2184     * https://dev.recurly.com/docs/update-account-acquisition
2185     *
2186     * @param accountCode The account's account code
2187     * @param acquisition The AccountAcquisition data
2188     * @return The created AccountAcquisition object
2189     */
2190    public AccountAcquisition updateAccountAcquisition(final String accountCode, final AccountAcquisition acquisition) {
2191        final String path = Account.ACCOUNT_RESOURCE + "/" + accountCode + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE;
2192        return doPUT(path, acquisition, AccountAcquisition.class);
2193    }
2194
2195    /**
2196     * Clear the acquisition details for an account
2197     * <p>
2198     * https://dev.recurly.com/docs/clear-account-acquisition
2199     *
2200     * @param accountCode The account's account code
2201     */
2202    public void deleteAccountAcquisition(final String accountCode) {
2203        doDELETE(Account.ACCOUNT_RESOURCE + "/" + accountCode + AccountAcquisition.ACCOUNT_ACQUISITION_RESOURCE);
2204    }
2205
2206
2207    /**
2208     * Get Credit Payments
2209     * <p>
2210     * Returns information about all credit payments.
2211     *
2212     * @return CreditPayments on success, null otherwise
2213     */
2214    public CreditPayments getCreditPayments() {
2215        return doGET(CreditPayments.CREDIT_PAYMENTS_RESOURCE, CreditPayments.class, new QueryParams());
2216    }
2217
2218    /**
2219     * Get Credit Payments
2220     * <p>
2221     * Returns information about all credit payments.
2222     *
2223     * @param params {@link QueryParams}
2224     * @return CreditPayments on success, null otherwise
2225     */
2226    public CreditPayments getCreditPayments(final QueryParams params) {
2227        return doGET(CreditPayments.CREDIT_PAYMENTS_RESOURCE, CreditPayments.class, params);
2228    }
2229
2230    /**
2231     * Get Credit Payments for a given account
2232     * <p>
2233     * Returns information about all credit payments.
2234     *
2235     * @param accountCode The account code to filter
2236     * @param params {@link QueryParams}
2237     * @return CreditPayments on success, null otherwise
2238     */
2239    public CreditPayments getCreditPayments(final String accountCode, final QueryParams params) {
2240        final String path = Accounts.ACCOUNTS_RESOURCE + "/" + accountCode + CreditPayments.CREDIT_PAYMENTS_RESOURCE;
2241        return doGET(path, CreditPayments.class, params);
2242    }
2243
2244    /**
2245     * Get Shipping Methods for the site
2246     * <p>
2247     * https://dev.recurly.com/docs/list-shipping-methods
2248     *
2249     * @return ShippingMethods on success, null otherwise
2250     */
2251    public ShippingMethods getShippingMethods() {
2252        return doGET(ShippingMethods.SHIPPING_METHODS_RESOURCE, ShippingMethods.class, new QueryParams());
2253    }
2254
2255    /**
2256     * Get Shipping Methods for the site
2257     * <p>
2258     * https://dev.recurly.com/docs/list-shipping-methods
2259     *
2260     * @param params {@link QueryParams}
2261     * @return ShippingMethods on success, null otherwise
2262     */
2263    public ShippingMethods getShippingMethods(final QueryParams params) {
2264        return doGET(ShippingMethods.SHIPPING_METHODS_RESOURCE, ShippingMethods.class, params);
2265    }
2266
2267    /**
2268     * Look up a shipping method
2269     * <p>
2270     * https://dev.recurly.com/docs/lookup-shipping-method
2271     *
2272     * @param shippingMethodCode The code for the {@link ShippingMethod}
2273     * @return The {@link ShippingMethod} object as identified by the passed in code
2274     */
2275    public ShippingMethod getShippingMethod(final String shippingMethodCode) {
2276        if (shippingMethodCode == null || shippingMethodCode.isEmpty())
2277            throw new RuntimeException("shippingMethodCode cannot be empty!");
2278
2279        return doGET(ShippingMethod.SHIPPING_METHOD_RESOURCE + "/" + shippingMethodCode, ShippingMethod.class);
2280    }
2281
2282    private <T> T fetch(final String recurlyToken, final Class<T> clazz) {
2283        return doGET(FETCH_RESOURCE + "/" + recurlyToken, clazz);
2284    }
2285
2286    ///////////////////////////////////////////////////////////////////////////
2287
2288    private InputStream doGETPdf(final String resource) {
2289        return doGETPdfWithFullURL(baseUrl + resource);
2290    }
2291
2292    private <T> T doGET(final String resource, final Class<T> clazz) {
2293        return doGETWithFullURL(clazz, baseUrl + resource);
2294    }
2295
2296    private <T> T doGET(final String resource, final Class<T> clazz, QueryParams params) {
2297        return doGETWithFullURL(clazz, constructUrl(resource, params));
2298    }
2299
2300    private String constructUrl(final String resource, QueryParams params) {
2301        return baseUrl + resource + params.toString();
2302    }
2303
2304    public <T> T doGETWithFullURL(final Class<T> clazz, final String url) {
2305        if (debug()) {
2306            log.info("Msg to Recurly API [GET] :: URL : {}", url);
2307        }
2308        validateHost(url);
2309        return callRecurlySafeXmlContent(client.prepareGet(url), clazz);
2310    }
2311
2312    private InputStream doGETPdfWithFullURL(final String url) {
2313        if (debug()) {
2314            log.info(" [GET] :: URL : {}", url);
2315        }
2316
2317        return callRecurlySafeGetPdf(url);
2318    }
2319
2320    private InputStream callRecurlySafeGetPdf(String url) {
2321        validateHost(url);
2322
2323        final Response response;
2324        final InputStream pdfInputStream;
2325        try {
2326            response = clientRequestBuilderCommon(client.prepareGet(url))
2327                    .addHeader("Accept", "application/pdf")
2328                    .addHeader("Content-Type", "application/pdf")
2329                    .execute()
2330                    .get();
2331            pdfInputStream = response.getResponseBodyAsStream();
2332
2333        } catch (InterruptedException e) {
2334            log.error("Interrupted while calling recurly", e);
2335            return null;
2336        } catch (ExecutionException e) {
2337            log.error("Execution error", e);
2338            return null;
2339        } catch (IOException e) {
2340            log.error("Error retrieving response body", e);
2341            return null;
2342        }
2343
2344        if (response.getStatusCode() != 200) {
2345            final RecurlyAPIError recurlyAPIError = RecurlyAPIError.buildFromResponse(response);
2346            throw new RecurlyAPIException(recurlyAPIError);
2347        }
2348
2349        return pdfInputStream;
2350    }
2351
2352    private <T> T doPOST(final String resource, final RecurlyObject payload, final Class<T> clazz) {
2353        final String xmlPayload;
2354        try {
2355            xmlPayload = xmlMapper.writeValueAsString(payload);
2356            if (debug()) {
2357                log.info("Msg to Recurly API [POST]:: URL : {}", baseUrl + resource);
2358                log.info("Payload for [POST]:: {}", xmlPayload);
2359            }
2360        } catch (IOException e) {
2361            log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
2362            return null;
2363        }
2364
2365        validateHost(baseUrl + resource);
2366
2367        return callRecurlySafeXmlContent(client.preparePost(baseUrl + resource).setBody(xmlPayload), clazz);
2368    }
2369
2370    private <T> T doPUT(final String resource, final RecurlyObject payload, final Class<T> clazz) {
2371        return doPUT(resource, payload, clazz, new QueryParams());
2372    }
2373
2374    private <T> T doPUT(final String resource, final RecurlyObject payload, final Class<T> clazz, final QueryParams params) {
2375        final String xmlPayload;
2376        try {
2377            if (payload != null) {
2378                xmlPayload = xmlMapper.writeValueAsString(payload);
2379            } else {
2380                xmlPayload = null;
2381            }
2382
2383            if (debug()) {
2384                log.info("Msg to Recurly API [PUT]:: URL : {}", baseUrl + resource);
2385                log.info("Payload for [PUT]:: {}", xmlPayload);
2386            }
2387        } catch (IOException e) {
2388            log.warn("Unable to serialize {} object as XML: {}", clazz.getName(), payload.toString());
2389            return null;
2390        }
2391
2392        final String url = baseUrl + resource;
2393        validateHost(url);
2394
2395        return callRecurlySafeXmlContent(client.preparePut(url).setBody(xmlPayload), clazz);
2396    }
2397
2398    private FluentCaseInsensitiveStringsMap doHEAD(final String resource, QueryParams params) {
2399        if (params == null) {
2400            params = new QueryParams();
2401        }
2402
2403        final String url = constructUrl(resource, params);
2404        if (debug()) {
2405            log.info("Msg to Recurly API [HEAD]:: URL : {}", url);
2406        }
2407
2408        validateHost(url);
2409
2410        return callRecurlyNoContent(client.prepareHead(url));
2411    }
2412
2413    private void doDELETE(final String resource) {
2414        validateHost(baseUrl + resource);
2415
2416        callRecurlySafeXmlContent(client.prepareDelete(baseUrl + resource), null);
2417    }
2418
2419    private FluentCaseInsensitiveStringsMap callRecurlyNoContent(final AsyncHttpClient.BoundRequestBuilder builder) {
2420        try {
2421            final Response response = clientRequestBuilderCommon(builder)
2422                    .addHeader("Accept", "application/xml")
2423                    .addHeader("Content-Type", "application/xml; charset=utf-8")
2424                    .execute()
2425                    .get();
2426
2427            return response.getHeaders();
2428        } catch (ExecutionException e) {
2429            log.error("Execution error", e);
2430            return null;
2431        }
2432        catch (InterruptedException e) {
2433            log.error("Interrupted while calling Recurly", e);
2434            return null;
2435        }
2436    }
2437
2438    private <T> T callRecurlySafeXmlContent(final AsyncHttpClient.BoundRequestBuilder builder, @Nullable final Class<T> clazz) {
2439        try {
2440            return callRecurlyXmlContent(builder, clazz);
2441        } catch (IOException e) {
2442            log.warn("Error while calling Recurly", e);
2443            return null;
2444        } catch (ExecutionException e) {
2445            // Extract the errors exception, if any
2446            if (e.getCause() instanceof ConnectException) {
2447                // See https://github.com/killbilling/recurly-java-library/issues/185
2448                throw new ConnectionErrorException(e.getCause());
2449            } else if (e.getCause() != null &&
2450                e.getCause().getCause() != null &&
2451                e.getCause().getCause() instanceof TransactionErrorException) {
2452                throw (TransactionErrorException) e.getCause().getCause();
2453            } else if (e.getCause() != null &&
2454                       e.getCause() instanceof TransactionErrorException) {
2455                // See https://github.com/killbilling/recurly-java-library/issues/16
2456                throw (TransactionErrorException) e.getCause();
2457            }
2458            log.error("Execution error", e);
2459            return null;
2460        } catch (InterruptedException e) {
2461            log.error("Interrupted while calling Recurly", e);
2462            return null;
2463        }
2464    }
2465
2466    private <T> T callRecurlyXmlContent(final AsyncHttpClient.BoundRequestBuilder builder, @Nullable final Class<T> clazz)
2467            throws IOException, ExecutionException, InterruptedException {
2468        final Response response = clientRequestBuilderCommon(builder)
2469                .addHeader("Accept", "application/xml")
2470                .addHeader("Content-Type", "application/xml; charset=utf-8")
2471                .execute()
2472                .get();
2473
2474        final InputStream in = response.getResponseBodyAsStream();
2475        try {
2476            final String payload = convertStreamToString(in);
2477            if (debug()) {
2478                log.info("Msg from Recurly API :: {}", payload);
2479            }
2480
2481            // Handle errors payload
2482            if (response.getStatusCode() >= 300) {
2483                log.warn("Recurly error whilst calling: {}\n{}", response.getUri(), payload);
2484                log.warn("Error status code: {}\n", response.getStatusCode());
2485                RecurlyAPIError recurlyError = RecurlyAPIError.buildFromResponse(response);
2486
2487                if (response.getStatusCode() == 422) {
2488                    // 422 is returned for transaction errors (see https://dev.recurly.com/page/transaction-errors)
2489                    // as well as bad input payloads
2490                    final Errors errors;
2491                    try {
2492                        errors = xmlMapper.readValue(payload, Errors.class);
2493                    } catch (Exception e) {
2494                        log.warn("Unable to extract error", e);
2495                        return null;
2496                    }
2497
2498                    // Sometimes a single `Error` response is returned rather than `Errors`.
2499                    // In this case, all fields will be null.
2500                    if (errors == null || (
2501                        errors.getRecurlyErrors() == null &&
2502                        errors.getTransaction() == null &&
2503                        errors.getTransactionError() == null
2504                    )) {
2505                        recurlyError = RecurlyAPIError.buildFromXml(xmlMapper, payload, response);
2506                        throw new RecurlyAPIException(recurlyError);
2507                    }
2508                    throw new TransactionErrorException(errors);
2509                } else if (response.getStatusCode() == 401) {
2510                    recurlyError.setSymbol("unauthorized");
2511                    recurlyError.setDescription("We could not authenticate your request. Either your subdomain and private key are not set or incorrect");
2512
2513                    throw new RecurlyAPIException(recurlyError);
2514                } else {
2515                    try {
2516                        recurlyError = RecurlyAPIError.buildFromXml(xmlMapper, payload, response);
2517                    } catch (Exception e) {
2518                        log.debug("Unable to extract error", e);
2519                    }
2520
2521                    throw new RecurlyAPIException(recurlyError);
2522                }
2523            }
2524
2525            if (clazz == null) {
2526                return null;
2527            }
2528
2529            String location = response.getHeader("Location");
2530            if (clazz == Coupons.class && location != null && !location.isEmpty()) {
2531                final RecurlyObjects recurlyObjects = new Coupons();
2532                recurlyObjects.setRecurlyClient(this);
2533                recurlyObjects.setStartUrl(location);
2534                return (T) recurlyObjects;
2535            }
2536
2537            final T obj = xmlMapper.readValue(payload, clazz);
2538            final ResponseMetadata meta = new ResponseMetadata(response);
2539            if (obj instanceof RecurlyObject) {
2540                ((RecurlyObject) obj).setRecurlyClient(this);
2541            } else if (obj instanceof RecurlyObjects) {
2542                final RecurlyObjects recurlyObjects = (RecurlyObjects) obj;
2543                recurlyObjects.setRecurlyClient(this);
2544
2545                // Set the RecurlyClient on all objects for later use
2546                for (final Object object : recurlyObjects) {
2547                    ((RecurlyObject) object).setRecurlyClient(this);
2548                }
2549
2550                // Set links for pagination
2551                final String linkHeader = response.getHeader(LINK_HEADER_NAME);
2552                if (linkHeader != null) {
2553                    final String[] links = PaginationUtils.getLinks(linkHeader);
2554                    recurlyObjects.setStartUrl(links[0]);
2555                    recurlyObjects.setNextUrl(links[1]);
2556                }
2557            }
2558
2559            // Save value of rate limit remaining header
2560            String rateLimitRemainingString = response.getHeader(X_RATELIMIT_REMAINING_HEADER_NAME);
2561            if (rateLimitRemainingString != null)
2562                rateLimitRemaining = Integer.parseInt(rateLimitRemainingString);
2563
2564            return obj;
2565        } finally {
2566            closeStream(in);
2567        }
2568    }
2569
2570    private AsyncHttpClient.BoundRequestBuilder clientRequestBuilderCommon(AsyncHttpClient.BoundRequestBuilder requestBuilder) {
2571        return requestBuilder.addHeader("Authorization", "Basic " + key)
2572                .addHeader("X-Api-Version", RECURLY_API_VERSION)
2573                .addHeader(HttpHeaders.USER_AGENT, userAgent)
2574                .addHeader("Accept-Language", acceptLanguage)
2575                .setBodyEncoding("UTF-8");
2576    }
2577
2578    private String convertStreamToString(final java.io.InputStream is) {
2579        try {
2580            return new Scanner(is).useDelimiter("\\A").next();
2581        } catch (final NoSuchElementException e) {
2582            return "";
2583        }
2584    }
2585
2586    private void closeStream(final InputStream in) {
2587        if (in != null) {
2588            try {
2589                in.close();
2590            } catch (IOException e) {
2591                log.warn("Failed to close http-client - provided InputStream: {}", e.getLocalizedMessage());
2592            }
2593        }
2594    }
2595
2596    protected AsyncHttpClient createHttpClient() throws KeyManagementException, NoSuchAlgorithmException {
2597        final AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder();
2598
2599        // Don't limit the number of connections per host
2600        // See https://github.com/ning/async-http-client/issues/issue/28
2601        builder.setMaxConnectionsPerHost(-1);
2602        builder.setSSLContext(SslUtils.getInstance().getSSLContext());
2603
2604        return new AsyncHttpClient(builder.build());
2605    }
2606
2607    private void validateHost(String url) {
2608        String host = URI.create(url).getHost();
2609
2610        // Remove the subdomain from the host
2611        host = host.substring(host.indexOf(".")+1);
2612
2613        if (!validHosts.contains(host)) {
2614            String exc = String.format("Attempted to make call to %s instead of Recurly", host);
2615            throw new RuntimeException(exc);
2616        }
2617    }
2618
2619    @VisibleForTesting
2620    String getUserAgent() {
2621        return userAgent;
2622    }
2623
2624    private String buildUserAgent() {
2625        final String defaultVersion = "0.0.0";
2626        final String defaultJavaVersion = "0.0.0";
2627
2628        try {
2629            final Properties gitRepositoryState = new Properties();
2630            final URL resourceURL = Resources.getResource(GIT_PROPERTIES_FILE);
2631            final CharSource charSource = Resources.asCharSource(resourceURL, Charset.forName("UTF-8"));
2632
2633            Reader reader = null;
2634            try {
2635                reader = charSource.openStream();
2636                gitRepositoryState.load(reader);
2637            } finally {
2638                if (reader != null) {
2639                    reader.close();
2640                }
2641            }
2642
2643            final String version = MoreObjects.firstNonNull(getVersionFromGitRepositoryState(gitRepositoryState), defaultVersion);
2644            final String javaVersion = MoreObjects.firstNonNull(StandardSystemProperty.JAVA_VERSION.value(), defaultJavaVersion);
2645            return String.format("KillBill/%s; %s", version, javaVersion);
2646        } catch (final Exception e) {
2647            return String.format("KillBill/%s; %s", defaultVersion, defaultJavaVersion);
2648        }
2649    }
2650
2651    @VisibleForTesting
2652    String getVersionFromGitRepositoryState(final Properties gitRepositoryState) {
2653        final String gitDescribe = gitRepositoryState.getProperty(GIT_COMMIT_ID_DESCRIBE_SHORT);
2654        if (gitDescribe == null) {
2655            return null;
2656        }
2657        final Matcher matcher = TAG_FROM_GIT_DESCRIBE_PATTERN.matcher(gitDescribe);
2658        return matcher.find() ? matcher.group(1) : null;
2659    }
2660
2661}