killbill-memoizeit

Changes

.idea/libraries/Maven__com_ning_billing_killbill_api_0_6_7.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_billing_plugin_killbill_plugin_api_notification_0_4_0.xml 13(+0 -13)

.idea/libraries/Maven__com_ning_billing_plugin_killbill_plugin_api_payment_0_4_0.xml 13(+0 -13)

.travis.yml 2(+1 -1)

pom.xml 2(+1 -1)

Details

diff --git a/.idea/dictionaries/pierre.xml b/.idea/dictionaries/pierre.xml
index b1bb5c9..044303d 100644
--- a/.idea/dictionaries/pierre.xml
+++ b/.idea/dictionaries/pierre.xml
@@ -2,7 +2,9 @@
   <dictionary name="pierre">
     <words>
       <w>aoped</w>
+      <w>daos</w>
       <w>guice</w>
+      <w>infos</w>
       <w>jdbc</w>
       <w>killbill</w>
       <w>shiro</w>

.travis.yml 2(+1 -1)

diff --git a/.travis.yml b/.travis.yml
index add54e4..6283386 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -5,7 +5,7 @@ install: mvn install -DskipTests=true
 
 notifications:
   email:
-    - killbilling-dev@googlegroups.com
+    - kill-bill-commits@googlegroups.com
 
 jdk:
   - openjdk6
diff --git a/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java b/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
index 5f434c7..27d1601 100644
--- a/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
+++ b/account/src/main/java/com/ning/billing/account/api/svcs/DefaultAccountInternalApi.java
@@ -26,6 +26,7 @@ import com.ning.billing.account.api.Account;
 import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountData;
 import com.ning.billing.account.api.AccountEmail;
+import com.ning.billing.account.api.AccountInternalApi;
 import com.ning.billing.account.api.DefaultAccount;
 import com.ning.billing.account.api.DefaultAccountEmail;
 import com.ning.billing.account.dao.AccountDao;
@@ -33,11 +34,13 @@ import com.ning.billing.account.dao.AccountEmailModelDao;
 import com.ning.billing.account.dao.AccountModelDao;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
-import com.ning.billing.account.api.AccountInternalApi;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 
 public class DefaultAccountInternalApi implements AccountInternalApi {
 
diff --git a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
index cd2c53e..d1ffe58 100644
--- a/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
+++ b/account/src/main/java/com/ning/billing/account/api/user/DefaultAccountUserApi.java
@@ -94,6 +94,7 @@ public class DefaultAccountUserApi implements AccountUserApi {
     public Pagination<Account> searchAccounts(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
         final Pagination<AccountModelDao> accountModelDaos = accountDao.searchAccounts(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
         return new DefaultPagination<Account>(accountModelDaos,
+                                              limit,
                                               Iterators.<AccountModelDao, Account>transform(accountModelDaos.iterator(),
                                                                                             new Function<AccountModelDao, Account>() {
                                                                                                 @Override
@@ -107,6 +108,7 @@ public class DefaultAccountUserApi implements AccountUserApi {
     public Pagination<Account> getAccounts(final Long offset, final Long limit, final TenantContext context) {
         final Pagination<AccountModelDao> accountModelDaos = accountDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
         return new DefaultPagination<Account>(accountModelDaos,
+                                              limit,
                                               Iterators.<AccountModelDao, Account>transform(accountModelDaos.iterator(),
                                                                                             new Function<AccountModelDao, Account>() {
                                                                                                 @Override
diff --git a/account/src/main/java/com/ning/billing/account/dao/AccountDao.java b/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
index b6ed783..fbe6bfc 100644
--- a/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/AccountDao.java
@@ -30,7 +30,7 @@ public interface AccountDao extends EntityDao<AccountModelDao, Account, AccountA
 
     public AccountModelDao getAccountByKey(String key, InternalTenantContext context);
 
-    public Pagination<AccountModelDao> searchAccounts(String searchKey, Long offset, Long rowCount, InternalTenantContext context);
+    public Pagination<AccountModelDao> searchAccounts(String searchKey, Long offset, Long limit, InternalTenantContext context);
 
     /**
      * @throws AccountApiException when externalKey is null
diff --git a/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java b/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
index 7c71ea2..fca3684 100644
--- a/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
+++ b/account/src/main/java/com/ning/billing/account/dao/DefaultAccountDao.java
@@ -107,18 +107,31 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
     }
 
     @Override
-    public Pagination<AccountModelDao> searchAccounts(final String searchKey, final Long offset, final Long rowCount, final InternalTenantContext context) {
+    public Pagination<AccountModelDao> searchAccounts(final String searchKey, 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 AccountSqlDao accountSqlDao = entitySqlDaoWrapperFactory.become(AccountSqlDao.class);
+                final Iterator<AccountModelDao> dumbIterator = accountSqlDao.searchAccounts(searchKey, offset, 1L, context);
+                // Make sure to go through the results to close the connection
+                while (dumbIterator.hasNext()) {
+                    dumbIterator.next();
+                }
+                return accountSqlDao.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 AccountSqlDao accountSqlDao = transactionalSqlDao.onDemand(AccountSqlDao.class);
+        final Long totalCount = accountSqlDao.getCount(context);
+        final Iterator<AccountModelDao> results = accountSqlDao.searchAccounts(searchKey, offset, limit, context);
 
-        // Note: the connection will be busy as we stream the results out: hence we cannot use
-        // SQL_CALC_FOUND_ROWS / FOUND_ROWS and we don't know the total number of results (in this case,
-        // performing a second time the search just to get the count is too expensive to be worth it).
-        final Long count = null;
-
-        final Iterator<AccountModelDao> results = accountSqlDao.searchAccounts(searchKey, offset, rowCount, context);
-        return new DefaultPagination<AccountModelDao>(offset, rowCount, count, results);
+        return new DefaultPagination<AccountModelDao>(offset, limit, count, totalCount, results);
     }
 
     @Override
diff --git a/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg b/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
index 561566e..328f846 100644
--- a/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
+++ b/account/src/main/resources/com/ning/billing/account/dao/AccountSqlDao.sql.stg
@@ -87,7 +87,7 @@ getAccountByKey() ::= <<
 >>
 
 searchAccounts(searchKey, offset, rowCount) ::= <<
-select <allTableFields()>
+select SQL_CALC_FOUND_ROWS <allTableFields()>
 from (
   select <allTableFields()>
   from accounts
diff --git a/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java b/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
index 02ee51e..19f818f 100644
--- a/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
+++ b/api/src/main/java/com/ning/billing/account/api/AccountInternalApi.java
@@ -21,6 +21,7 @@ import java.util.UUID;
 
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public interface AccountInternalApi {
 
diff --git a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
index 0a53eb3..6d0feb4 100644
--- a/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/com/ning/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -118,6 +118,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
         // Invoices will be shallow, i.e. won't contain items nor payments
         final Pagination<InvoiceModelDao> invoiceModelDaos = dao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
         return new DefaultPagination<Invoice>(invoiceModelDaos,
+                                              limit,
                                               Iterators.<InvoiceModelDao, Invoice>transform(invoiceModelDaos.iterator(),
                                                                                             new Function<InvoiceModelDao, Invoice>() {
                                                                                                 @Override
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
index 4576e33..a210318 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/AccountResource.java
@@ -19,6 +19,7 @@ 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.Collection;
 import java.util.Collections;
@@ -103,6 +104,7 @@ import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Function;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Multimap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -156,6 +158,20 @@ public class AccountResource extends JaxRsResourceBase {
     }
 
     @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getAccounts(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                @QueryParam(QUERY_ACCOUNT_WITH_BALANCE) @DefaultValue("false") final Boolean accountWithBalance,
+                                @QueryParam(QUERY_ACCOUNT_WITH_BALANCE_AND_CBA) @DefaultValue("false") final Boolean accountWithBalanceAndCBA,
+                                @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<Account> accounts = accountUserApi.getAccounts(offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "getAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of());
+        return buildStreamingAccountsResponse(accounts, accountWithBalance, accountWithBalanceAndCBA, nextPageUri, tenantContext);
+    }
+
+    @GET
     @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
     @Produces(APPLICATION_JSON)
     public Response searchAccounts(@PathParam("searchKey") final String searchKey,
@@ -166,7 +182,12 @@ public class AccountResource extends JaxRsResourceBase {
                                    @javax.ws.rs.core.Context final HttpServletRequest request) throws AccountApiException {
         final TenantContext tenantContext = context.createContext(request);
         final Pagination<Account> accounts = accountUserApi.searchAccounts(searchKey, offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "searchAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey));
+        return buildStreamingAccountsResponse(accounts, accountWithBalance, accountWithBalanceAndCBA, nextPageUri, tenantContext);
+    }
 
+    private Response buildStreamingAccountsResponse(final Pagination<Account> accounts, final Boolean accountWithBalance,
+                                                    final Boolean accountWithBalanceAndCBA, final URI nextPageUri, final TenantContext tenantContext) {
         final StreamingOutput json = new StreamingOutput() {
             @Override
             public void write(final OutputStream output) throws IOException, WebApplicationException {
@@ -186,13 +207,12 @@ public class AccountResource extends JaxRsResourceBase {
                        .entity(json)
                        .header(HDR_PAGINATION_CURRENT_OFFSET, accounts.getCurrentOffset())
                        .header(HDR_PAGINATION_NEXT_OFFSET, accounts.getNextOffset())
-                       .header(HDR_PAGINATION_TOTAL_NB_RESULTS, accounts.getTotalNbResults())
-                       .header(HDR_PAGINATION_NB_RESULTS, accounts.getNbResults())
-                       .header(HDR_PAGINATION_NB_RESULTS_FROM_OFFSET, accounts.getNbResultsFromOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, accounts.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, accounts.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
                        .build();
     }
 
-
     @GET
     @Path("/{accountId:" + UUID_PATTERN + "}/" + BUNDLES)
     @Produces(APPLICATION_JSON)
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 07e7ac3..4d73b69 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
@@ -42,9 +42,9 @@ public interface JaxrsResource {
     public static String HDR_COMMENT = "X-Killbill-Comment";
     public static String HDR_PAGINATION_CURRENT_OFFSET = "X-Killbill-Pagination-CurrentOffset";
     public static String HDR_PAGINATION_NEXT_OFFSET = "X-Killbill-Pagination-NextOffset";
-    public static String HDR_PAGINATION_TOTAL_NB_RESULTS = "X-Killbill-Pagination-TotalNbResults";
-    public static String HDR_PAGINATION_NB_RESULTS = "X-Killbill-Pagination-NbResults";
-    public static String HDR_PAGINATION_NB_RESULTS_FROM_OFFSET = "X-Killbill-Pagination-NbResultsFromOffset";
+    public static String HDR_PAGINATION_TOTAL_NB_RECORDS = "X-Killbill-Pagination-TotalNbRecords";
+    public static String HDR_PAGINATION_MAX_NB_RECORDS = "X-Killbill-Pagination-MaxNbRecords";
+    public static String HDR_PAGINATION_NEXT_PAGE_URI = "X-Killbill-Pagination-NextPageUri";
 
     /*
      * Patterns
@@ -100,6 +100,8 @@ public interface JaxrsResource {
 
     public static final String QUERY_NOTIFICATION_CALLBACK = "cb";
 
+    public static final String PAGINATION = "pagination";
+
     public static final String ACCOUNTS = "accounts";
     public static final String ACCOUNTS_PATH = PREFIX + "/" + ACCOUNTS;
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
index c24c399..150b97e 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -63,7 +63,7 @@ import com.google.common.collect.ImmutableList;
 
 public abstract class JaxRsResourceBase implements JaxrsResource {
 
-    private static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
+    static final Logger log = LoggerFactory.getLogger(JaxRsResourceBase.class);
 
     protected static final ObjectMapper mapper = new ObjectMapper();
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
index 2af1d58..b84898c 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/PaymentMethodResource.java
@@ -18,6 +18,7 @@ package com.ning.billing.jaxrs.resources;
 
 import java.io.IOException;
 import java.io.OutputStream;
+import java.net.URI;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.UUID;
@@ -56,6 +57,7 @@ import com.ning.billing.util.entity.Pagination;
 
 import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -96,6 +98,28 @@ public class PaymentMethodResource extends JaxRsResourceBase {
     }
 
     @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getPaymentMethods(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                      @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                      @QueryParam(QUERY_PAYMENT_METHOD_PLUGIN_NAME) final String pluginName,
+                                      @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        final Pagination<PaymentMethod> paymentMethods;
+        final Map<String, String> nextUriParams = new HashMap<String, String>();
+        if (Strings.isNullOrEmpty(pluginName)) {
+            paymentMethods = paymentApi.getPaymentMethods(offset, limit, tenantContext);
+        } else {
+            paymentMethods = paymentApi.getPaymentMethods(offset, limit, pluginName, tenantContext);
+            nextUriParams.put(QUERY_PAYMENT_METHOD_PLUGIN_NAME, pluginName);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "getPaymentMethods", paymentMethods.getNextOffset(), limit, nextUriParams);
+        return buildStreamingPaymentMethodsResponse(paymentMethods, nextPageUri, tenantContext);
+    }
+
+    @GET
     @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
     @Produces(APPLICATION_JSON)
     public Response searchPaymentMethods(@PathParam("searchKey") final String searchKey,
@@ -113,6 +137,11 @@ public class PaymentMethodResource extends JaxRsResourceBase {
             paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, pluginName, tenantContext);
         }
 
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "searchPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of());
+        return buildStreamingPaymentMethodsResponse(paymentMethods, nextPageUri, tenantContext);
+    }
+
+    private Response buildStreamingPaymentMethodsResponse(final Pagination<PaymentMethod> paymentMethods, final URI nextPageUri, final TenantContext tenantContext) {
         final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
         final StreamingOutput json = new StreamingOutput() {
             @Override
@@ -129,7 +158,8 @@ public class PaymentMethodResource extends JaxRsResourceBase {
                             account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
                             accounts.put(paymentMethod.getAccountId(), account);
                         } catch (AccountApiException e) {
-                            throw new RuntimeException(e);
+                            log.warn("Unable to retrieve account", e);
+                            continue;
                         }
                     }
 
@@ -144,9 +174,9 @@ public class PaymentMethodResource extends JaxRsResourceBase {
                        .entity(json)
                        .header(HDR_PAGINATION_CURRENT_OFFSET, paymentMethods.getCurrentOffset())
                        .header(HDR_PAGINATION_NEXT_OFFSET, paymentMethods.getNextOffset())
-                       .header(HDR_PAGINATION_TOTAL_NB_RESULTS, paymentMethods.getTotalNbResults())
-                       .header(HDR_PAGINATION_NB_RESULTS, paymentMethods.getNbResults())
-                       .header(HDR_PAGINATION_NB_RESULTS_FROM_OFFSET, paymentMethods.getNbResultsFromOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, paymentMethods.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, paymentMethods.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
                        .build();
     }
 
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
index d7ca875..4a25ec0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/util/JaxrsUriBuilder.java
@@ -13,12 +13,16 @@
  * License for the specific language governing permissions and limitations
  * under the License.
  */
+
 package com.ning.billing.jaxrs.util;
 
+import java.net.URI;
+import java.util.Map;
+
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriBuilder;
-import java.net.URI;
 
+import com.ning.billing.jaxrs.resources.JaxRsResourceBase;
 import com.ning.billing.jaxrs.resources.JaxrsResource;
 
 public class JaxrsUriBuilder {
@@ -37,6 +41,21 @@ public class JaxrsUriBuilder {
         }).build();
     }
 
+    public URI nextPage(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Long nextOffset, final Long limit, final Map<String, String> params) {
+        if (nextOffset == null || limit == null) {
+            // End of pagination?
+            return null;
+        }
+
+        final UriBuilder uriBuilder = UriBuilder.fromResource(theClass)
+                                                .path(theClass, getMethodName)
+                                                .queryParam(JaxRsResourceBase.QUERY_SEARCH_OFFSET, nextOffset)
+                                                .queryParam(JaxRsResourceBase.QUERY_SEARCH_LIMIT, limit);
+        for (final String key : params.keySet()) {
+            uriBuilder.queryParam(key, params.get(key));
+        }
+        return uriBuilder.build();
+    }
 
     public Response buildResponse(final Class<? extends JaxrsResource> theClass, final String getMethodName, final Object objectId, final String baseUri) {
 
diff --git a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
index c318727..88066fa 100644
--- a/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
+++ b/osgi/src/main/java/com/ning/billing/osgi/FileInstall.java
@@ -187,6 +187,7 @@ public class FileInstall {
         if (new File(expectedPath).isFile()) {
             return expectedPath;
         } else {
+            logger.warn("Unable to find the JRuby bundle at {}, ruby plugins won't be started!", expectedPath);
             return null;
         }
     }
diff --git a/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
index dffd273..c4ef01b 100644
--- a/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/beatrix/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -144,18 +144,13 @@ public class TestPaymentPluginApi implements PaymentPluginApi {
             }
 
             @Override
-            public Long getTotalNbResults() {
+            public Long getMaxNbRecords() {
                 return 0L;
             }
 
             @Override
-            public Long getNbResults() {
-                return null;
-            }
-
-            @Override
-            public Long getNbResultsFromOffset() {
-                return null;
+            public Long getTotalNbRecords() {
+                return 0L;
             }
 
             @Override
diff --git a/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java b/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
index 0d4f742..c9059b0 100644
--- a/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
+++ b/osgi-bundles/tests/payment/src/test/java/com/ning/billing/osgi/bundles/test/TestPaymentPluginApi.java
@@ -224,18 +224,13 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
             }
 
             @Override
-            public Long getTotalNbResults() {
+            public Long getMaxNbRecords() {
                 return 0L;
             }
 
             @Override
-            public Long getNbResults() {
-                return null;
-            }
-
-            @Override
-            public Long getNbResultsFromOffset() {
-                return null;
+            public Long getTotalNbRecords() {
+                return 0L;
             }
 
             @Override
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 aeb556b..8352981 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
@@ -27,6 +27,7 @@ import java.util.UUID;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.account.api.Account;
 import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.clock.Clock;
 import com.ning.billing.payment.core.PaymentMethodProcessor;
 import com.ning.billing.payment.core.PaymentProcessor;
 import com.ning.billing.payment.core.RefundProcessor;
@@ -44,15 +45,18 @@ public class DefaultPaymentApi implements PaymentApi {
     private final PaymentProcessor paymentProcessor;
     private final RefundProcessor refundProcessor;
     private final InternalCallContextFactory internalCallContextFactory;
+    private final Clock clock;
 
     @Inject
     public DefaultPaymentApi(final PaymentMethodProcessor methodProcessor,
                              final PaymentProcessor paymentProcessor,
                              final RefundProcessor refundProcessor,
+                             final Clock clock,
                              final InternalCallContextFactory internalCallContextFactory) {
         this.methodProcessor = methodProcessor;
         this.paymentProcessor = paymentProcessor;
         this.refundProcessor = refundProcessor;
+        this.clock = clock;
         this.internalCallContextFactory = internalCallContextFactory;
     }
 
@@ -174,6 +178,16 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final TenantContext context) {
+        return methodProcessor.getPaymentMethods(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+        return methodProcessor.getPaymentMethods(offset, limit, pluginName, context, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
     public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
         return methodProcessor.searchPaymentMethods(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
     }
diff --git a/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
index 9f05903..c8d8786 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/PaymentMethodProcessor.java
@@ -53,6 +53,7 @@ import com.ning.billing.payment.provider.DefaultNoOpPaymentMethodPlugin;
 import com.ning.billing.payment.provider.DefaultPaymentMethodInfoPlugin;
 import com.ning.billing.payment.provider.ExternalPaymentProviderPlugin;
 import com.ning.billing.tag.TagInternalApi;
+import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Pagination;
@@ -157,27 +158,104 @@ public class PaymentMethodProcessor extends ProcessorBase {
         return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
     }
 
+    public Pagination<PaymentMethod> getPaymentMethods(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<PaymentMethod> allResults = new LinkedList<PaymentMethod>();
+        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<PaymentMethod> paymentMethods;
+                if (allResults.size() >= limit) {
+                    // We have enough results, we just keep going (limit 1) to get the stats
+                    paymentMethods = getPaymentMethods(firstSearch ? offset : 0L, 1L, pluginName, tenantContext, internalTenantContext);
+                    // Required to close database connections
+                    ImmutableList.<PaymentMethod>copyOf(paymentMethods);
+                } else {
+                    paymentMethods = getPaymentMethods(firstSearch ? offset : 0L, limit - allResults.size(), pluginName, tenantContext, internalTenantContext);
+                    allResults.addAll(ImmutableList.<PaymentMethod>copyOf(paymentMethods));
+                }
+                firstSearch = false;
+                totalNbRecords += paymentMethods.getTotalNbRecords();
+                maxNbRecords += paymentMethods.getMaxNbRecords();
+            } catch (PaymentApiException e) {
+                log.warn("Error while searching plugin " + pluginName, e);
+                // Non-fatal, continue to search other plugins
+            }
+        }
+
+        return new DefaultPagination<PaymentMethod>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+    }
+
+    public Pagination<PaymentMethod> getPaymentMethods(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+        // Find all payment methods for all accounts
+        final Pagination<PaymentMethodModelDao> paymentMethodModelDaos = paymentDao.getPaymentMethods(pluginName, offset, limit, internalTenantContext);
+
+        return new DefaultPagination<PaymentMethod>(paymentMethodModelDaos,
+                                                    limit,
+                                                    Iterators.<PaymentMethod>filter(Iterators.<PaymentMethodModelDao, PaymentMethod>transform(paymentMethodModelDaos.iterator(),
+                                                                                                                                              new Function<PaymentMethodModelDao, PaymentMethod>() {
+                                                                                                                                                  @Override
+                                                                                                                                                  public PaymentMethod apply(final PaymentMethodModelDao paymentMethodModelDao) {
+                                                                                                                                                      final PaymentMethodPlugin paymentMethodPlugin;
+                                                                                                                                                      try {
+                                                                                                                                                          paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), tenantContext);
+                                                                                                                                                      } catch (PaymentPluginApiException e) {
+                                                                                                                                                          log.warn("Unable to find payment method id " + paymentMethodModelDao.getId() + " in plugin " + pluginName);
+                                                                                                                                                          return null;
+                                                                                                                                                      }
+
+                                                                                                                                                      if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+                                                                                                                                                          // Garbage from the plugin?
+                                                                                                                                                          log.debug("Plugin {} returned a payment method without a kbPaymentMethodId", pluginName);
+                                                                                                                                                          return null;
+                                                                                                                                                      }
+
+                                                                                                                                                      return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+                                                                                                                                                  }
+                                                                                                                                              }),
+                                                                                    Predicates.<PaymentMethod>notNull()));
+    }
+
     public Pagination<PaymentMethod> searchPaymentMethods(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
         // one plugin gets it wrong, it may starve the others).
         final List<PaymentMethod> allResults = new LinkedList<PaymentMethod>();
+        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<PaymentMethod> paymentMethods = searchPaymentMethods(searchKey, 0L, Long.MAX_VALUE, pluginName, internalTenantContext);
-                allResults.addAll(ImmutableList.<PaymentMethod>copyOf(paymentMethods));
-                if (allResults.size() > offset + limit) {
-                    break;
+                final Pagination<PaymentMethod> paymentMethods;
+                if (allResults.size() >= limit) {
+                    // We have enough results, we just keep going (limit 1) to get the stats
+                    paymentMethods = searchPaymentMethods(searchKey, firstSearch ? offset : 0L, 1L, pluginName, internalTenantContext);
+                    // Required to close database connections
+                    ImmutableList.<PaymentMethod>copyOf(paymentMethods);
+                } else {
+                    paymentMethods = searchPaymentMethods(searchKey, firstSearch ? offset : 0L, limit - allResults.size(), pluginName, internalTenantContext);
+                    allResults.addAll(ImmutableList.<PaymentMethod>copyOf(paymentMethods));
                 }
+                firstSearch = false;
+                totalNbRecords += paymentMethods.getTotalNbRecords();
+                maxNbRecords += paymentMethods.getMaxNbRecords();
             } catch (PaymentApiException e) {
                 log.warn("Error while searching plugin " + pluginName, e);
                 // Non-fatal, continue to search other plugins
             }
         }
 
-        return DefaultPagination.<PaymentMethod>build(offset, limit, allResults);
+        return new DefaultPagination<PaymentMethod>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
     }
 
     public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
@@ -190,6 +268,7 @@ public class PaymentMethodProcessor extends ProcessorBase {
         }
 
         return new DefaultPagination<PaymentMethod>(paymentMethods,
+                                                    limit,
                                                     Iterators.<PaymentMethod>filter(Iterators.<PaymentMethodPlugin, PaymentMethod>transform(paymentMethods.iterator(),
                                                                                                                                             new Function<PaymentMethodPlugin, PaymentMethod>() {
                                                                                                                                                 @Override
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 0b50ee1..7c0b657 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
@@ -17,6 +17,7 @@
 package com.ning.billing.payment.dao;
 
 import java.util.Collection;
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -24,14 +25,16 @@ import javax.inject.Inject;
 
 import org.skife.jdbi.v2.IDBI;
 
-import com.ning.billing.payment.api.PaymentStatus;
-import com.ning.billing.payment.dao.RefundModelDao.RefundStatus;
-import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.clock.Clock;
-import com.ning.billing.util.dao.NonEntityDao;
 import com.ning.billing.entity.EntityPersistenceException;
+import com.ning.billing.payment.api.PaymentStatus;
+import com.ning.billing.payment.dao.RefundModelDao.RefundStatus;
+import com.ning.billing.util.cache.CacheControllerDispatcher;
+import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
@@ -215,6 +218,34 @@ public class DefaultPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<PaymentMethodModelDao> getPaymentMethods(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 PaymentMethodSqlDao sqlDao = entitySqlDaoWrapperFactory.become(PaymentMethodSqlDao.class);
+                final Iterator<PaymentMethodModelDao> 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 PaymentMethodSqlDao paymentMethodSqlDao = transactionalSqlDao.onDemand(PaymentMethodSqlDao.class);
+        final Long totalCount = paymentMethodSqlDao.getCount(context);
+        final Iterator<PaymentMethodModelDao> results = paymentMethodSqlDao.getByPluginName(pluginName, offset, limit, context);
+
+        return new DefaultPagination<PaymentMethodModelDao>(offset, limit, count, totalCount, results);
+    }
+
+    @Override
     public void deletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
         transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
             @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 3ea92e4..505df70 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
@@ -25,6 +25,7 @@ import com.ning.billing.payment.api.PaymentStatus;
 import com.ning.billing.payment.dao.RefundModelDao.RefundStatus;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.Pagination;
 
 public interface PaymentDao {
 
@@ -65,6 +66,8 @@ public interface PaymentDao {
 
     public List<PaymentMethodModelDao> getPaymentMethods(UUID accountId, InternalTenantContext context);
 
+    public Pagination<PaymentMethodModelDao> getPaymentMethods(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
     public void deletedPaymentMethod(UUID paymentMethodId, InternalCallContext context);
 
     public List<PaymentMethodModelDao> refreshPaymentMethods(UUID accountId, String pluginName, List<PaymentMethodModelDao> paymentMethods, InternalCallContext context);
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodSqlDao.java b/payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodSqlDao.java
index cd62daf..d5af8bf 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodSqlDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/PaymentMethodSqlDao.java
@@ -16,17 +16,20 @@
 
 package com.ning.billing.payment.dao;
 
+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.Define;
+import org.skife.jdbi.v2.sqlobject.customizers.FetchSize;
 
-import com.ning.billing.payment.api.PaymentMethod;
-import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.payment.api.PaymentMethod;
+import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.entity.dao.Audited;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoStringTemplate;
@@ -53,4 +56,13 @@ public interface PaymentMethodSqlDao extends EntitySqlDao<PaymentMethodModelDao,
 
     @SqlQuery
     List<PaymentMethodModelDao> getByAccountIdIncludedDelete(@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<PaymentMethodModelDao> 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/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
index d431522..1ab82f0 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentMethodPlugin.java
@@ -19,6 +19,8 @@ package com.ning.billing.payment.provider;
 import java.util.List;
 import java.util.UUID;
 
+import javax.annotation.Nullable;
+
 import com.ning.billing.payment.api.PaymentMethodKVInfo;
 import com.ning.billing.payment.api.PaymentMethodPlugin;
 
@@ -39,7 +41,14 @@ public class DefaultNoOpPaymentMethodPlugin implements PaymentMethodPlugin {
     public DefaultNoOpPaymentMethodPlugin(final String externalId,
                                           final boolean isDefault,
                                           final List<PaymentMethodKVInfo> props) {
-        this.kbPaymentMethodId = null;
+        this(null, externalId, isDefault, props);
+    }
+
+    public DefaultNoOpPaymentMethodPlugin(@Nullable final UUID kbPaymentMethodId,
+                                          final String externalId,
+                                          final boolean isDefault,
+                                          final List<PaymentMethodKVInfo> props) {
+        this.kbPaymentMethodId = kbPaymentMethodId;
         this.externalId = externalId;
         this.isDefault = isDefault;
         this.props = props;
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
index f934d59..f33a754 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
@@ -160,7 +160,7 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
 
     @Override
     public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
-        final ImmutableList<PaymentMethodPlugin> results = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(Iterables.<PaymentMethodPlugin>concat(paymentMethods.values()), new Predicate<PaymentMethodPlugin>() {
+        final ImmutableList<PaymentMethodPlugin> allResults = ImmutableList.<PaymentMethodPlugin>copyOf(Iterables.<PaymentMethodPlugin>filter(Iterables.<PaymentMethodPlugin>concat(paymentMethods.values()), new Predicate<PaymentMethodPlugin>() {
             @Override
             public boolean apply(final PaymentMethodPlugin input) {
                 return (input.getAddress1() != null && input.getAddress1().contains(searchKey)) ||
@@ -172,7 +172,17 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
                        (input.getCountry() != null && input.getCountry().contains(searchKey));
             }
         }));
-        return DefaultPagination.<PaymentMethodPlugin>build(offset, limit, results);
+
+        final List<PaymentMethodPlugin> results;
+        if (offset >= allResults.size()) {
+            results = ImmutableList.<PaymentMethodPlugin>of();
+        } else if (offset + limit > allResults.size()) {
+            results = allResults.subList(offset.intValue(), allResults.size());
+        } else {
+            results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
+        }
+
+        return new DefaultPagination<PaymentMethodPlugin>(offset, limit, (long) results.size(), (long) paymentMethods.values().size(), results.iterator());
     }
 
     @Override
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
index 396a17d..a2abb55 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/ExternalPaymentProviderPlugin.java
@@ -88,7 +88,7 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
 
     @Override
     public PaymentMethodPlugin getPaymentMethodDetail(final UUID kbAccountId, final UUID kbPaymentMethodId, final TenantContext context) throws PaymentPluginApiException {
-        return new DefaultNoOpPaymentMethodPlugin("unknown", false, Collections.<PaymentMethodKVInfo>emptyList());
+        return new DefaultNoOpPaymentMethodPlugin(kbPaymentMethodId, "unknown", false, Collections.<PaymentMethodKVInfo>emptyList());
     }
 
     @Override
@@ -101,8 +101,8 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
     }
 
     @Override
-    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKy, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
-        return new DefaultPagination<PaymentMethodPlugin>(0L, Iterators.<PaymentMethodPlugin>emptyIterator());
+    public Pagination<PaymentMethodPlugin> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new DefaultPagination<PaymentMethodPlugin>(offset, limit, 0L, 0L, Iterators.<PaymentMethodPlugin>emptyIterator());
     }
 
     @Override
diff --git a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentMethodSqlDao.sql.stg b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentMethodSqlDao.sql.stg
index 755a1ee..8652152 100644
--- a/payment/src/main/resources/com/ning/billing/payment/dao/PaymentMethodSqlDao.sql.stg
+++ b/payment/src/main/resources/com/ning/billing/payment/dao/PaymentMethodSqlDao.sql.stg
@@ -68,3 +68,14 @@ from <tableName()>
 where account_id = :accountId
 ;
 >>
+
+getByPluginName(pluginName, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields()>
+from <tableName()>
+where plugin_name = :pluginName
+and 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 2c5e51f..418301b 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
@@ -25,10 +25,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
-import com.ning.billing.payment.api.PaymentStatus;
-import com.ning.billing.payment.dao.RefundModelDao.RefundStatus;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.payment.api.PaymentStatus;
+import com.ning.billing.payment.dao.RefundModelDao.RefundStatus;
+import com.ning.billing.util.entity.Pagination;
 
 import com.google.common.collect.ImmutableList;
 
@@ -150,6 +151,11 @@ public class MockPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<PaymentMethodModelDao> getPaymentMethods(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public void deletedPaymentMethod(final UUID paymentMethodId, final InternalCallContext context) {
         final Iterator<PaymentMethodModelDao> it = paymentMethods.iterator();
         while (it.hasNext()) {

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 68be740..1f51d19 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.0-SNAPSHOT</version>
+        <version>0.5.1</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.6.18-SNAPSHOT</version>
diff --git a/server/src/main/resources/logback.xml b/server/src/main/resources/logback.xml
index 0cb11ef..8649b66 100644
--- a/server/src/main/resources/logback.xml
+++ b/server/src/main/resources/logback.xml
@@ -23,7 +23,14 @@
     </encoder>
   </appender>
 
-  <root level="info">
+  <!-- Silence verbose loggers in DEBUG mode -->
+  <logger name="com.dmurph" level="INFO"/>
+  <logger name="com.ning.billing.notificationq" level="INFO"/>
+  <logger name="com.ning.billing.queue" level="INFO"/>
+  <logger name="com.ning.billing.server.updatechecker" level="INFO"/>
+  <logger name="org.eclipse" level="INFO"/>
+
+  <root level="INFO">
     <appender-ref ref="STDOUT" />
   </root>
 </configuration>
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
index 477a305..8989b1b 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDao.java
@@ -36,7 +36,7 @@ public interface EntityDao<M extends EntityModelDao<E>, E extends Entity, U exte
 
     public Pagination<M> getAll(InternalTenantContext context);
 
-    public Pagination<M> get(Long offset, Long rowCount, InternalTenantContext context);
+    public Pagination<M> get(Long offset, Long limit, InternalTenantContext context);
 
     public Long getCount(InternalTenantContext context);
 
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
index f2e2462..d494c4e 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntityDaoBase.java
@@ -129,13 +129,31 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
     }
 
     @Override
-    public Pagination<M> get(final Long offset, final Long rowCount, final InternalTenantContext context) {
-        // See notes above
+    public Pagination<M> get(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 EntitySqlDao<M, E> sqlDao = entitySqlDaoWrapperFactory.become(realSqlDao);
+                final Iterator<M> dumbIterator = sqlDao.get(offset, 1L, getNaturalOrderingColumns(), 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 EntitySqlDao<M, E> sqlDao = transactionalSqlDao.onDemand(realSqlDao);
-        final Long count = sqlDao.getCount(context);
+        final Long totalCount = sqlDao.getCount(context);
+        final Iterator<M> results = sqlDao.get(offset, limit, getNaturalOrderingColumns(), context);
 
-        final Iterator<M> results = sqlDao.get(offset, rowCount, getNaturalOrderingColumns(), context);
-        return new DefaultPagination<M>(offset, rowCount, count, results);
+        return new DefaultPagination<M>(offset, limit, count, totalCount, results);
     }
 
     @Override
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
index d6f8fbe..f4c9148 100644
--- a/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/EntitySqlDao.java
@@ -68,6 +68,9 @@ public interface EntitySqlDao<M extends EntityModelDao<E>, E extends Entity> ext
                             @BindBean final InternalTenantContext context);
 
     @SqlQuery
+    public Long getFoundRows(@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)
diff --git a/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java b/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
index 0000879..71389d1 100644
--- a/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
+++ b/util/src/main/java/com/ning/billing/util/entity/DefaultPagination.java
@@ -20,16 +20,21 @@ import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 import com.google.common.collect.ImmutableList;
 
+// Assumes the original offset starts at zero.
 public class DefaultPagination<T> implements Pagination<T> {
 
     private final Long currentOffset;
-    private final Long approximateNbResults;
-    private final Long approximateTotalNbResults;
+    private final Long limit;
+    private final Long totalNbRecords;
+    private final Long maxNbRecords;
     private final Iterator<T> delegateIterator;
 
-    // Builder when the streaming API can't be used
+    // Builder when the streaming API can't be used (should only be used for tests)
+    // Notes: elements should be the entire records set (regardless of filtering) otherwise maxNbRecords won't be accurate
     public static <T> Pagination<T> build(final Long offset, final Long limit, final Collection<T> elements) {
         final List<T> allResults = ImmutableList.<T>copyOf(elements);
 
@@ -41,24 +46,26 @@ public class DefaultPagination<T> implements Pagination<T> {
         } else {
             results = allResults.subList(offset.intValue(), offset.intValue() + limit.intValue());
         }
-        return new DefaultPagination<T>(offset, (long) results.size(), (long) allResults.size(), results.iterator());
+        return new DefaultPagination<T>(offset, limit, (long) results.size(), (long) allResults.size(), results.iterator());
     }
 
     // Constructor for DAO -> API bridge
-    public DefaultPagination(final Pagination original, final Iterator<T> delegate) {
-        this(original.getCurrentOffset(), original.getNbResults(), original.getTotalNbResults(), delegate);
+    public DefaultPagination(final Pagination original, final Long limit, final Iterator<T> delegate) {
+        this(original.getCurrentOffset(), limit, original.getTotalNbRecords(), original.getMaxNbRecords(), delegate);
     }
 
     // Constructor for DAO getAll calls
-    public DefaultPagination(final Long approximateTotalNbResults, final Iterator<T> results) {
-        this(0L, approximateTotalNbResults, approximateTotalNbResults, results);
+    public DefaultPagination(final Long maxNbRecords, final Iterator<T> results) {
+        this(0L, Long.MAX_VALUE, maxNbRecords, maxNbRecords, results);
     }
 
-    public DefaultPagination(final Long currentOffset, final Long approximateNbResults,
-                             final Long approximateTotalNbResults, final Iterator<T> delegateIterator) {
+    public DefaultPagination(final Long currentOffset, final Long limit,
+                             @Nullable final Long totalNbRecords, @Nullable final Long maxNbRecords,
+                             final Iterator<T> delegateIterator) {
         this.currentOffset = currentOffset;
-        this.approximateNbResults = approximateNbResults;
-        this.approximateTotalNbResults = approximateTotalNbResults;
+        this.limit = limit;
+        this.totalNbRecords = totalNbRecords;
+        this.maxNbRecords = maxNbRecords;
         this.delegateIterator = delegateIterator;
     }
 
@@ -74,22 +81,27 @@ public class DefaultPagination<T> implements Pagination<T> {
 
     @Override
     public Long getNextOffset() {
-        return currentOffset + approximateNbResults;
-    }
-
-    @Override
-    public Long getTotalNbResults() {
-        return approximateTotalNbResults;
+        final long candidate = currentOffset + limit;
+        if (totalNbRecords != null && candidate >= totalNbRecords) {
+            // No more results
+            return null;
+        } else {
+            // When we don't know the total number of records, the next offset
+            // returned here won't make sense once the last result is returned.
+            // It is the responsibility of the client to handle the pagination stop condition
+            // in that case (i.e. check if there is no more results).
+            return candidate;
+        }
     }
 
     @Override
-    public Long getNbResults() {
-        return approximateNbResults;
+    public Long getMaxNbRecords() {
+        return maxNbRecords;
     }
 
     @Override
-    public Long getNbResultsFromOffset() {
-        return approximateNbResults - getNextOffset() + 1;
+    public Long getTotalNbRecords() {
+        return totalNbRecords;
     }
 
     @Override
@@ -97,9 +109,8 @@ public class DefaultPagination<T> implements Pagination<T> {
         final StringBuilder sb = new StringBuilder("DefaultPagination{");
         sb.append("currentOffset=").append(currentOffset);
         sb.append(", nextOffset=").append(getNextOffset());
-        sb.append(", approximateNbResults=").append(approximateNbResults);
-        sb.append(", approximateTotalNbResults=").append(approximateTotalNbResults);
-        sb.append(", approximateNbResultsFromOffset=").append(getNbResultsFromOffset());
+        sb.append(", totalNbRecords=").append(totalNbRecords);
+        sb.append(", maxNbRecords=").append(maxNbRecords);
         sb.append('}');
         return sb.toString();
     }
@@ -116,10 +127,10 @@ public class DefaultPagination<T> implements Pagination<T> {
 
         final DefaultPagination that = (DefaultPagination) o;
 
-        if (approximateNbResults != null ? !approximateNbResults.equals(that.approximateNbResults) : that.approximateNbResults != null) {
+        if (totalNbRecords != null ? !totalNbRecords.equals(that.totalNbRecords) : that.totalNbRecords != null) {
             return false;
         }
-        if (approximateTotalNbResults != null ? !approximateTotalNbResults.equals(that.approximateTotalNbResults) : that.approximateTotalNbResults != null) {
+        if (maxNbRecords != null ? !maxNbRecords.equals(that.maxNbRecords) : that.maxNbRecords != null) {
             return false;
         }
         if (currentOffset != null ? !currentOffset.equals(that.currentOffset) : that.currentOffset != null) {
@@ -135,8 +146,8 @@ public class DefaultPagination<T> implements Pagination<T> {
     @Override
     public int hashCode() {
         int result = currentOffset != null ? currentOffset.hashCode() : 0;
-        result = 31 * result + (approximateNbResults != null ? approximateNbResults.hashCode() : 0);
-        result = 31 * result + (approximateTotalNbResults != null ? approximateTotalNbResults.hashCode() : 0);
+        result = 31 * result + (totalNbRecords != null ? totalNbRecords.hashCode() : 0);
+        result = 31 * result + (maxNbRecords != null ? maxNbRecords.hashCode() : 0);
         result = 31 * result + (delegateIterator != null ? delegateIterator.hashCode() : 0);
         return result;
     }
diff --git a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
index 646d457..1bc6a90 100644
--- a/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
+++ b/util/src/main/resources/com/ning/billing/util/entity/dao/EntitySqlDao.sql.stg
@@ -133,6 +133,10 @@ allHistoryTableValues() ::= <<
 CHECK_TENANT(prefix) ::= "<prefix>tenant_record_id = :tenantRecordId"
 AND_CHECK_TENANT(prefix) ::= "and <CHECK_TENANT(prefix)>"
 
+getFoundRows() ::= <<
+select FOUND_ROWS();
+>>
+
 getAll() ::= <<
 select
 <allTableFields("t.")>
@@ -143,7 +147,7 @@ where <CHECK_TENANT("t.")>
 >>
 
 get(offset, rowCount, orderBy) ::= <<
-select
+select SQL_CALC_FOUND_ROWS
 <allTableFields("t.")>
 from <tableName()> t
 where <CHECK_TENANT("t.")>
diff --git a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
index bc1c714..1306cfe 100644
--- a/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
+++ b/util/src/test/java/com/ning/billing/util/dao/TestStringTemplateInheritance.java
@@ -113,7 +113,7 @@ public class TestStringTemplateInheritance extends UtilTestSuiteNoDB {
                                                                          "from kombucha t\n" +
                                                                          "where t.tenant_record_id = :tenantRecordId\n" +
                                                                          ";");
-        Assert.assertEquals(kombucha.getInstanceOf("get", ImmutableMap.<String, String>of("orderBy", "recordId", "offset", "3", "rowCount", "12")).toString(), "select\n" +
+        Assert.assertEquals(kombucha.getInstanceOf("get", ImmutableMap.<String, String>of("orderBy", "recordId", "offset", "3", "rowCount", "12")).toString(), "select SQL_CALC_FOUND_ROWS\n" +
                                                                                                                                                                "  t.record_id\n" +
                                                                                                                                                                ", t.id\n" +
                                                                                                                                                                ", t.tea\n" +
diff --git a/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java b/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
index d51217e..0aabc34 100644
--- a/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
+++ b/util/src/test/java/com/ning/billing/util/entity/dao/MockEntityDaoBase.java
@@ -74,8 +74,8 @@ public class MockEntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U 
     }
 
     @Override
-    public Pagination<M> get(final Long offset, final Long rowCount, final InternalTenantContext context) {
-        return DefaultPagination.<M>build(offset, rowCount, ImmutableList.<M>copyOf(getAll(context)));
+    public Pagination<M> get(final Long offset, final Long limit, final InternalTenantContext context) {
+        return DefaultPagination.<M>build(offset, limit, ImmutableList.<M>copyOf(getAll(context)));
     }
 
     @Override
diff --git a/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java b/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java
index cfb29f5..2600ff1 100644
--- a/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java
+++ b/util/src/test/java/com/ning/billing/util/entity/TestDefaultPagination.java
@@ -46,8 +46,8 @@ public class TestDefaultPagination extends UtilTestSuiteNoDB {
         Assert.assertEquals(DefaultPagination.<Integer>build(5L, 3L, ImmutableList.<Integer>of(1, 2, 3, 4, 5)), expectedOf(5L, 0L, 5L, ImmutableList.<Integer>of()));
     }
 
-    private Pagination<Integer> expectedOf(final Long currentOffset, final Long approximateNbResults,
-                                           final Long approximateTotalNbResults, final List<Integer> delegate) {
-        return new DefaultPagination<Integer>(currentOffset, approximateNbResults, approximateTotalNbResults, delegate.iterator());
+    private Pagination<Integer> expectedOf(final Long currentOffset, final Long totalNbRecords,
+                                           final Long maxNbRecords, final List<Integer> delegate) {
+        return new DefaultPagination<Integer>(currentOffset, Long.MAX_VALUE, totalNbRecords, maxNbRecords, delegate.iterator());
     }
 }