killbill-memoizeit
Details
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
index f7046e9..0234816 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxrsResource.java
@@ -80,6 +80,7 @@ public interface JaxrsResource {
public static final String QUERY_PAYMENT_EXTERNAL = "externalPayment";
public static final String QUERY_PAYMENT_WITH_REFUNDS_AND_CHARGEBACKS = "withRefundsAndChargebacks";
+ public static final String QUERY_PAYMENT_PLUGIN_NAME = "pluginName";
public static final String QUERY_TAGS = "tagList";
public static final String QUERY_TAGS_INCLUDED_DELETED = "includedDeleted";
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
index 8f99a77..2856f16 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentResource.java
@@ -16,7 +16,10 @@
package com.ning.billing.jaxrs.resources;
+import java.io.IOException;
+import java.io.OutputStream;
import java.math.BigDecimal;
+import java.net.URI;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@@ -35,8 +38,10 @@ import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
+import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.StreamingOutput;
import javax.ws.rs.core.UriInfo;
import com.ning.billing.ObjectType;
@@ -66,9 +71,13 @@ import com.ning.billing.util.api.TagDefinitionApiException;
import com.ning.billing.util.api.TagUserApi;
import com.ning.billing.util.callcontext.CallContext;
import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
+import com.fasterxml.jackson.core.JsonGenerator;
import com.google.common.base.Function;
+import com.google.common.base.Strings;
import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
import com.google.inject.Inject;
import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@@ -130,6 +139,76 @@ public class PaymentResource extends JaxRsResourceBase {
return Response.status(Status.OK).entity(paymentJson).build();
}
+ @GET
+ @Path("/" + PAGINATION)
+ @Produces(APPLICATION_JSON)
+ public Response getPayments(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ final Pagination<Payment> payments;
+ final Map<String, String> nextUriParams = new HashMap<String, String>();
+ if (Strings.isNullOrEmpty(pluginName)) {
+ payments = paymentApi.getPayments(offset, limit, tenantContext);
+ } else {
+ payments = paymentApi.getPayments(offset, limit, pluginName, tenantContext);
+ nextUriParams.put(QUERY_PAYMENT_PLUGIN_NAME, pluginName);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "getPayments", payments.getNextOffset(), limit, nextUriParams);
+ return buildStreamingPaymentsResponse(payments, nextPageUri);
+ }
+
+ @GET
+ @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+ @Produces(APPLICATION_JSON)
+ public Response searchPayments(@PathParam("searchKey") final String searchKey,
+ @QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+ @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+ @QueryParam(QUERY_PAYMENT_PLUGIN_NAME) final String pluginName,
+ @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
+ final TenantContext tenantContext = context.createContext(request);
+
+ // Search the plugin(s)
+ final Pagination<Payment> payments;
+ if (Strings.isNullOrEmpty(pluginName)) {
+ payments = paymentApi.searchPayments(searchKey, offset, limit, tenantContext);
+ } else {
+ payments = paymentApi.searchPayments(searchKey, offset, limit, pluginName, tenantContext);
+ }
+
+ final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "searchPayments", payments.getNextOffset(), limit, ImmutableMap.<String, String>of());
+ return buildStreamingPaymentsResponse(payments, nextPageUri);
+ }
+
+ private Response buildStreamingPaymentsResponse(final Pagination<Payment> payments, final URI nextPageUri) {
+ final StreamingOutput json = new StreamingOutput() {
+ @Override
+ public void write(final OutputStream output) throws IOException, WebApplicationException {
+ final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
+ generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
+
+ generator.writeStartArray();
+ for (final Payment payment : payments) {
+ final PaymentJson asJson = new PaymentJson(payment, null);
+ generator.writeObject(asJson);
+ }
+ generator.writeEndArray();
+ generator.close();
+ }
+ };
+ return Response.status(Status.OK)
+ .entity(json)
+ .header(HDR_PAGINATION_CURRENT_OFFSET, payments.getCurrentOffset())
+ .header(HDR_PAGINATION_NEXT_OFFSET, payments.getNextOffset())
+ .header(HDR_PAGINATION_TOTAL_NB_RECORDS, payments.getTotalNbRecords())
+ .header(HDR_PAGINATION_MAX_NB_RECORDS, payments.getMaxNbRecords())
+ .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+ .build();
+ }
+
@PUT
@Path("/{paymentId:" + UUID_PATTERN + "}")
@Consumes(APPLICATION_JSON)
NEWS 3(+2 -1)
diff --git a/NEWS b/NEWS
index 2038119..8922870 100644
--- a/NEWS
+++ b/NEWS
@@ -1,5 +1,6 @@
0.8.8
- Update killbill-oss-parent to 0.5.14
+ Add APIs to retrieve all payments for a given tenant
+ Update killbill-oss-parent to 0.5.15
0.8.7
DDL: remove unused paid_through_date column
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
index 0c0173b..e1777d2 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultPaymentApi.java
@@ -87,6 +87,16 @@ public class DefaultPaymentApi implements PaymentApi {
}
@Override
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final TenantContext context) {
+ return paymentProcessor.getPayments(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+ }
+
+ @Override
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext) throws PaymentApiException {
+ return paymentProcessor.getPayments(offset, limit, pluginName, tenantContext, internalCallContextFactory.createInternalTenantContext(tenantContext));
+ }
+
+ @Override
public Payment getPayment(final UUID paymentId, final boolean withPluginInfo, final TenantContext context) throws PaymentApiException {
final Payment payment = paymentProcessor.getPayment(paymentId, withPluginInfo, internalCallContextFactory.createInternalTenantContext(context));
if (payment == null) {
@@ -133,7 +143,7 @@ public class DefaultPaymentApi implements PaymentApi {
@Override
public void notifyPendingRefundOfStateChanged(final Account account, final UUID refundId, final boolean isSuccess, final CallContext context) throws PaymentApiException {
refundProcessor.notifyPendingRefundOfStateChanged(account, refundId, isSuccess,
- internalCallContextFactory.createInternalCallContext(account.getId(), context));
+ internalCallContextFactory.createInternalCallContext(account.getId(), context));
}
@Override
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
index db6fcad..3f36672 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentProcessor.java
@@ -69,6 +69,7 @@ import com.ning.billing.payment.retry.AutoPayRetryService.AutoPayRetryServiceSch
import com.ning.billing.payment.retry.FailedPaymentRetryService.FailedPaymentRetryServiceScheduler;
import com.ning.billing.payment.retry.PluginFailureRetryService.PluginFailureRetryServiceScheduler;
import com.ning.billing.tag.TagInternalApi;
+import com.ning.billing.util.callcontext.TenantContext;
import com.ning.billing.util.config.PaymentConfig;
import com.ning.billing.util.dao.NonEntityDao;
import com.ning.billing.util.entity.DefaultPagination;
@@ -147,6 +148,72 @@ public class PaymentProcessor extends ProcessorBase {
return fromPaymentModelDao(model, pluginInfo, context);
}
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
+ // Note that we cannot easily do streaming here, since we would have to rely on the statistics
+ // returned by the Pagination objects from the plugins and we probably don't want to do that (if
+ // one plugin gets it wrong, it may starve the others).
+ final List<Payment> allResults = new LinkedList<Payment>();
+ Long totalNbRecords = 0L;
+ Long maxNbRecords = 0L;
+
+ // Search in all plugins (we treat the full set of results as a union with respect to offset/limit)
+ boolean firstSearch = true;
+ for (final String pluginName : getAvailablePlugins()) {
+ try {
+ final Pagination<Payment> payments;
+ if (allResults.size() >= limit) {
+ // We have enough results, we just keep going (limit 1) to get the stats
+ payments = getPayments(firstSearch ? offset : 0L, 1L, pluginName, tenantContext, internalTenantContext);
+ // Required to close database connections
+ ImmutableList.<Payment>copyOf(payments);
+ } else {
+ payments = getPayments(firstSearch ? offset : 0L, limit - allResults.size(), pluginName, tenantContext, internalTenantContext);
+ allResults.addAll(ImmutableList.<Payment>copyOf(payments));
+ }
+ firstSearch = false;
+ totalNbRecords += payments.getTotalNbRecords();
+ maxNbRecords += payments.getMaxNbRecords();
+ } catch (final PaymentApiException e) {
+ log.warn("Error while searching plugin " + pluginName, e);
+ // Non-fatal, continue to search other plugins
+ }
+ }
+
+ return new DefaultPagination<Payment>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+ }
+
+ public Pagination<Payment> getPayments(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+ final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+ // Find all payments for all accounts
+ final Pagination<PaymentModelDao> paymentModelDaos = paymentDao.getPayments(pluginName, offset, limit, internalTenantContext);
+
+ return new DefaultPagination<Payment>(paymentModelDaos,
+ limit,
+ Iterators.<Payment>filter(Iterators.<PaymentModelDao, Payment>transform(paymentModelDaos.iterator(),
+ new Function<PaymentModelDao, Payment>() {
+ @Override
+ public Payment apply(final PaymentModelDao paymentModelDao) {
+ final PaymentInfoPlugin pluginInfo;
+ try {
+ pluginInfo = pluginApi.getPaymentInfo(paymentModelDao.getAccountId(), paymentModelDao.getId(), tenantContext);
+ } catch (final PaymentPluginApiException e) {
+ log.warn("Unable to find payment id " + paymentModelDao.getId() + " in plugin " + pluginName);
+ return null;
+ }
+
+ if (pluginInfo.getKbPaymentId() == null) {
+ // Garbage from the plugin?
+ log.debug("Plugin {} returned a payment without a kbPaymentId", pluginName);
+ return null;
+ }
+
+ return fromPaymentModelDao(paymentModelDao, pluginInfo, internalTenantContext);
+ }
+ }),
+ Predicates.<Payment>notNull()));
+ }
+
public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
// Note that we cannot easily do streaming here, since we would have to rely on the statistics
// returned by the Pagination objects from the plugins and we probably don't want to do that (if
@@ -403,7 +470,7 @@ public class PaymentProcessor extends ProcessorBase {
final List<PaymentAttemptModelDao> attempts = paymentDao.getAttemptsForPayment(paymentId, context);
final PaymentAttemptModelDao lastAttempt = attempts.get(attempts.size() - 1);
final PaymentStatus newPaymentStatus = isSuccess ? PaymentStatus.SUCCESS : PaymentStatus.PAYMENT_FAILURE_ABORTED;
- paymentDao.updatePaymentAndAttemptOnCompletion(paymentId, newPaymentStatus, payment.getProcessedAmount(), payment.getProcessedCurrency(), lastAttempt.getId(),null, null, context);
+ paymentDao.updatePaymentAndAttemptOnCompletion(paymentId, newPaymentStatus, payment.getProcessedAmount(), payment.getProcessedCurrency(), lastAttempt.getId(), null, null, context);
return null;
}
});
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
index ccc0e17..8d3cf23 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/DefaultPaymentDao.java
@@ -54,7 +54,6 @@ public class DefaultPaymentDao implements PaymentDao {
this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
}
-
@Override
public PaymentAttemptModelDao getPaymentAttempt(final UUID attemptId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentAttemptModelDao>() {
@@ -74,7 +73,6 @@ public class DefaultPaymentDao implements PaymentDao {
final PaymentSqlDao transactional = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
transactional.create(payment, context);
-
entitySqlDaoWrapperFactory.become(PaymentAttemptSqlDao.class).create(attempt, context);
return transactional.getById(payment.getId().toString(), context);
@@ -301,6 +299,34 @@ public class DefaultPaymentDao implements PaymentDao {
}
@Override
+ public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ // Note: the connection will be busy as we stream the results out: hence we cannot use
+ // SQL_CALC_FOUND_ROWS / FOUND_ROWS on the actual query.
+ // We still need to know the actual number of results, mainly for the UI so that it knows if it needs to fetch
+ // more pages. To do that, we perform a dummy search query with SQL_CALC_FOUND_ROWS (but limit 1).
+ final Long count = transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Long>() {
+ @Override
+ public Long inTransaction(final EntitySqlDaoWrapperFactory<EntitySqlDao> entitySqlDaoWrapperFactory) throws Exception {
+ final PaymentSqlDao sqlDao = entitySqlDaoWrapperFactory.become(PaymentSqlDao.class);
+ final Iterator<PaymentModelDao> dumbIterator = sqlDao.getByPluginName(pluginName, offset, 1L, context);
+ // Make sure to go through the results to close the connection
+ while (dumbIterator.hasNext()) {
+ dumbIterator.next();
+ }
+ return sqlDao.getFoundRows(context);
+ }
+ });
+
+ // We usually always want to wrap our queries in an EntitySqlDaoTransactionWrapper... except here.
+ // Since we want to stream the results out, we don't want to auto-commit when this method returns.
+ final PaymentSqlDao paymentSqlDao = transactionalSqlDao.onDemand(PaymentSqlDao.class);
+ final Long totalCount = paymentSqlDao.getCount(context);
+ final Iterator<PaymentModelDao> results = paymentSqlDao.getByPluginName(pluginName, offset, limit, context);
+
+ return new DefaultPagination<PaymentModelDao>(offset, limit, count, totalCount, results);
+ }
+
+ @Override
public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<PaymentModelDao>() {
@Override
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
index 2c2e76e..613a8d0 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentDao.java
@@ -45,6 +45,8 @@ public interface PaymentDao {
public PaymentModelDao getLastPaymentForPaymentMethod(UUID accountId, UUID paymentMethodId, InternalTenantContext context);
+ public Pagination<PaymentModelDao> getPayments(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
public PaymentModelDao getPayment(UUID paymentId, InternalTenantContext context);
public List<PaymentAttemptModelDao> getAttemptsForPayment(UUID paymentId, InternalTenantContext context);
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
index 8a1749f..e234194 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentSqlDao.java
@@ -18,18 +18,20 @@ package com.ning.billing.payment.dao;
import java.math.BigDecimal;
import java.util.Date;
+import java.util.Iterator;
import java.util.List;
import org.skife.jdbi.v2.sqlobject.Bind;
import org.skife.jdbi.v2.sqlobject.BindBean;
import org.skife.jdbi.v2.sqlobject.SqlQuery;
import org.skife.jdbi.v2.sqlobject.SqlUpdate;
+import org.skife.jdbi.v2.sqlobject.customizers.FetchSize;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
import com.ning.billing.catalog.api.Currency;
import com.ning.billing.payment.api.Payment;
import com.ning.billing.util.audit.ChangeType;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
import com.ning.billing.util.entity.dao.Audited;
import com.ning.billing.util.entity.dao.EntitySqlDao;
import com.ning.billing.util.entity.dao.EntitySqlDaoStringTemplate;
@@ -65,5 +67,14 @@ public interface PaymentSqlDao extends EntitySqlDao<PaymentModelDao, Payment> {
@SqlQuery
List<PaymentModelDao> getPaymentsForAccount(@Bind("accountId") final String accountId,
@BindBean final InternalTenantContext context);
+
+ @SqlQuery
+ // Magic value to force MySQL to stream from the database
+ // See http://dev.mysql.com/doc/refman/5.0/en/connector-j-reference-implementation-notes.html (ResultSet)
+ @FetchSize(Integer.MIN_VALUE)
+ public Iterator<PaymentModelDao> getByPluginName(@Bind("pluginName") final String pluginName,
+ @Bind("offset") final Long offset,
+ @Bind("rowCount") final Long rowCount,
+ @BindBean final InternalTenantContext context);
}
diff --git a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
index 3f9624c..cc119e5 100644
--- a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
+++ b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentSqlDao.sql.stg
@@ -104,4 +104,14 @@ where id = :id
;
>>
-
+getByPluginName(pluginName, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields("t.")>
+from <tableName()> t
+join payment_methods pm on pm.id = t.payment_method_id
+where pm.plugin_name = :pluginName
+and pm.is_active = 1
+order by record_id
+limit :offset, :rowCount
+;
+>>
diff --git a/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java b/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
index 2a245c6..3e14944 100644
--- a/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
+++ b/payment/src/test/java/com/ning/billing/payment/dao/MockPaymentDao.java
@@ -108,6 +108,11 @@ public class MockPaymentDao implements PaymentDao {
}
@Override
+ public Pagination<PaymentModelDao> getPayments(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
public PaymentModelDao getPayment(final UUID paymentId, final InternalTenantContext context) {
return payments.get(paymentId);
}
@@ -186,13 +191,11 @@ public class MockPaymentDao implements PaymentDao {
return null;
}
-
@Override
public void updateRefundStatus(final UUID refundId, final RefundStatus status, final BigDecimal processedAmount, final Currency processedCurrency, final InternalCallContext context) {
return;
}
-
@Override
public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
return null;
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 1b696e9..78df4c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -19,7 +19,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>com.ning.billing</groupId>
- <version>0.5.14</version>
+ <version>0.5.15</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.8.8-SNAPSHOT</version>