killbill-memoizeit

search: implement new search APIs Refactor the existing

1/14/2014 7:43:07 PM

Changes

pom.xml 2(+1 -1)

Details

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 d1ffe58..4c935bb 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
@@ -34,15 +34,16 @@ import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallContextFactory;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
-import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
 public class DefaultAccountUserApi implements AccountUserApi {
 
     private final CallContextFactory callContextFactory;
@@ -92,30 +93,38 @@ public class DefaultAccountUserApi implements AccountUserApi {
 
     @Override
     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
-                                                                                                public Account apply(final AccountModelDao input) {
-                                                                                                    return new DefaultAccount(input);
-                                                                                                }
-                                                                                            }));
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<AccountModelDao, AccountApiException>() {
+                                                  @Override
+                                                  public Pagination<AccountModelDao> build() {
+                                                      return accountDao.searchAccounts(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              new Function<AccountModelDao, Account>() {
+                                                  @Override
+                                                  public Account apply(final AccountModelDao accountModelDao) {
+                                                      return new DefaultAccount(accountModelDao);
+                                                  }
+                                              }
+                                             );
     }
 
     @Override
     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
-                                                                                                public Account apply(final AccountModelDao input) {
-                                                                                                    return new DefaultAccount(input);
-                                                                                                }
-                                                                                            }));
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<AccountModelDao, AccountApiException>() {
+                                                  @Override
+                                                  public Pagination<AccountModelDao> build() {
+                                                      return accountDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              new Function<AccountModelDao, Account>() {
+                                                  @Override
+                                                  public Account apply(final AccountModelDao accountModelDao) {
+                                                      return new DefaultAccount(accountModelDao);
+                                                  }
+                                              }
+                                             );
     }
 
     @Override
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 fca3684..3ce879e 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
@@ -43,8 +43,8 @@ import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 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.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
@@ -108,30 +108,16 @@ public class DefaultAccountDao extends EntityDaoBase<AccountModelDao, Account, A
 
     @Override
     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);
-
-        return new DefaultPagination<AccountModelDao>(offset, limit, count, totalCount, results);
+        return paginationHelper.getPagination(AccountSqlDao.class,
+                                              new PaginationIteratorBuilder<AccountModelDao, Account, AccountSqlDao>() {
+                                                  @Override
+                                                  public Iterator<AccountModelDao> build(final AccountSqlDao accountSqlDao, final Long limit) {
+                                                      return accountSqlDao.searchAccounts(searchKey, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
     }
 
     @Override
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 d3b627f..171d337 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
@@ -56,17 +56,18 @@ import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
-import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.Tag;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
 public class DefaultInvoiceUserApi implements InvoiceUserApi {
 
     private static final Logger log = LoggerFactory.getLogger(DefaultInvoiceUserApi.class);
@@ -115,17 +116,21 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
 
     @Override
     public Pagination<Invoice> getInvoices(final Long offset, final Long limit, final TenantContext context) {
-        // 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
-                                                                                                public Invoice apply(final InvoiceModelDao input) {
-                                                                                                    return new DefaultInvoice(input);
-                                                                                                }
-                                                                                            }));
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<InvoiceModelDao, InvoiceApiException>() {
+                                                  @Override
+                                                  public Pagination<InvoiceModelDao> build() {
+                                                      // Invoices will be shallow, i.e. won't contain items nor payments
+                                                      return dao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              new Function<InvoiceModelDao, Invoice>() {
+                                                  @Override
+                                                  public Invoice apply(final InvoiceModelDao invoiceModelDao) {
+                                                      return new DefaultInvoice(invoiceModelDao);
+                                                  }
+                                              }
+                                             );
     }
 
     @Override
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentMethodJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentMethodJson.java
index 5321232..6a44dc1 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentMethodJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/PaymentMethodJson.java
@@ -33,7 +33,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 
-public class PaymentMethodJson {
+public class PaymentMethodJson extends JsonBase {
 
     private final String paymentMethodId;
     private final String accountId;
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
index 7840747..ec0dd64 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/json/TagJson.java
@@ -20,7 +20,9 @@ import java.util.List;
 
 import javax.annotation.Nullable;
 
+import com.ning.billing.ObjectType;
 import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.TagDefinition;
 
 import com.fasterxml.jackson.annotation.JsonCreator;
@@ -28,20 +30,34 @@ import com.fasterxml.jackson.annotation.JsonProperty;
 
 public class TagJson extends JsonBase {
 
+    private final String tagId;
+    private final ObjectType objectType;
     private final String tagDefinitionId;
     private final String tagDefinitionName;
 
     @JsonCreator
-    public TagJson(@JsonProperty("tagDefinitionId") final String tagDefinitionId,
+    public TagJson(@JsonProperty("tagId") final String tagId,
+                   @JsonProperty("objectType") final ObjectType objectType,
+                   @JsonProperty("tagDefinitionId") final String tagDefinitionId,
                    @JsonProperty("tagDefinitionName") final String tagDefinitionName,
                    @JsonProperty("auditLogs") @Nullable final List<AuditLogJson> auditLogs) {
         super(auditLogs);
+        this.tagId = tagId;
+        this.objectType = objectType;
         this.tagDefinitionId = tagDefinitionId;
         this.tagDefinitionName = tagDefinitionName;
     }
 
-    public TagJson(final TagDefinition tagDefinition, @Nullable final List<AuditLog> auditLogs) {
-        this(tagDefinition.getId().toString(), tagDefinition.getName(), toAuditLogJson(auditLogs));
+    public TagJson(final Tag tag, final TagDefinition tagDefinition, @Nullable final List<AuditLog> auditLogs) {
+        this(tag.getId().toString(), tag.getObjectType(), tagDefinition.getId().toString(), tagDefinition.getName(), toAuditLogJson(auditLogs));
+    }
+
+    public String getTagId() {
+        return tagId;
+    }
+
+    public ObjectType getObjectType() {
+        return objectType;
     }
 
     public String getTagDefinitionId() {
@@ -55,7 +71,9 @@ public class TagJson extends JsonBase {
     @Override
     public String toString() {
         final StringBuilder sb = new StringBuilder("TagJson{");
-        sb.append("tagDefinitionId='").append(tagDefinitionId).append('\'');
+        sb.append("tagId='").append(tagId).append('\'');
+        sb.append(", objectType=").append(objectType);
+        sb.append(", tagDefinitionId='").append(tagDefinitionId).append('\'');
         sb.append(", tagDefinitionName='").append(tagDefinitionName).append('\'');
         sb.append('}');
         return sb.toString();
@@ -72,19 +90,27 @@ public class TagJson extends JsonBase {
 
         final TagJson tagJson = (TagJson) o;
 
+        if (objectType != tagJson.objectType) {
+            return false;
+        }
         if (tagDefinitionId != null ? !tagDefinitionId.equals(tagJson.tagDefinitionId) : tagJson.tagDefinitionId != null) {
             return false;
         }
         if (tagDefinitionName != null ? !tagDefinitionName.equals(tagJson.tagDefinitionName) : tagJson.tagDefinitionName != null) {
             return false;
         }
+        if (tagId != null ? !tagId.equals(tagJson.tagId) : tagJson.tagId != null) {
+            return false;
+        }
 
         return true;
     }
 
     @Override
     public int hashCode() {
-        int result = tagDefinitionId != null ? tagDefinitionId.hashCode() : 0;
+        int result = tagId != null ? tagId.hashCode() : 0;
+        result = 31 * result + (objectType != null ? objectType.hashCode() : 0);
+        result = 31 * result + (tagDefinitionId != null ? tagDefinitionId.hashCode() : 0);
         result = 31 * result + (tagDefinitionName != null ? tagDefinitionName.hashCode() : 0);
         return result;
     }
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 eb44ad3..f1727d0 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
@@ -16,8 +16,6 @@
 
 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;
@@ -39,10 +37,8 @@ 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.ErrorCode;
@@ -96,7 +92,6 @@ import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.tag.ControlTagType;
 
-import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Function;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.Collections2;
@@ -163,8 +158,16 @@ 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.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);
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "getAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_ACCOUNT_WITH_BALANCE, accountWithBalance.toString(),
+                                                                                                                                                           QUERY_ACCOUNT_WITH_BALANCE_AND_CBA, accountWithBalanceAndCBA.toString()));
+        return buildStreamingPaginationResponse(accounts,
+                                                new Function<Account, AccountJson>() {
+                                                    @Override
+                                                    public AccountJson apply(final Account account) {
+                                                        return getAccount(account, accountWithBalance, accountWithBalanceAndCBA, tenantContext);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @GET
@@ -178,35 +181,17 @@ 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 {
-                final JsonGenerator generator = mapper.getFactory().createJsonGenerator(output);
-                generator.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false);
-
-                generator.writeStartArray();
-                for (final Account account : accounts) {
-                    final AccountJson asJson = getAccount(account, accountWithBalance, accountWithBalanceAndCBA, tenantContext);
-                    generator.writeObject(asJson);
-                }
-                generator.writeEndArray();
-                generator.close();
-            }
-        };
-        return Response.status(Status.OK)
-                       .entity(json)
-                       .header(HDR_PAGINATION_CURRENT_OFFSET, accounts.getCurrentOffset())
-                       .header(HDR_PAGINATION_NEXT_OFFSET, accounts.getNextOffset())
-                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, accounts.getTotalNbRecords())
-                       .header(HDR_PAGINATION_MAX_NB_RECORDS, accounts.getMaxNbRecords())
-                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
-                       .build();
+        final URI nextPageUri = uriBuilder.nextPage(AccountResource.class, "searchAccounts", accounts.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+                                                                                                                                                              QUERY_ACCOUNT_WITH_BALANCE, accountWithBalance.toString(),
+                                                                                                                                                              QUERY_ACCOUNT_WITH_BALANCE_AND_CBA, accountWithBalanceAndCBA.toString()));
+        return buildStreamingPaginationResponse(accounts,
+                                                new Function<Account, AccountJson>() {
+                                                    @Override
+                                                    public AccountJson apply(final Account account) {
+                                                        return getAccount(account, accountWithBalance, accountWithBalanceAndCBA, tenantContext);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @GET
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CustomFieldResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CustomFieldResource.java
new file mode 100644
index 0000000..641a74d
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/CustomFieldResource.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.resources;
+
+import java.net.URI;
+import java.util.List;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
+import javax.ws.rs.GET;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Response;
+
+import com.ning.billing.ObjectType;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.clock.Clock;
+import com.ning.billing.jaxrs.json.CustomFieldJson;
+import com.ning.billing.jaxrs.util.Context;
+import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.api.CustomFieldApiException;
+import com.ning.billing.util.api.CustomFieldUserApi;
+import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.customfield.CustomField;
+import com.ning.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.CUSTOM_FIELDS_PATH)
+public class CustomFieldResource extends JaxRsResourceBase {
+
+    @Inject
+    public CustomFieldResource(final JaxrsUriBuilder uriBuilder,
+                               final TagUserApi tagUserApi,
+                               final CustomFieldUserApi customFieldUserApi,
+                               final AuditUserApi auditUserApi,
+                               final AccountUserApi accountUserApi,
+                               final Clock clock,
+                               final Context context) {
+        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+    }
+
+    @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getCustomFields(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                    @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                    @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                    @javax.ws.rs.core.Context final HttpServletRequest request) throws CustomFieldApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<CustomField> customFields = customFieldUserApi.getCustomFields(offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(CustomFieldResource.class, "getCustomFields", customFields.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_AUDIT, auditMode.toString()));
+
+        return buildStreamingPaginationResponse(customFields,
+                                                new Function<CustomField, CustomFieldJson>() {
+                                                    @Override
+                                                    public CustomFieldJson apply(final CustomField customField) {
+                                                        // TODO Really slow - we should instead try to figure out the account id
+                                                        final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(customField.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), tenantContext);
+                                                        return new CustomFieldJson(customField, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
+    }
+
+    @GET
+    @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+    @Produces(APPLICATION_JSON)
+    public Response searchCustomFields(@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_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                       @javax.ws.rs.core.Context final HttpServletRequest request) throws CustomFieldApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<CustomField> customFields = customFieldUserApi.searchCustomFields(searchKey, offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(CustomFieldResource.class, "searchCustomFields", customFields.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+                                                                                                                                                                          QUERY_AUDIT, auditMode.toString()));
+        return buildStreamingPaginationResponse(customFields,
+                                                new Function<CustomField, CustomFieldJson>() {
+                                                    @Override
+                                                    public CustomFieldJson apply(final CustomField customField) {
+                                                        // TODO Really slow - we should instead try to figure out the account id
+                                                        final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(customField.getId(), ObjectType.CUSTOM_FIELD, auditMode.getLevel(), tenantContext);
+                                                        return new CustomFieldJson(customField, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
index dbd5d85..44522e0 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/InvoiceResource.java
@@ -17,9 +17,13 @@
 package com.ning.billing.jaxrs.resources;
 
 import java.io.IOException;
+import java.net.URI;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.Consumes;
@@ -71,8 +75,11 @@ import com.ning.billing.util.audit.AccountAuditLogs;
 import com.ning.billing.util.audit.AccountAuditLogsForObjectType;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
+import com.google.common.base.Function;
 import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@@ -151,6 +158,34 @@ public class InvoiceResource extends JaxRsResourceBase {
         return Response.status(Status.OK).entity(invoiceApi.getInvoiceAsHTML(UUID.fromString(invoiceId), context.createContext(request))).build();
     }
 
+    @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getInvoices(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                                @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                                @QueryParam(QUERY_INVOICE_WITH_ITEMS) @DefaultValue("false") final Boolean withItems,
+                                @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                @javax.ws.rs.core.Context final HttpServletRequest request) throws InvoiceApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<Invoice> invoices = invoiceApi.getInvoices(offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(InvoiceResource.class, "getInvoices", invoices.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_INVOICE_WITH_ITEMS, withItems.toString(),
+                                                                                                                                                           QUERY_AUDIT, auditMode.toString()));
+
+        final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+        return buildStreamingPaginationResponse(invoices,
+                                                new Function<Invoice, InvoiceJson>() {
+                                                    @Override
+                                                    public InvoiceJson apply(final Invoice invoice) {
+                                                        // Cache audit logs per account
+                                                        if (accountsAuditLogs.get().get(invoice.getAccountId()) == null) {
+                                                            accountsAuditLogs.get().put(invoice.getAccountId(), auditUserApi.getAccountAuditLogs(invoice.getAccountId(), auditMode.getLevel(), tenantContext));
+                                                        }
+                                                        return new InvoiceJson(invoice, withItems, accountsAuditLogs.get().get(invoice.getAccountId()));
+                                                    }
+                                                },
+                                                nextPageUri);
+    }
+
     @POST
     @Consumes(APPLICATION_JSON)
     @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 0234816..71d8418 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
@@ -148,7 +148,11 @@ public interface JaxrsResource {
     public static final String CHARGEBACKS_PATH = PREFIX + "/" + CHARGEBACKS;
 
     public static final String TAGS = "tags";
+    public static final String TAGS_PATH = PREFIX + "/" + TAGS;
+
     public static final String CUSTOM_FIELDS = "customFields";
+    public static final String CUSTOM_FIELDS_PATH = PREFIX + "/" + CUSTOM_FIELDS;
+
     public static final String EMAILS = "emails";
     public static final String EMAIL_NOTIFICATIONS = "emailNotifications";
 
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 e583cd4..5a265fd 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
@@ -16,6 +16,9 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -23,7 +26,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 
+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 org.joda.time.DateTime;
@@ -41,6 +47,7 @@ import com.ning.billing.account.api.AccountApiException;
 import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.clock.Clock;
 import com.ning.billing.jaxrs.json.CustomFieldJson;
+import com.ning.billing.jaxrs.json.JsonBase;
 import com.ning.billing.jaxrs.json.TagJson;
 import com.ning.billing.jaxrs.util.Context;
 import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
@@ -56,10 +63,13 @@ import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.customfield.StringCustomField;
+import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.jackson.ObjectMapper;
 import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.TagDefinition;
 
+import com.fasterxml.jackson.core.JsonGenerator;
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
@@ -114,7 +124,7 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
             final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
 
             final List<AuditLog> auditLogs = tagsAuditLogs.getAuditLogs(tag.getId());
-            result.add(new TagJson(tagDefinition, auditLogs));
+            result.add(new TagJson(tag, tagDefinition, auditLogs));
         }
 
         return Response.status(Response.Status.OK).entity(result).build();
@@ -181,6 +191,34 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
         return Response.status(Response.Status.OK).build();
     }
 
+    protected <E extends Entity, J extends JsonBase> Response buildStreamingPaginationResponse(final Pagination<E> entities,
+                                                                                               final Function<E, J> toJson,
+                                                                                               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 E entity : entities) {
+                    generator.writeObject(toJson.apply(entity));
+                }
+                generator.writeEndArray();
+                generator.close();
+            }
+        };
+
+        return Response.status(Status.OK)
+                       .entity(json)
+                       .header(HDR_PAGINATION_CURRENT_OFFSET, entities.getCurrentOffset())
+                       .header(HDR_PAGINATION_NEXT_OFFSET, entities.getNextOffset())
+                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, entities.getTotalNbRecords())
+                       .header(HDR_PAGINATION_MAX_NB_RECORDS, entities.getMaxNbRecords())
+                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
+                       .build();
+    }
+
     protected LocalDate toLocalDate(final UUID accountId, final String inputDate, final TenantContext context) {
 
         final LocalDate maybeResult = extractLocalDate(inputDate);
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 f273d0a..de740e4 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
@@ -16,12 +16,11 @@
 
 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;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.DELETE;
@@ -32,10 +31,8 @@ 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 com.ning.billing.ObjectType;
 import com.ning.billing.account.api.Account;
@@ -51,11 +48,12 @@ import com.ning.billing.payment.api.PaymentMethod;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AccountAuditLogs;
 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.ImmutableMap;
 import com.google.inject.Inject;
@@ -103,20 +101,48 @@ public class PaymentMethodResource extends JaxRsResourceBase {
     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,
+                                      @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
                                       @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);
+        final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "getPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+                                                                                                                                                                             QUERY_AUDIT, auditMode.toString()));
+
+        final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+        final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
+        return buildStreamingPaginationResponse(paymentMethods,
+                                                new Function<PaymentMethod, PaymentMethodJson>() {
+                                                    @Override
+                                                    public PaymentMethodJson apply(final PaymentMethod paymentMethod) {
+                                                        // Cache audit logs per account
+                                                        if (accountsAuditLogs.get().get(paymentMethod.getAccountId()) == null) {
+                                                            accountsAuditLogs.get().put(paymentMethod.getAccountId(), auditUserApi.getAccountAuditLogs(paymentMethod.getAccountId(), auditMode.getLevel(), tenantContext));
+                                                        }
+
+                                                        // Lookup the associated account(s)
+                                                        if (accounts.get(paymentMethod.getAccountId()) == null) {
+                                                            final Account account;
+                                                            try {
+                                                                account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+                                                                accounts.put(paymentMethod.getAccountId(), account);
+                                                            } catch (final AccountApiException e) {
+                                                                log.warn("Unable to retrieve account", e);
+                                                                return null;
+                                                            }
+                                                        }
+
+                                                        // TODO populate audit logs
+                                                        return PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @GET
@@ -126,6 +152,7 @@ public class PaymentMethodResource extends JaxRsResourceBase {
                                          @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,
+                                         @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
                                          @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException, AccountApiException {
         final TenantContext tenantContext = context.createContext(request);
 
@@ -137,47 +164,38 @@ public class PaymentMethodResource extends JaxRsResourceBase {
             paymentMethods = paymentApi.searchPaymentMethods(searchKey, offset, limit, pluginName, tenantContext);
         }
 
-        final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "searchPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of());
-        return buildStreamingPaymentMethodsResponse(paymentMethods, nextPageUri, tenantContext);
-    }
+        final URI nextPageUri = uriBuilder.nextPage(PaymentMethodResource.class, "searchPaymentMethods", paymentMethods.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+                                                                                                                                                                                QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+                                                                                                                                                                                QUERY_AUDIT, auditMode.toString()));
 
-    private Response buildStreamingPaymentMethodsResponse(final Pagination<PaymentMethod> paymentMethods, final URI nextPageUri, final TenantContext tenantContext) {
+        final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
         final Map<UUID, Account> accounts = new HashMap<UUID, Account>();
-        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 PaymentMethod paymentMethod : paymentMethods) {
-                    // Lookup the associated account(s)
-                    if (accounts.get(paymentMethod.getAccountId()) == null) {
-                        final Account account;
-                        try {
-                            account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
-                            accounts.put(paymentMethod.getAccountId(), account);
-                        } catch (AccountApiException e) {
-                            log.warn("Unable to retrieve account", e);
-                            continue;
-                        }
-                    }
-
-                    final PaymentMethodJson asJson = PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod);
-                    generator.writeObject(asJson);
-                }
-                generator.writeEndArray();
-                generator.close();
-            }
-        };
-        return Response.status(Status.OK)
-                       .entity(json)
-                       .header(HDR_PAGINATION_CURRENT_OFFSET, paymentMethods.getCurrentOffset())
-                       .header(HDR_PAGINATION_NEXT_OFFSET, paymentMethods.getNextOffset())
-                       .header(HDR_PAGINATION_TOTAL_NB_RECORDS, paymentMethods.getTotalNbRecords())
-                       .header(HDR_PAGINATION_MAX_NB_RECORDS, paymentMethods.getMaxNbRecords())
-                       .header(HDR_PAGINATION_NEXT_PAGE_URI, nextPageUri)
-                       .build();
+        return buildStreamingPaginationResponse(paymentMethods,
+                                                new Function<PaymentMethod, PaymentMethodJson>() {
+                                                    @Override
+                                                    public PaymentMethodJson apply(final PaymentMethod paymentMethod) {
+                                                        // Cache audit logs per account
+                                                        if (accountsAuditLogs.get().get(paymentMethod.getAccountId()) == null) {
+                                                            accountsAuditLogs.get().put(paymentMethod.getAccountId(), auditUserApi.getAccountAuditLogs(paymentMethod.getAccountId(), auditMode.getLevel(), tenantContext));
+                                                        }
+
+                                                        // Lookup the associated account(s)
+                                                        if (accounts.get(paymentMethod.getAccountId()) == null) {
+                                                            final Account account;
+                                                            try {
+                                                                account = accountUserApi.getAccountById(paymentMethod.getAccountId(), tenantContext);
+                                                                accounts.put(paymentMethod.getAccountId(), account);
+                                                            } catch (final AccountApiException e) {
+                                                                log.warn("Unable to retrieve account", e);
+                                                                return null;
+                                                            }
+                                                        }
+
+                                                        // TODO populate audit logs
+                                                        return PaymentMethodJson.toPaymentMethodJson(accounts.get(paymentMethod.getAccountId()), paymentMethod);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @DELETE
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 2856f16..73ac13a 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,8 +16,6 @@
 
 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;
@@ -38,10 +36,8 @@ 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;
@@ -73,7 +69,6 @@ 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;
@@ -158,7 +153,15 @@ public class PaymentResource extends JaxRsResourceBase {
         }
 
         final URI nextPageUri = uriBuilder.nextPage(PaymentResource.class, "getPayments", payments.getNextOffset(), limit, nextUriParams);
-        return buildStreamingPaymentsResponse(payments, nextPageUri);
+
+        return buildStreamingPaginationResponse(payments,
+                                                new Function<Payment, PaymentJson>() {
+                                                    @Override
+                                                    public PaymentJson apply(final Payment payment) {
+                                                        return new PaymentJson(payment, null);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @GET
@@ -168,7 +171,7 @@ public class PaymentResource extends JaxRsResourceBase {
                                    @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 {
+                                   @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
         final TenantContext tenantContext = context.createContext(request);
 
         // Search the plugin(s)
@@ -180,33 +183,15 @@ public class PaymentResource extends JaxRsResourceBase {
         }
 
         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();
+        return buildStreamingPaginationResponse(payments,
+                                                new Function<Payment, PaymentJson>() {
+                                                    @Override
+                                                    public PaymentJson apply(final Payment payment) {
+                                                        return new PaymentJson(payment, null);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @PUT
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
index c8d4c05..7790be3 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/RefundResource.java
@@ -16,13 +16,20 @@
 
 package com.ning.billing.jaxrs.resources;
 
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.UUID;
+import java.util.concurrent.atomic.AtomicReference;
 
 import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
@@ -35,10 +42,18 @@ import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
 import com.ning.billing.payment.api.PaymentApi;
 import com.ning.billing.payment.api.PaymentApiException;
 import com.ning.billing.payment.api.Refund;
+import com.ning.billing.util.api.AuditLevel;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AccountAuditLogs;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
 
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
@@ -65,10 +80,112 @@ public class RefundResource extends JaxRsResourceBase {
     @Path("/{refundId:" + UUID_PATTERN + "}")
     @Produces(APPLICATION_JSON)
     public Response getRefund(@PathParam("refundId") final String refundId,
+                              @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
                               @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
-        final Refund refund = paymentApi.getRefund(UUID.fromString(refundId), false, context.createContext(request));
-        // TODO Return adjusted items and audits
-        return Response.status(Status.OK).entity(new RefundJson(refund, null, null)).build();
+        final TenantContext tenantContext = context.createContext(request);
+        final Refund refund = paymentApi.getRefund(UUID.fromString(refundId), false, tenantContext);
+        final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(refund.getId(), ObjectType.REFUND, auditMode.getLevel(), tenantContext);
+        // TODO Return adjusted items
+        return Response.status(Status.OK).entity(new RefundJson(refund, null, auditLogs)).build();
+    }
+
+    @GET
+    @Path("/" + PAGINATION)
+    @Produces(APPLICATION_JSON)
+    public Response getRefunds(@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,
+                               @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                               @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        final Pagination<Refund> refunds;
+        if (Strings.isNullOrEmpty(pluginName)) {
+            refunds = paymentApi.getRefunds(offset, limit, tenantContext);
+        } else {
+            refunds = paymentApi.getRefunds(offset, limit, pluginName, tenantContext);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(RefundResource.class, "getRefunds", refunds.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+                                                                                                                                                        QUERY_AUDIT, auditMode.toString()));
+
+        final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+        final Map<UUID, UUID> paymentIdAccountIdMappings = new HashMap<UUID, UUID>();
+        return buildStreamingPaginationResponse(refunds,
+                                                new Function<Refund, RefundJson>() {
+                                                    @Override
+                                                    public RefundJson apply(final Refund refund) {
+                                                        UUID kbAccountId = null;
+                                                        if (!AuditLevel.NONE.equals(auditMode.getLevel()) && paymentIdAccountIdMappings.get(refund.getPaymentId()) == null) {
+                                                            try {
+                                                                kbAccountId = paymentApi.getPayment(refund.getPaymentId(), false, tenantContext).getAccountId();
+                                                                paymentIdAccountIdMappings.put(refund.getPaymentId(), kbAccountId);
+                                                            } catch (final PaymentApiException e) {
+                                                                log.warn("Unable to retrieve payment for id " + refund.getPaymentId());
+                                                            }
+                                                        }
+
+                                                        // Cache audit logs per account
+                                                        if (accountsAuditLogs.get().get(kbAccountId) == null) {
+                                                            accountsAuditLogs.get().put(kbAccountId, auditUserApi.getAccountAuditLogs(kbAccountId, auditMode.getLevel(), tenantContext));
+                                                        }
+
+                                                        final List<AuditLog> auditLogs = accountsAuditLogs.get().get(kbAccountId) == null ? null : accountsAuditLogs.get().get(kbAccountId).getAuditLogsForRefund(refund.getId());
+                                                        return new RefundJson(refund, null, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
+    }
+
+    @GET
+    @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
+    @Produces(APPLICATION_JSON)
+    public Response searchRefunds(@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,
+                                  @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                                  @javax.ws.rs.core.Context final HttpServletRequest request) throws PaymentApiException {
+        final TenantContext tenantContext = context.createContext(request);
+
+        // Search the plugin(s)
+        final Pagination<Refund> refunds;
+        if (Strings.isNullOrEmpty(pluginName)) {
+            refunds = paymentApi.searchRefunds(searchKey, offset, limit, tenantContext);
+        } else {
+            refunds = paymentApi.searchRefunds(searchKey, offset, limit, pluginName, tenantContext);
+        }
+
+        final URI nextPageUri = uriBuilder.nextPage(RefundResource.class, "searchRefunds", refunds.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+                                                                                                                                                           QUERY_PAYMENT_METHOD_PLUGIN_NAME, Strings.nullToEmpty(pluginName),
+                                                                                                                                                           QUERY_AUDIT, auditMode.toString()));
+
+        final AtomicReference<Map<UUID, AccountAuditLogs>> accountsAuditLogs = new AtomicReference<Map<UUID, AccountAuditLogs>>(new HashMap<UUID, AccountAuditLogs>());
+        final Map<UUID, UUID> paymentIdAccountIdMappings = new HashMap<UUID, UUID>();
+        return buildStreamingPaginationResponse(refunds,
+                                                new Function<Refund, RefundJson>() {
+                                                    @Override
+                                                    public RefundJson apply(final Refund refund) {
+                                                        UUID kbAccountId = null;
+                                                        if (!AuditLevel.NONE.equals(auditMode.getLevel()) && paymentIdAccountIdMappings.get(refund.getPaymentId()) == null) {
+                                                            try {
+                                                                kbAccountId = paymentApi.getPayment(refund.getPaymentId(), false, tenantContext).getAccountId();
+                                                                paymentIdAccountIdMappings.put(refund.getPaymentId(), kbAccountId);
+                                                            } catch (final PaymentApiException e) {
+                                                                log.warn("Unable to retrieve payment for id " + refund.getPaymentId());
+                                                            }
+                                                        }
+
+                                                        // Cache audit logs per account
+                                                        if (accountsAuditLogs.get().get(kbAccountId) == null) {
+                                                            accountsAuditLogs.get().put(kbAccountId, auditUserApi.getAccountAuditLogs(kbAccountId, auditMode.getLevel(), tenantContext));
+                                                        }
+
+                                                        final List<AuditLog> auditLogs = accountsAuditLogs.get().get(kbAccountId) == null ? null : accountsAuditLogs.get().get(kbAccountId).getAuditLogsForRefund(refund.getId());
+                                                        return new RefundJson(refund, null, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @Override
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagDefinitionResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagDefinitionResource.java
new file mode 100644
index 0000000..bc7202c
--- /dev/null
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagDefinitionResource.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2010-2013 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.jaxrs.resources;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.UUID;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.HeaderParam;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+
+import com.ning.billing.ObjectType;
+import com.ning.billing.account.api.AccountUserApi;
+import com.ning.billing.clock.Clock;
+import com.ning.billing.jaxrs.json.TagDefinitionJson;
+import com.ning.billing.jaxrs.util.Context;
+import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
+import com.ning.billing.util.api.AuditUserApi;
+import com.ning.billing.util.api.CustomFieldUserApi;
+import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.tag.TagDefinition;
+
+import com.google.common.base.Preconditions;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
+
+@Singleton
+@Path(JaxrsResource.TAG_DEFINITIONS_PATH)
+public class TagDefinitionResource extends JaxRsResourceBase {
+
+    @Inject
+    public TagDefinitionResource(final JaxrsUriBuilder uriBuilder,
+                                 final TagUserApi tagUserApi,
+                                 final CustomFieldUserApi customFieldUserApi,
+                                 final AuditUserApi auditUserApi,
+                                 final AccountUserApi accountUserApi,
+                                 final Clock clock,
+                                 final Context context) {
+        super(uriBuilder, tagUserApi, customFieldUserApi, auditUserApi, accountUserApi, clock, context);
+    }
+
+    @GET
+    @Produces(APPLICATION_JSON)
+    public Response getTagDefinitions(@javax.ws.rs.core.Context final HttpServletRequest request) {
+        final List<TagDefinition> tagDefinitions = tagUserApi.getTagDefinitions(context.createContext(request));
+
+        final List<TagDefinitionJson> result = new LinkedList<TagDefinitionJson>();
+        for (final TagDefinition cur : tagDefinitions) {
+            result.add(new TagDefinitionJson(cur));
+        }
+
+        return Response.status(Status.OK).entity(result).build();
+    }
+
+    @GET
+    @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
+    @Produces(APPLICATION_JSON)
+    public Response getTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
+                                     @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
+        final TagDefinition tagDef = tagUserApi.getTagDefinition(UUID.fromString(tagDefId), context.createContext(request));
+        final TagDefinitionJson json = new TagDefinitionJson(tagDef);
+        return Response.status(Status.OK).entity(json).build();
+    }
+
+    @POST
+    @Consumes(APPLICATION_JSON)
+    @Produces(APPLICATION_JSON)
+    public Response createTagDefinition(final TagDefinitionJson json,
+                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                        @HeaderParam(HDR_REASON) final String reason,
+                                        @HeaderParam(HDR_COMMENT) final String comment,
+                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
+        // Checked as the database layer as well, but bail early and return 400 instead of 500
+        Preconditions.checkNotNull(json.getName(), String.format("TagDefinition name needs to be set"));
+        Preconditions.checkNotNull(json.getDescription(), String.format("TagDefinition description needs to be set"));
+
+        final TagDefinition createdTagDef = tagUserApi.createTagDefinition(json.getName(), json.getDescription(), context.createContext(createdBy, reason, comment, request));
+        return uriBuilder.buildResponse(TagDefinitionResource.class, "getTagDefinition", createdTagDef.getId());
+    }
+
+    @DELETE
+    @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
+    @Produces(APPLICATION_JSON)
+    public Response deleteTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
+                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
+                                        @HeaderParam(HDR_REASON) final String reason,
+                                        @HeaderParam(HDR_COMMENT) final String comment,
+                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
+        tagUserApi.deleteTagDefinition(UUID.fromString(tagDefId), context.createContext(createdBy, reason, comment, request));
+        return Response.status(Status.NO_CONTENT).build();
+    }
+
+    @Override
+    protected ObjectType getObjectType() {
+        return ObjectType.TAG_DEFINITION;
+    }
+}
diff --git a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagResource.java b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagResource.java
index 1bbff05..0734a08 100644
--- a/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagResource.java
+++ b/jaxrs/src/main/java/com/ning/billing/jaxrs/resources/TagResource.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2010-2014 Ning, Inc.
  *
  * Ning licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -16,42 +16,46 @@
 
 package com.ning.billing.jaxrs.resources;
 
-import java.util.LinkedList;
+import java.net.URI;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.UUID;
 
 import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.DELETE;
+import javax.ws.rs.DefaultValue;
 import javax.ws.rs.GET;
-import javax.ws.rs.HeaderParam;
-import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
 
 import com.ning.billing.ObjectType;
 import com.ning.billing.account.api.AccountUserApi;
 import com.ning.billing.clock.Clock;
-import com.ning.billing.jaxrs.json.TagDefinitionJson;
+import com.ning.billing.jaxrs.json.TagJson;
 import com.ning.billing.jaxrs.util.Context;
 import com.ning.billing.jaxrs.util.JaxrsUriBuilder;
 import com.ning.billing.util.api.AuditUserApi;
 import com.ning.billing.util.api.CustomFieldUserApi;
-import com.ning.billing.util.api.TagDefinitionApiException;
+import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.api.TagUserApi;
+import com.ning.billing.util.audit.AuditLog;
+import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.tag.Tag;
 import com.ning.billing.util.tag.TagDefinition;
 
-import com.google.common.base.Preconditions;
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableMap;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import static javax.ws.rs.core.MediaType.APPLICATION_JSON;
 
 @Singleton
-@Path(JaxrsResource.TAG_DEFINITIONS_PATH)
+@Path(JaxrsResource.TAGS_PATH)
 public class TagResource extends JaxRsResourceBase {
 
     @Inject
@@ -66,58 +70,62 @@ public class TagResource extends JaxRsResourceBase {
     }
 
     @GET
+    @Path("/" + PAGINATION)
     @Produces(APPLICATION_JSON)
-    public Response getTagDefinitions(@javax.ws.rs.core.Context final HttpServletRequest request) {
-        final List<TagDefinition> tagDefinitions = tagUserApi.getTagDefinitions(context.createContext(request));
-
-        final List<TagDefinitionJson> result = new LinkedList<TagDefinitionJson>();
-        for (final TagDefinition cur : tagDefinitions) {
-            result.add(new TagDefinitionJson(cur));
+    public Response getTags(@QueryParam(QUERY_SEARCH_OFFSET) @DefaultValue("0") final Long offset,
+                            @QueryParam(QUERY_SEARCH_LIMIT) @DefaultValue("100") final Long limit,
+                            @QueryParam(QUERY_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                            @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<Tag> tags = tagUserApi.getTags(offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(TagResource.class, "getTags", tags.getNextOffset(), limit, ImmutableMap.<String, String>of(QUERY_AUDIT, auditMode.toString()));
+
+        final Map<UUID, TagDefinition> tagDefinitionsCache = new HashMap<UUID, TagDefinition>();
+        for (final TagDefinition tagDefinition : tagUserApi.getTagDefinitions(tenantContext)) {
+            tagDefinitionsCache.put(tagDefinition.getId(), tagDefinition);
         }
 
-        return Response.status(Status.OK).entity(result).build();
+        return buildStreamingPaginationResponse(tags,
+                                                new Function<Tag, TagJson>() {
+                                                    @Override
+                                                    public TagJson apply(final Tag tag) {
+                                                        final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
+
+                                                        // TODO Really slow - we should instead try to figure out the account id
+                                                        final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tag.getId(), ObjectType.TAG, auditMode.getLevel(), tenantContext);
+                                                        return new TagJson(tag, tagDefinition, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 
     @GET
-    @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
-    @Produces(APPLICATION_JSON)
-    public Response getTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
-                                     @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
-        final TagDefinition tagDef = tagUserApi.getTagDefinition(UUID.fromString(tagDefId), context.createContext(request));
-        final TagDefinitionJson json = new TagDefinitionJson(tagDef);
-        return Response.status(Status.OK).entity(json).build();
-    }
-
-    @POST
-    @Consumes(APPLICATION_JSON)
+    @Path("/" + SEARCH + "/{searchKey:" + ANYTHING_PATTERN + "}")
     @Produces(APPLICATION_JSON)
-    public Response createTagDefinition(final TagDefinitionJson json,
-                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
-                                        @HeaderParam(HDR_REASON) final String reason,
-                                        @HeaderParam(HDR_COMMENT) final String comment,
-                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
-        // Checked as the database layer as well, but bail early and return 400 instead of 500
-        Preconditions.checkNotNull(json.getName(), String.format("TagDefinition name needs to be set"));
-        Preconditions.checkNotNull(json.getDescription(), String.format("TagDefinition description needs to be set"));
-
-        final TagDefinition createdTagDef = tagUserApi.createTagDefinition(json.getName(), json.getDescription(), context.createContext(createdBy, reason, comment, request));
-        return uriBuilder.buildResponse(TagResource.class, "getTagDefinition", createdTagDef.getId());
-    }
-
-    @DELETE
-    @Path("/{tagDefinitionId:" + UUID_PATTERN + "}")
-    @Produces(APPLICATION_JSON)
-    public Response deleteTagDefinition(@PathParam("tagDefinitionId") final String tagDefId,
-                                        @HeaderParam(HDR_CREATED_BY) final String createdBy,
-                                        @HeaderParam(HDR_REASON) final String reason,
-                                        @HeaderParam(HDR_COMMENT) final String comment,
-                                        @javax.ws.rs.core.Context final HttpServletRequest request) throws TagDefinitionApiException {
-        tagUserApi.deleteTagDefinition(UUID.fromString(tagDefId), context.createContext(createdBy, reason, comment, request));
-        return Response.status(Status.NO_CONTENT).build();
-    }
-
-    @Override
-    protected ObjectType getObjectType() {
-        return ObjectType.TAG_DEFINITION;
+    public Response searchTags(@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_AUDIT) @DefaultValue("NONE") final AuditMode auditMode,
+                               @javax.ws.rs.core.Context final HttpServletRequest request) throws TagApiException {
+        final TenantContext tenantContext = context.createContext(request);
+        final Pagination<Tag> tags = tagUserApi.searchTags(searchKey, offset, limit, tenantContext);
+        final URI nextPageUri = uriBuilder.nextPage(TagResource.class, "searchTags", tags.getNextOffset(), limit, ImmutableMap.<String, String>of("searchKey", searchKey,
+                                                                                                                                                  QUERY_AUDIT, auditMode.toString()));
+        final Map<UUID, TagDefinition> tagDefinitionsCache = new HashMap<UUID, TagDefinition>();
+        for (final TagDefinition tagDefinition : tagUserApi.getTagDefinitions(tenantContext)) {
+            tagDefinitionsCache.put(tagDefinition.getId(), tagDefinition);
+        }
+        return buildStreamingPaginationResponse(tags,
+                                                new Function<Tag, TagJson>() {
+                                                    @Override
+                                                    public TagJson apply(final Tag tag) {
+                                                        final TagDefinition tagDefinition = tagDefinitionsCache.get(tag.getTagDefinitionId());
+
+                                                        // TODO Really slow - we should instead try to figure out the account id
+                                                        final List<AuditLog> auditLogs = auditUserApi.getAuditLogs(tag.getId(), ObjectType.TAG, auditMode.getLevel(), tenantContext);
+                                                        return new TagJson(tag, tagDefinition, auditLogs);
+                                                    }
+                                                },
+                                                nextPageUri);
     }
 }
diff --git a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
index 6377b2d..a27b34e 100644
--- a/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
+++ b/osgi-bundles/bundles/jruby/src/main/java/com/ning/billing/osgi/bundles/jruby/JRubyPaymentPlugin.java
@@ -122,6 +122,16 @@ public class JRubyPaymentPlugin extends JRubyPlugin implements PaymentPluginApi 
     }
 
     @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
+            @Override
+            public Pagination<RefundInfoPlugin> doCall(final Ruby runtime) throws PaymentPluginApiException {
+                return ((PaymentPluginApi) pluginInstance).searchRefunds(searchKey, offset, limit, tenantContext);
+            }
+        });
+    }
+
+    @Override
     public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
 
         callWithRuntimeAndChecking(new PluginCallback(VALIDATION_PLUGIN_TYPE.PAYMENT) {
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 3396d2c..97552f8 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
@@ -149,6 +149,36 @@ public class TestPaymentPluginApi implements PaymentPluginApi {
     }
 
     @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new Pagination<RefundInfoPlugin>() {
+            @Override
+            public Long getCurrentOffset() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNextOffset() {
+                return null;
+            }
+
+            @Override
+            public Long getMaxNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Long getTotalNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Iterator<RefundInfoPlugin> iterator() {
+                return null;
+            }
+        };
+    }
+
+    @Override
     public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
     }
 
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 854aa07..fe376d1 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
@@ -193,10 +193,13 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
 
     @Override
     public RefundInfoPlugin processRefund(final UUID accountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
-
-        final BigDecimal someAmount = new BigDecimal("12.45");
         return withRuntimeCheckForExceptions(new RefundInfoPlugin() {
             @Override
+            public UUID getKbPaymentId() {
+                return kbPaymentId;
+            }
+
+            @Override
             public BigDecimal getAmount() {
                 return null;
             }
@@ -232,7 +235,12 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
             }
 
             @Override
-            public String getReferenceId() {
+            public String getFirstRefundReferenceId() {
+                return null;
+            }
+
+            @Override
+            public String getSecondRefundReferenceId() {
                 return null;
             }
         });
@@ -244,6 +252,36 @@ public class TestPaymentPluginApi implements PaymentPluginApiWithTestControl {
     }
 
     @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new Pagination<RefundInfoPlugin>() {
+            @Override
+            public Long getCurrentOffset() {
+                return 0L;
+            }
+
+            @Override
+            public Long getNextOffset() {
+                return null;
+            }
+
+            @Override
+            public Long getMaxNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Long getTotalNbRecords() {
+                return 0L;
+            }
+
+            @Override
+            public Iterator<RefundInfoPlugin> iterator() {
+                return null;
+            }
+        };
+    }
+
+    @Override
     public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
     }
 
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 e1777d2..3f00e66 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
@@ -116,6 +116,26 @@ public class DefaultPaymentApi implements PaymentApi {
     }
 
     @Override
+    public Pagination<Refund> getRefunds(final Long offset, final Long limit, final TenantContext context) {
+        return refundProcessor.getRefunds(offset, limit, context, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<Refund> getRefunds(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext) throws PaymentApiException {
+        return refundProcessor.getRefunds(offset, limit, pluginName, tenantContext, internalCallContextFactory.createInternalTenantContext(tenantContext));
+    }
+
+    @Override
+    public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+        return refundProcessor.searchRefunds(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
+    public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final String pluginName, final TenantContext context) throws PaymentApiException {
+        return refundProcessor.searchRefunds(searchKey, offset, limit, pluginName, internalCallContextFactory.createInternalTenantContext(context));
+    }
+
+    @Override
     public List<Payment> getInvoicePayments(final UUID invoiceId, final TenantContext context) {
         return paymentProcessor.getInvoicePayments(invoiceId, internalCallContextFactory.createInternalTenantContext(context));
     }
diff --git a/payment/src/main/java/com/ning/billing/payment/api/DefaultRefund.java b/payment/src/main/java/com/ning/billing/payment/api/DefaultRefund.java
index 9349504..b708a01 100644
--- a/payment/src/main/java/com/ning/billing/payment/api/DefaultRefund.java
+++ b/payment/src/main/java/com/ning/billing/payment/api/DefaultRefund.java
@@ -25,6 +25,7 @@ import org.joda.time.DateTime;
 
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.entity.EntityBase;
+import com.ning.billing.payment.dao.RefundModelDao;
 import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
 
 public class DefaultRefund extends EntityBase implements Refund {
@@ -35,10 +36,12 @@ public class DefaultRefund extends EntityBase implements Refund {
     private final boolean isAdjusted;
     private final DateTime effectiveDate;
     private final RefundStatus refundStatus;
+    private final RefundInfoPlugin refundInfoPlugin;
 
     public DefaultRefund(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
                          final UUID paymentId, final BigDecimal amount,
-                         final Currency currency, final boolean isAdjusted, final DateTime effectiveDate, final RefundStatus refundStatus) {
+                         final Currency currency, final boolean isAdjusted, final DateTime effectiveDate,
+                         final RefundStatus refundStatus, final RefundInfoPlugin refundInfoPlugin) {
         super(id, createdDate, updatedDate);
         this.paymentId = paymentId;
         this.amount = amount;
@@ -46,6 +49,19 @@ public class DefaultRefund extends EntityBase implements Refund {
         this.isAdjusted = isAdjusted;
         this.effectiveDate = effectiveDate;
         this.refundStatus = refundStatus;
+        this.refundInfoPlugin = refundInfoPlugin;
+    }
+
+    public DefaultRefund(final RefundModelDao refundModelDao, @Nullable final RefundInfoPlugin refundInfoPlugin) {
+        this(refundModelDao.getId(), refundModelDao.getCreatedDate(), refundModelDao.getUpdatedDate(),
+             refundModelDao.getPaymentId(), refundModelDao.getAmount(), refundModelDao.getCurrency(),
+             refundModelDao.isAdjusted(), refundModelDao.getCreatedDate(), refundModelDao.getRefundStatus(), refundInfoPlugin);
+    }
+
+    public DefaultRefund(final UUID id, @Nullable final DateTime createdDate, @Nullable final DateTime updatedDate,
+                         final UUID paymentId, final BigDecimal amount,
+                         final Currency currency, final boolean isAdjusted, final DateTime effectiveDate, final RefundStatus refundStatus) {
+        this(id, createdDate, updatedDate, paymentId, amount, currency, isAdjusted, effectiveDate, refundStatus, null);
     }
 
     @Override
@@ -79,21 +95,20 @@ public class DefaultRefund extends EntityBase implements Refund {
     }
 
     @Override
-    public RefundInfoPlugin getPluginDetail() {
-        // TODO not implemented
-        return null;
+    public RefundInfoPlugin getRefundInfoPlugin() {
+        return refundInfoPlugin;
     }
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("DefaultRefund");
-        sb.append("{paymentId=").append(paymentId);
+        final StringBuilder sb = new StringBuilder("DefaultRefund{");
+        sb.append("paymentId=").append(paymentId);
         sb.append(", amount=").append(amount);
         sb.append(", currency=").append(currency);
         sb.append(", isAdjusted=").append(isAdjusted);
-        sb.append(", status=").append(refundStatus);
         sb.append(", effectiveDate=").append(effectiveDate);
+        sb.append(", refundStatus=").append(refundStatus);
+        sb.append(", refundInfoPlugin=").append(refundInfoPlugin);
         sb.append('}');
         return sb.toString();
     }
@@ -106,39 +121,47 @@ public class DefaultRefund extends EntityBase implements Refund {
         if (o == null || getClass() != o.getClass()) {
             return false;
         }
+        if (!super.equals(o)) {
+            return false;
+        }
 
         final DefaultRefund that = (DefaultRefund) o;
 
         if (isAdjusted != that.isAdjusted) {
             return false;
         }
-        if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
-            return false;
-        }
-        if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+        if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
             return false;
         }
         if (currency != that.currency) {
             return false;
         }
-        if (refundStatus != that.refundStatus) {
+        if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
             return false;
         }
         if (paymentId != null ? !paymentId.equals(that.paymentId) : that.paymentId != null) {
             return false;
         }
+        if (refundInfoPlugin != null ? !refundInfoPlugin.equals(that.refundInfoPlugin) : that.refundInfoPlugin != null) {
+            return false;
+        }
+        if (refundStatus != that.refundStatus) {
+            return false;
+        }
 
         return true;
     }
 
     @Override
     public int hashCode() {
-        int result = paymentId != null ? paymentId.hashCode() : 0;
+        int result = super.hashCode();
+        result = 31 * result + (paymentId != null ? paymentId.hashCode() : 0);
         result = 31 * result + (amount != null ? amount.hashCode() : 0);
         result = 31 * result + (currency != null ? currency.hashCode() : 0);
-        result = 31 * result + (refundStatus != null ? refundStatus.hashCode() : 0);
         result = 31 * result + (isAdjusted ? 1 : 0);
         result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
+        result = 31 * result + (refundStatus != null ? refundStatus.hashCode() : 0);
+        result = 31 * result + (refundInfoPlugin != null ? refundInfoPlugin.hashCode() : 0);
         return result;
     }
 }
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 4117e03..0e65f04 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
@@ -18,7 +18,6 @@ package com.ning.billing.payment.core;
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
@@ -55,18 +54,19 @@ 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;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 
 import com.google.common.base.Function;
-import com.google.common.base.Predicates;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
 import com.google.inject.Inject;
 import com.google.inject.name.Named;
 
 import static com.ning.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins;
 
 public class PaymentMethodProcessor extends ProcessorBase {
 
@@ -156,134 +156,89 @@ public class PaymentMethodProcessor extends ProcessorBase {
     }
 
     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());
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return getPaymentMethods(offset, limit, pluginName, tenantContext, internalTenantContext);
+                                                  }
+                                              });
     }
 
     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) {
-                                                                                                                                                      PaymentMethodPlugin paymentMethodPlugin = null;
-                                                                                                                                                      try {
-                                                                                                                                                          paymentMethodPlugin = pluginApi.getPaymentMethodDetail(paymentMethodModelDao.getAccountId(), paymentMethodModelDao.getId(), tenantContext);
-                                                                                                                                                          if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
-                                                                                                                                                              // Garbage from the plugin?
-                                                                                                                                                              log.debug("Plugin {} returned a payment method without a kbPaymentMethodId", pluginName);
-                                                                                                                                                              paymentMethodPlugin = null;
-                                                                                                                                                          }
-                                                                                                                                                      } catch (PaymentPluginApiException e) {
-                                                                                                                                                          log.warn("Unable to find payment method id " + paymentMethodModelDao.getId() + " in plugin " + pluginName);
-                                                                                                                                                      }
-
-                                                                                                                                                      return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
-                                                                                                                                                  }
-                                                                                                                                              }),
-                                                                                    Predicates.<PaymentMethod>notNull()));
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentMethodModelDao, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentMethodModelDao> build() {
+                                           // Find all payment methods for all accounts
+                                           return paymentDao.getPaymentMethods(pluginName, offset, limit, internalTenantContext);
+                                       }
+                                   },
+                                   new Function<PaymentMethodModelDao, PaymentMethod>() {
+                                       @Override
+                                       public PaymentMethod apply(final PaymentMethodModelDao paymentMethodModelDao) {
+                                           PaymentMethodPlugin paymentMethodPlugin = null;
+                                           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 new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+                                       }
+                                   }
+                                  );
     }
 
     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;
-                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 new DefaultPagination<PaymentMethod>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<PaymentMethod, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<PaymentMethod> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return searchPaymentMethods(searchKey, offset, limit, pluginName, internalTenantContext);
+                                                  }
+                                              });
     }
 
     public Pagination<PaymentMethod> searchPaymentMethods(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-        final Pagination<PaymentMethodPlugin> paymentMethods;
-        try {
-            paymentMethods = pluginApi.searchPaymentMethods(searchKey, offset, limit, buildTenantContext(internalTenantContext));
-        } catch (PaymentPluginApiException e) {
-            throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
-        }
 
-        return new DefaultPagination<PaymentMethod>(paymentMethods,
-                                                    limit,
-                                                    Iterators.<PaymentMethod>filter(Iterators.<PaymentMethodPlugin, PaymentMethod>transform(paymentMethods.iterator(),
-                                                                                                                                            new Function<PaymentMethodPlugin, PaymentMethod>() {
-                                                                                                                                                @Override
-                                                                                                                                                public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
-                                                                                                                                                    if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
-                                                                                                                                                        // Garbage from the plugin?
-                                                                                                                                                        log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
-                                                                                                                                                        return null;
-                                                                                                                                                    }
-
-                                                                                                                                                    final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
-                                                                                                                                                    if (paymentMethodModelDao == null) {
-                                                                                                                                                        log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
-                                                                                                                                                        return null;
-                                                                                                                                                    }
-
-                                                                                                                                                    return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
-                                                                                                                                                }
-                                                                                                                                            }),
-                                                                                    Predicates.<PaymentMethod>notNull()));
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentMethodPlugin, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentMethodPlugin> build() throws PaymentApiException {
+                                           try {
+                                               return pluginApi.searchPaymentMethods(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+                                           } catch (final PaymentPluginApiException e) {
+                                               throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENT_METHODS, pluginName, searchKey);
+                                           }
+                                       }
+                                   },
+                                   new Function<PaymentMethodPlugin, PaymentMethod>() {
+                                       @Override
+                                       public PaymentMethod apply(final PaymentMethodPlugin paymentMethodPlugin) {
+                                           if (paymentMethodPlugin.getKbPaymentMethodId() == null) {
+                                               // Garbage from the plugin?
+                                               log.debug("Plugin {} returned a payment method without a kbPaymentMethodId for searchKey {}", pluginName, searchKey);
+                                               return null;
+                                           }
+
+                                           final PaymentMethodModelDao paymentMethodModelDao = paymentDao.getPaymentMethodIncludedDeleted(paymentMethodPlugin.getKbPaymentMethodId(), internalTenantContext);
+                                           if (paymentMethodModelDao == null) {
+                                               log.warn("Unable to find payment method id " + paymentMethodPlugin.getKbPaymentMethodId() + " present in plugin " + pluginName);
+                                               return null;
+                                           }
+
+                                           return new DefaultPaymentMethod(paymentMethodModelDao, paymentMethodPlugin);
+                                       }
+                                   }
+                                  );
     }
 
     public PaymentMethod getExternalPaymentMethod(final Account account, final InternalTenantContext context) throws PaymentApiException {
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 567a8ca..eba9931 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
@@ -72,18 +72,18 @@ 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;
 import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 
 import com.google.common.base.Function;
 import com.google.common.base.Predicate;
-import com.google.common.base.Predicates;
 import com.google.common.collect.Collections2;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
 import com.google.inject.name.Named;
 
 import static com.ning.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins;
 
 public class PaymentProcessor extends ProcessorBase {
 
@@ -149,134 +149,89 @@ public class PaymentProcessor extends ProcessorBase {
     }
 
     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());
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<Payment, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<Payment> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return getPayments(offset, limit, pluginName, tenantContext, internalTenantContext);
+                                                  }
+                                              });
     }
 
     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) {
-                                                                                                                              PaymentInfoPlugin pluginInfo = null;
-                                                                                                                              try {
-                                                                                                                                  pluginInfo = pluginApi.getPaymentInfo(paymentModelDao.getAccountId(), paymentModelDao.getId(), tenantContext);
-                                                                                                                                  if (pluginInfo.getKbPaymentId() == null) {
-                                                                                                                                      // Garbage from the plugin?
-                                                                                                                                      log.debug("Plugin {} returned a payment without a kbPaymentId", pluginName);
-                                                                                                                                      pluginInfo = null;
-                                                                                                                                  }
-                                                                                                                              } catch (final PaymentPluginApiException e) {
-                                                                                                                                  log.warn("Unable to find payment  id " + paymentModelDao.getId() + " in plugin " + pluginName);
-                                                                                                                              }
-
-                                                                                                                              return fromPaymentModelDao(paymentModelDao, pluginInfo, internalTenantContext);
-                                                                                                                          }
-                                                                                                                      }),
-                                                                        Predicates.<Payment>notNull()));
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentModelDao, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentModelDao> build() {
+                                           // Find all payments for all accounts
+                                           return paymentDao.getPayments(pluginName, offset, limit, internalTenantContext);
+                                       }
+                                   },
+                                   new Function<PaymentModelDao, Payment>() {
+                                       @Override
+                                       public Payment apply(final PaymentModelDao paymentModelDao) {
+                                           PaymentInfoPlugin pluginInfo = null;
+                                           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 fromPaymentModelDao(paymentModelDao, pluginInfo, internalTenantContext);
+                                       }
+                                   }
+                                  );
     }
 
     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
-        // 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 = searchPayments(searchKey, firstSearch ? offset : 0L, 1L, pluginName, internalTenantContext);
-                    // Required to close database connections
-                    ImmutableList.<Payment>copyOf(payments);
-                } else {
-                    payments = searchPayments(searchKey, firstSearch ? offset : 0L, limit - allResults.size(), pluginName, 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());
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<Payment, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<Payment> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return searchPayments(searchKey, offset, limit, pluginName, internalTenantContext);
+                                                  }
+                                              });
     }
 
     public Pagination<Payment> searchPayments(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
         final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
-        final Pagination<PaymentInfoPlugin> payments;
-        try {
-            payments = pluginApi.searchPayments(searchKey, offset, limit, buildTenantContext(internalTenantContext));
-        } catch (final PaymentPluginApiException e) {
-            throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENTS, pluginName, searchKey);
-        }
 
-        return new DefaultPagination<Payment>(payments,
-                                              limit,
-                                              Iterators.<Payment>filter(Iterators.<PaymentInfoPlugin, Payment>transform(payments.iterator(),
-                                                                                                                        new Function<PaymentInfoPlugin, Payment>() {
-                                                                                                                            @Override
-                                                                                                                            public Payment apply(final PaymentInfoPlugin paymentInfoPlugin) {
-                                                                                                                                if (paymentInfoPlugin.getKbPaymentId() == null) {
-                                                                                                                                    // Garbage from the plugin?
-                                                                                                                                    log.debug("Plugin {} returned a payment without a kbPaymentId for searchKey {}", pluginName, searchKey);
-                                                                                                                                    return null;
-                                                                                                                                }
-
-                                                                                                                                final PaymentModelDao model = paymentDao.getPayment(paymentInfoPlugin.getKbPaymentId(), internalTenantContext);
-                                                                                                                                if (model == null) {
-                                                                                                                                    log.warn("Unable to find payment id " + paymentInfoPlugin.getKbPaymentId() + " present in plugin " + pluginName);
-                                                                                                                                    return null;
-                                                                                                                                }
-
-                                                                                                                                return fromPaymentModelDao(model, paymentInfoPlugin, internalTenantContext);
-                                                                                                                            }
-                                                                                                                        }),
-                                                                        Predicates.<Payment>notNull()));
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<PaymentInfoPlugin, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<PaymentInfoPlugin> build() throws PaymentApiException {
+                                           try {
+                                               return pluginApi.searchPayments(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+                                           } catch (final PaymentPluginApiException e) {
+                                               throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_PAYMENTS, pluginName, searchKey);
+                                           }
+                                       }
+                                   },
+                                   new Function<PaymentInfoPlugin, Payment>() {
+                                       @Override
+                                       public Payment apply(final PaymentInfoPlugin paymentInfoPlugin) {
+                                           if (paymentInfoPlugin.getKbPaymentId() == null) {
+                                               // Garbage from the plugin?
+                                               log.debug("Plugin {} returned a payment without a kbPaymentId for searchKey {}", pluginName, searchKey);
+                                               return null;
+                                           }
+
+                                           final PaymentModelDao model = paymentDao.getPayment(paymentInfoPlugin.getKbPaymentId(), internalTenantContext);
+                                           if (model == null) {
+                                               log.warn("Unable to find payment id " + paymentInfoPlugin.getKbPaymentId() + " present in plugin " + pluginName);
+                                               return null;
+                                           }
+
+                                           return fromPaymentModelDao(model, paymentInfoPlugin, internalTenantContext);
+                                       }
+                                   }
+                                  );
     }
 
     public List<Payment> getInvoicePayments(final UUID invoiceId, final InternalTenantContext context) {
diff --git a/payment/src/main/java/com/ning/billing/payment/core/ProcessorBase.java b/payment/src/main/java/com/ning/billing/payment/core/ProcessorBase.java
index a68b447..5a128d6 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/ProcessorBase.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/ProcessorBase.java
@@ -30,28 +30,28 @@ import org.slf4j.LoggerFactory;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.ObjectType;
 import com.ning.billing.account.api.Account;
+import com.ning.billing.account.api.AccountInternalApi;
 import com.ning.billing.bus.api.PersistentBus;
 import com.ning.billing.bus.api.PersistentBus.EventBusException;
+import com.ning.billing.callcontext.InternalCallContext;
+import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.commons.locker.GlobalLock;
 import com.ning.billing.commons.locker.GlobalLocker;
 import com.ning.billing.commons.locker.LockFailedException;
+import com.ning.billing.events.BusInternalEvent;
 import com.ning.billing.invoice.api.Invoice;
 import com.ning.billing.invoice.api.InvoiceApiException;
+import com.ning.billing.invoice.api.InvoiceInternalApi;
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
 import com.ning.billing.payment.api.PaymentApiException;
 import com.ning.billing.payment.dao.PaymentDao;
 import com.ning.billing.payment.dao.PaymentMethodModelDao;
 import com.ning.billing.payment.plugin.api.PaymentPluginApi;
+import com.ning.billing.tag.TagInternalApi;
 import com.ning.billing.util.api.TagApiException;
-import com.ning.billing.callcontext.InternalCallContext;
-import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.dao.NonEntityDao;
-import com.ning.billing.events.BusInternalEvent;
 import com.ning.billing.util.globallocker.LockerType;
-import com.ning.billing.account.api.AccountInternalApi;
-import com.ning.billing.invoice.api.InvoiceInternalApi;
-import com.ning.billing.tag.TagInternalApi;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.Tag;
 
diff --git a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
index b2a62b1..d5c0b27 100644
--- a/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
+++ b/payment/src/main/java/com/ning/billing/payment/core/RefundProcessor.java
@@ -20,11 +20,12 @@ import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
 import java.util.concurrent.ExecutorService;
-import java.util.concurrent.TimeoutException;
 
 import javax.annotation.Nullable;
 import javax.inject.Inject;
@@ -47,31 +48,37 @@ import com.ning.billing.invoice.api.InvoiceItem;
 import com.ning.billing.osgi.api.OSGIServiceRegistration;
 import com.ning.billing.payment.api.DefaultRefund;
 import com.ning.billing.payment.api.PaymentApiException;
-import com.ning.billing.payment.api.PaymentStatus;
 import com.ning.billing.payment.api.Refund;
 import com.ning.billing.payment.api.RefundStatus;
-import com.ning.billing.payment.dao.PaymentAttemptModelDao;
 import com.ning.billing.payment.dao.PaymentDao;
 import com.ning.billing.payment.dao.PaymentModelDao;
 import com.ning.billing.payment.dao.RefundModelDao;
 import com.ning.billing.payment.plugin.api.PaymentPluginApi;
 import com.ning.billing.payment.plugin.api.PaymentPluginApiException;
 import com.ning.billing.payment.plugin.api.RefundInfoPlugin;
+import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 import com.ning.billing.tag.TagInternalApi;
-import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.CallOrigin;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
+import com.ning.billing.util.callcontext.TenantContext;
 import com.ning.billing.util.callcontext.UserType;
 import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.EntityPaginationBuilder;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 
 import com.google.common.base.Function;
 import com.google.common.base.Objects;
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.inject.name.Named;
 
 import static com.ning.billing.payment.glue.PaymentModule.PLUGIN_EXECUTOR_NAMED;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPagination;
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationFromPlugins;
 
 public class RefundProcessor extends ProcessorBase {
 
@@ -151,7 +158,11 @@ public class RefundProcessor extends ProcessorBase {
                         default:
                             paymentDao.updateRefundStatus(refundInfo.getId(), RefundStatus.PLUGIN_ERRORED, refundAmount, account.getCurrency(), context);
                             throw new PaymentPluginApiException("Refund error for RefundInfo: " + refundInfo.toString(),
-                                                                String.format("Gateway error: %s, Gateway error code: %s, Reference id: %s", refundInfoPlugin.getGatewayError(), refundInfoPlugin.getGatewayErrorCode(), refundInfoPlugin.getReferenceId()));
+                                                                String.format("Gateway error: %s, Gateway error code: %s, Reference ids: %s / %s",
+                                                                              refundInfoPlugin.getGatewayError(),
+                                                                              refundInfoPlugin.getGatewayErrorCode(),
+                                                                              refundInfoPlugin.getFirstRefundReferenceId(),
+                                                                              refundInfoPlugin.getSecondRefundReferenceId()));
                     }
                 } catch (PaymentPluginApiException e) {
                     throw new PaymentApiException(ErrorCode.PAYMENT_CREATE_REFUND, account.getId(), e.getErrorMessage());
@@ -162,7 +173,6 @@ public class RefundProcessor extends ProcessorBase {
         });
     }
 
-
     public void notifyPendingRefundOfStateChanged(final Account account, final UUID refundId, final boolean isSuccess, final InternalCallContext context)
             throws PaymentApiException {
 
@@ -231,23 +241,184 @@ public class RefundProcessor extends ProcessorBase {
         throw new IllegalArgumentException("Unable to find invoice item for id " + itemId);
     }
 
-    public Refund getRefund(final UUID refundId, final boolean withPluginInfo /* not yet implemented */, final InternalTenantContext context)
-            throws PaymentApiException {
+    public Refund getRefund(final UUID refundId, final boolean withPluginInfo, final InternalTenantContext context) throws PaymentApiException {
         RefundModelDao result = paymentDao.getRefund(refundId, context);
         if (result == null) {
             throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
         }
+
         final List<RefundModelDao> filteredInput = filterUncompletedPluginRefund(Collections.singletonList(result));
-        if (filteredInput.size() == 0) {
+        if (filteredInput.isEmpty()) {
             throw new PaymentApiException(ErrorCode.PAYMENT_NO_SUCH_REFUND, refundId);
         }
 
         if (completePluginCompletedRefund(filteredInput, context)) {
             result = paymentDao.getRefund(refundId, context);
         }
-        return new DefaultRefund(result.getId(), result.getCreatedDate(), result.getUpdatedDate(),
-                                 result.getPaymentId(), result.getAmount(), result.getCurrency(),
-                                 result.isAdjusted(), result.getCreatedDate(), result.getRefundStatus());
+
+        final Account account;
+        try {
+            account = accountInternalApi.getAccountById(result.getAccountId(), context);
+        } catch (final AccountApiException e) {
+            throw new PaymentApiException(e);
+        }
+
+        final PaymentPluginApi plugin = withPluginInfo ? getPaymentProviderPlugin(account, context) : null;
+        List<RefundInfoPlugin> refundInfoPlugins = ImmutableList.<RefundInfoPlugin>of();
+        if (plugin != null) {
+            try {
+                refundInfoPlugins = plugin.getRefundInfo(account.getId(), result.getPaymentId(), buildTenantContext(context));
+            } catch (final PaymentPluginApiException e) {
+                throw new PaymentApiException(ErrorCode.PAYMENT_PLUGIN_GET_REFUND_INFO, refundId, e.toString());
+            }
+        }
+
+        return new DefaultRefund(result, findRefundInfoPlugin(result, refundInfoPlugins));
+    }
+
+    private RefundInfoPlugin findRefundInfoPlugin(final RefundModelDao refundModelDao, final List<RefundInfoPlugin> refundInfoPlugins) {
+        // We have a mapping 1:N for payment:refunds and a mapping 1:1 for RefundModelDao:RefundInfoPlugin.
+        // Unfortunately, we processing a refund, we don't tell the plugin about the refund id, so we need to do some heuristics
+        // to map a RefundInfoPlugin back to its RefundModelDao
+        // TODO This will break for multiple partial refunds of the same amount. Check the effective date won't help for same day partial refunds and checking effective datetime seems risky
+        return Iterables.<RefundInfoPlugin>tryFind(refundInfoPlugins,
+                                                   new Predicate<RefundInfoPlugin>() {
+                                                       @Override
+                                                       public boolean apply(final RefundInfoPlugin refundInfoPlugin) {
+                                                           return refundObjectsMatch(refundModelDao, refundInfoPlugin);
+                                                       }
+                                                   }).orNull();
+    }
+
+    private boolean refundObjectsMatch(final RefundModelDao refundModelDao, final RefundInfoPlugin refundInfoPlugin) {
+        return (refundInfoPlugin.getKbPaymentId() != null && refundModelDao.getPaymentId() != null && refundInfoPlugin.getKbPaymentId().equals(refundModelDao.getPaymentId())) &&
+               (refundInfoPlugin.getAmount() != null && refundModelDao.getProcessedAmount() != null && refundInfoPlugin.getAmount().compareTo(refundModelDao.getProcessedAmount()) == 0) &&
+               (refundInfoPlugin.getCurrency() != null && refundModelDao.getProcessedCurrency() != null && refundInfoPlugin.getCurrency().equals(refundModelDao.getProcessedCurrency())) &&
+               (
+                       (refundInfoPlugin.getStatus().equals(RefundPluginStatus.PROCESSED) && refundModelDao.getRefundStatus().equals(RefundStatus.COMPLETED)) ||
+                       (refundInfoPlugin.getStatus().equals(RefundPluginStatus.PENDING) && refundModelDao.getRefundStatus().equals(RefundStatus.PENDING))
+               );
+    }
+
+    public Pagination<Refund> getRefunds(final Long offset, final Long limit, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) {
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<Refund, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<Refund> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return getRefunds(offset, limit, pluginName, tenantContext, internalTenantContext);
+                                                  }
+                                              });
+    }
+
+    public Pagination<Refund> getRefunds(final Long offset, final Long limit, final String pluginName, final TenantContext tenantContext, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<RefundModelDao, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<RefundModelDao> build() {
+                                           // Find all refunds for all accounts
+                                           return paymentDao.getRefunds(pluginName, offset, limit, internalTenantContext);
+                                       }
+                                   },
+                                   new Function<RefundModelDao, Refund>() {
+                                       @Override
+                                       public Refund apply(final RefundModelDao refundModelDao) {
+                                           List<RefundInfoPlugin> refundInfoPlugins = null;
+                                           try {
+                                               refundInfoPlugins = pluginApi.getRefundInfo(refundModelDao.getAccountId(), refundModelDao.getId(), tenantContext);
+                                           } catch (final PaymentPluginApiException e) {
+                                               log.warn("Unable to find refund id " + refundModelDao.getId() + " in plugin " + pluginName);
+                                           }
+
+                                           final RefundInfoPlugin refundInfoPlugin = findRefundInfoPlugin(refundModelDao, refundInfoPlugins);
+                                           return new DefaultRefund(refundModelDao, refundInfoPlugin);
+                                       }
+                                   }
+                                  );
+    }
+
+    public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final InternalTenantContext internalTenantContext) {
+        return getEntityPaginationFromPlugins(getAvailablePlugins(),
+                                              offset,
+                                              limit,
+                                              new EntityPaginationBuilder<Refund, PaymentApiException>() {
+                                                  @Override
+                                                  public Pagination<Refund> build(final Long offset, final Long limit, final String pluginName) throws PaymentApiException {
+                                                      return searchRefunds(searchKey, offset, limit, pluginName, internalTenantContext);
+                                                  }
+                                              });
+    }
+
+    public Pagination<Refund> searchRefunds(final String searchKey, final Long offset, final Long limit, final String pluginName, final InternalTenantContext internalTenantContext) throws PaymentApiException {
+        final PaymentPluginApi pluginApi = getPaymentPluginApi(pluginName);
+
+        final Map<UUID, List<RefundInfoPlugin>> refundsByPaymentId = new HashMap<UUID, List<RefundInfoPlugin>>();
+        final Map<UUID, List<RefundModelDao>> refundModelDaosByPaymentId = new HashMap<UUID, List<RefundModelDao>>();
+
+        return getEntityPagination(limit,
+                                   new SourcePaginationBuilder<RefundInfoPlugin, PaymentApiException>() {
+                                       @Override
+                                       public Pagination<RefundInfoPlugin> build() throws PaymentApiException {
+                                           final Pagination<RefundInfoPlugin> refunds;
+                                           try {
+                                               refunds = pluginApi.searchRefunds(searchKey, offset, limit, buildTenantContext(internalTenantContext));
+                                           } catch (final PaymentPluginApiException e) {
+                                               throw new PaymentApiException(e, ErrorCode.PAYMENT_PLUGIN_SEARCH_REFUNDS, pluginName, searchKey);
+                                           }
+
+                                           // We need to group the refunds from the plugin by payment id. Since the ordering of the results is unspecified,
+                                           // we cannot do streaming here unfortunately
+                                           for (final RefundInfoPlugin refundInfoPlugin : refunds) {
+                                               if (refundInfoPlugin.getKbPaymentId() == null) {
+                                                   // Garbage from the plugin?
+                                                   log.debug("Plugin {} returned a refund without a kbPaymentId for searchKey {}", pluginName, searchKey);
+                                                   continue;
+                                               }
+
+                                               if (refundsByPaymentId.get(refundInfoPlugin.getKbPaymentId()) == null) {
+                                                   refundsByPaymentId.put(refundInfoPlugin.getKbPaymentId(), new LinkedList<RefundInfoPlugin>());
+                                               }
+                                               refundsByPaymentId.get(refundInfoPlugin.getKbPaymentId()).add(refundInfoPlugin);
+                                           }
+
+                                           return refunds;
+                                       }
+                                   },
+                                   new Function<RefundInfoPlugin, Refund>() {
+                                       @Override
+                                       public Refund apply(final RefundInfoPlugin refundInfoPlugin) {
+                                           if (refundInfoPlugin.getKbPaymentId() == null) {
+                                               // Garbage from the plugin?
+                                               log.debug("Plugin {} returned a refund without a kbPaymentId for searchKey {}", pluginName, searchKey);
+                                               return null;
+                                           }
+
+                                           List<RefundModelDao> modelCandidates = refundModelDaosByPaymentId.get(refundInfoPlugin.getKbPaymentId());
+                                           if (modelCandidates == null) {
+                                               refundModelDaosByPaymentId.put(refundInfoPlugin.getKbPaymentId(), paymentDao.getRefundsForPayment(refundInfoPlugin.getKbPaymentId(), internalTenantContext));
+                                               modelCandidates = refundModelDaosByPaymentId.get(refundInfoPlugin.getKbPaymentId());
+                                           }
+
+                                           final RefundModelDao model = Iterables.<RefundModelDao>tryFind(modelCandidates,
+                                                                                                          new Predicate<RefundModelDao>() {
+                                                                                                              @Override
+                                                                                                              public boolean apply(final RefundModelDao refundModelDao) {
+                                                                                                                  return refundObjectsMatch(refundModelDao, refundInfoPlugin);
+                                                                                                              }
+                                                                                                          }).orNull();
+
+                                           if (model == null) {
+                                               log.warn("Unable to find refund for payment id " + refundInfoPlugin.getKbPaymentId() + " present in plugin " + pluginName);
+                                               return null;
+                                           }
+
+                                           return new DefaultRefund(model, refundInfoPlugin);
+                                       }
+                                   }
+                                  );
     }
 
     public List<Refund> getAccountRefunds(final Account account, final InternalTenantContext context)
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 8d3cf23..0aedc0c 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
@@ -31,12 +31,16 @@ import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.catalog.api.Currency;
 import com.ning.billing.clock.Clock;
 import com.ning.billing.entity.EntityPersistenceException;
+import com.ning.billing.payment.api.Payment;
+import com.ning.billing.payment.api.PaymentMethod;
 import com.ning.billing.payment.api.PaymentStatus;
+import com.ning.billing.payment.api.Refund;
 import com.ning.billing.payment.api.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.DefaultPaginationSqlDaoHelper;
+import com.ning.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
@@ -48,10 +52,12 @@ import com.google.common.collect.Collections2;
 public class DefaultPaymentDao implements PaymentDao {
 
     private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+    private final DefaultPaginationSqlDaoHelper paginationHelper;
 
     @Inject
     public DefaultPaymentDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
         this.transactionalSqlDao = new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao);
+        this.paginationHelper = new DefaultPaginationSqlDaoHelper(transactionalSqlDao);
     }
 
     @Override
@@ -160,6 +166,20 @@ public class DefaultPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<RefundModelDao> getRefunds(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+        return paginationHelper.getPagination(RefundSqlDao.class,
+                                              new PaginationIteratorBuilder<RefundModelDao, Refund, RefundSqlDao>() {
+                                                  @Override
+                                                  public Iterator<RefundModelDao> build(final RefundSqlDao refundSqlDao, final Long limit) {
+                                                      return refundSqlDao.getByPluginName(pluginName, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
+    }
+
+    @Override
     public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
         return transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<RefundModelDao>() {
             @Override
@@ -221,30 +241,16 @@ 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);
+        return paginationHelper.getPagination(PaymentMethodSqlDao.class,
+                                              new PaginationIteratorBuilder<PaymentMethodModelDao, PaymentMethod, PaymentMethodSqlDao>() {
+                                                  @Override
+                                                  public Iterator<PaymentMethodModelDao> build(final PaymentMethodSqlDao paymentMethodSqlDao, final Long limit) {
+                                                      return paymentMethodSqlDao.getByPluginName(pluginName, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
     }
 
     @Override
@@ -300,30 +306,16 @@ 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);
+        return paginationHelper.getPagination(PaymentSqlDao.class,
+                                              new PaginationIteratorBuilder<PaymentModelDao, Payment, PaymentSqlDao>() {
+                                                  @Override
+                                                  public Iterator<PaymentModelDao> build(final PaymentSqlDao paymentSqlDao, final Long limit) {
+                                                      return paymentSqlDao.getByPluginName(pluginName, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
     }
 
     @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 613a8d0..17e2503 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
@@ -55,6 +55,8 @@ public interface PaymentDao {
 
     public void updateRefundStatus(UUID refundId, RefundStatus status, BigDecimal processedAmount, Currency processedCurrency, InternalCallContext context);
 
+    public Pagination<RefundModelDao> getRefunds(String pluginName, Long offset, Long limit, InternalTenantContext context);
+
     public RefundModelDao getRefund(UUID refundId, InternalTenantContext context);
 
     public List<RefundModelDao> getRefundsForPayment(UUID paymentId, InternalTenantContext context);
diff --git a/payment/src/main/java/com/ning/billing/payment/dao/RefundSqlDao.java b/payment/src/main/java/com/ning/billing/payment/dao/RefundSqlDao.java
index 4c3f0cb..4c3d5cb 100644
--- a/payment/src/main/java/com/ning/billing/payment/dao/RefundSqlDao.java
+++ b/payment/src/main/java/com/ning/billing/payment/dao/RefundSqlDao.java
@@ -17,18 +17,20 @@
 package com.ning.billing.payment.dao;
 
 import java.math.BigDecimal;
+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.Refund;
 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;
@@ -51,4 +53,13 @@ public interface RefundSqlDao extends EntitySqlDao<RefundModelDao, Refund> {
     @SqlQuery
     List<RefundModelDao> getRefundsForAccount(@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<RefundModelDao> 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/DefaultNoOpPaymentProviderPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpPaymentProviderPlugin.java
index 066000e..a1026cc 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
@@ -228,7 +228,7 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
                                                                   refundAmount, kbPaymentId.toString(), paymentInfoPlugin.getAmount(), PLUGIN_NAME));
         }
 
-        final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+        final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(kbPaymentId, refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
         refunds.put(kbPaymentId.toString(), refundInfoPlugin);
 
         return refundInfoPlugin;
@@ -238,4 +238,26 @@ public class DefaultNoOpPaymentProviderPlugin implements NoOpPaymentPluginApi {
     public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) {
         return ImmutableList.<RefundInfoPlugin>copyOf(refunds.get(kbPaymentId.toString()));
     }
+
+    @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        final ImmutableList<RefundInfoPlugin> allResults = ImmutableList.<RefundInfoPlugin>copyOf(Iterables.<RefundInfoPlugin>filter(Iterables.<RefundInfoPlugin>concat(refunds.values()), new Predicate<RefundInfoPlugin>() {
+            @Override
+            public boolean apply(final RefundInfoPlugin input) {
+                return (input.getFirstRefundReferenceId() != null && input.getFirstRefundReferenceId().contains(searchKey)) ||
+                       (input.getSecondRefundReferenceId() != null && input.getSecondRefundReferenceId().contains(searchKey));
+            }
+        }));
+
+        final List<RefundInfoPlugin> results;
+        if (offset >= allResults.size()) {
+            results = ImmutableList.<RefundInfoPlugin>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<RefundInfoPlugin>(offset, limit, (long) results.size(), (long) refunds.values().size(), results.iterator());
+    }
 }
diff --git a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java
index c3294d4..698804d 100644
--- a/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java
+++ b/payment/src/main/java/com/ning/billing/payment/provider/DefaultNoOpRefundInfoPlugin.java
@@ -17,6 +17,7 @@
 package com.ning.billing.payment.provider;
 
 import java.math.BigDecimal;
+import java.util.UUID;
 
 import org.joda.time.DateTime;
 
@@ -26,6 +27,7 @@ import com.ning.billing.payment.plugin.api.RefundPluginStatus;
 
 public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
 
+    private final UUID kbPaymentId;
     private final BigDecimal amount;
     private final Currency currency;
     private final DateTime effectiveDate;
@@ -33,8 +35,9 @@ public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
     private final RefundPluginStatus status;
     private final String error;
 
-    public DefaultNoOpRefundInfoPlugin(final BigDecimal amount, final Currency currency, final DateTime effectiveDate,
+    public DefaultNoOpRefundInfoPlugin(final UUID kbPaymentId, final BigDecimal amount, final Currency currency, final DateTime effectiveDate,
                                        final DateTime createdDate, final RefundPluginStatus status, final String error) {
+        this.kbPaymentId = kbPaymentId;
         this.amount = amount;
         this.currency = currency;
         this.effectiveDate = effectiveDate;
@@ -44,6 +47,11 @@ public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
     }
 
     @Override
+    public UUID getKbPaymentId() {
+        return kbPaymentId;
+    }
+
+    @Override
     public BigDecimal getAmount() {
         return amount;
     }
@@ -79,15 +87,21 @@ public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
     }
 
     @Override
-    public String getReferenceId() {
+    public String getFirstRefundReferenceId() {
+        return null;
+    }
+
+    @Override
+    public String getSecondRefundReferenceId() {
         return null;
     }
 
     @Override
     public String toString() {
-        final StringBuilder sb = new StringBuilder();
-        sb.append("DefaultNoOpRefundInfoPlugin");
-        sb.append("{amount=").append(amount);
+        final StringBuilder sb = new StringBuilder("DefaultNoOpRefundInfoPlugin{");
+        sb.append("kbPaymentId=").append(kbPaymentId);
+        sb.append(", amount=").append(amount);
+        sb.append(", currency=").append(currency);
         sb.append(", effectiveDate=").append(effectiveDate);
         sb.append(", createdDate=").append(createdDate);
         sb.append(", status=").append(status);
@@ -107,18 +121,24 @@ public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
 
         final DefaultNoOpRefundInfoPlugin that = (DefaultNoOpRefundInfoPlugin) o;
 
-        if (amount != null ? !amount.equals(that.amount) : that.amount != null) {
+        if (amount != null ? amount.compareTo(that.amount) != 0 : that.amount != null) {
+            return false;
+        }
+        if (createdDate != null ? createdDate.compareTo(that.createdDate) != 0 : that.createdDate != null) {
             return false;
         }
-        if (createdDate != null ? !createdDate.equals(that.createdDate) : that.createdDate != null) {
+        if (currency != that.currency) {
             return false;
         }
-        if (effectiveDate != null ? !effectiveDate.equals(that.effectiveDate) : that.effectiveDate != null) {
+        if (effectiveDate != null ? effectiveDate.compareTo(that.effectiveDate) != 0 : that.effectiveDate != null) {
             return false;
         }
         if (error != null ? !error.equals(that.error) : that.error != null) {
             return false;
         }
+        if (kbPaymentId != null ? !kbPaymentId.equals(that.kbPaymentId) : that.kbPaymentId != null) {
+            return false;
+        }
         if (status != that.status) {
             return false;
         }
@@ -128,7 +148,9 @@ public class DefaultNoOpRefundInfoPlugin implements RefundInfoPlugin {
 
     @Override
     public int hashCode() {
-        int result = amount != null ? amount.hashCode() : 0;
+        int result = kbPaymentId != null ? kbPaymentId.hashCode() : 0;
+        result = 31 * result + (amount != null ? amount.hashCode() : 0);
+        result = 31 * result + (currency != null ? currency.hashCode() : 0);
         result = 31 * result + (effectiveDate != null ? effectiveDate.hashCode() : 0);
         result = 31 * result + (createdDate != null ? createdDate.hashCode() : 0);
         result = 31 * result + (status != null ? status.hashCode() : 0);
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 ba12401..1d4572e 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
@@ -75,7 +75,7 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
 
     @Override
     public RefundInfoPlugin processRefund(final UUID kbAccountId, final UUID kbPaymentId, final BigDecimal refundAmount, final Currency currency, final CallContext context) throws PaymentPluginApiException {
-        return new DefaultNoOpRefundInfoPlugin(BigDecimal.ZERO, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+        return new DefaultNoOpRefundInfoPlugin(kbPaymentId, BigDecimal.ZERO, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
     }
 
     @Override
@@ -84,6 +84,11 @@ public class ExternalPaymentProviderPlugin implements PaymentPluginApi {
     }
 
     @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        return new DefaultPagination<RefundInfoPlugin>(offset, limit, 0L, 0L, Iterators.<RefundInfoPlugin>emptyIterator());
+    }
+
+    @Override
     public void addPaymentMethod(final UUID kbAccountId, final UUID kbPaymentMethodId, final PaymentMethodPlugin paymentMethodProps, final boolean setDefault, final CallContext context) throws PaymentPluginApiException {
     }
 
diff --git a/payment/src/main/resources/com/ning/billing/payment/dao/RefundSqlDao.sql.stg b/payment/src/main/resources/com/ning/billing/payment/dao/RefundSqlDao.sql.stg
index a288ea5..dc9025b 100644
--- a/payment/src/main/resources/com/ning/billing/payment/dao/RefundSqlDao.sql.stg
+++ b/payment/src/main/resources/com/ning/billing/payment/dao/RefundSqlDao.sql.stg
@@ -63,3 +63,14 @@ where account_id = :accountId
 <defaultOrderBy()>
 ;
 >>
+
+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
+order by record_id
+limit :offset, :rowCount
+;
+>>
\ No newline at end of file
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 3e14944..431860e 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
@@ -197,6 +197,11 @@ public class MockPaymentDao implements PaymentDao {
     }
 
     @Override
+    public Pagination<RefundModelDao> getRefunds(final String pluginName, final Long offset, final Long limit, final InternalTenantContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public RefundModelDao getRefund(final UUID refundId, final InternalTenantContext context) {
         return null;
     }
diff --git a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
index 39d330b..246cfd3 100644
--- a/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
+++ b/payment/src/test/java/com/ning/billing/payment/provider/MockPaymentProviderPlugin.java
@@ -121,7 +121,7 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
         final ImmutableList<PaymentInfoPlugin> results = ImmutableList.<PaymentInfoPlugin>copyOf(Iterables.<PaymentInfoPlugin>filter(payments.values(), new Predicate<PaymentInfoPlugin>() {
             @Override
             public boolean apply(final PaymentInfoPlugin input) {
-                return (input.getKbPaymentId() != null && input.getKbPaymentId().equals(searchKey)) ||
+                return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) ||
                        (input.getFirstPaymentReferenceId() != null && input.getFirstPaymentReferenceId().contains(searchKey)) ||
                        (input.getSecondPaymentReferenceId() != null && input.getSecondPaymentReferenceId().contains(searchKey));
             }
@@ -202,7 +202,7 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
                                                                   refundAmount, kbPaymentId.toString(), paymentInfoPlugin.getAmount(), PLUGIN_NAME));
         }
 
-        final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
+        final DefaultNoOpRefundInfoPlugin refundInfoPlugin = new DefaultNoOpRefundInfoPlugin(kbPaymentId, refundAmount, currency, clock.getUTCNow(), clock.getUTCNow(), RefundPluginStatus.PROCESSED, null);
         refunds.put(kbPaymentId.toString(), refundInfoPlugin);
 
         return refundInfoPlugin;
@@ -212,4 +212,17 @@ public class MockPaymentProviderPlugin implements NoOpPaymentPluginApi {
     public List<RefundInfoPlugin> getRefundInfo(final UUID kbAccountId, final UUID kbPaymentId, final TenantContext context) throws PaymentPluginApiException {
         return Collections.<RefundInfoPlugin>emptyList();
     }
+
+    @Override
+    public Pagination<RefundInfoPlugin> searchRefunds(final String searchKey, final Long offset, final Long limit, final TenantContext tenantContext) throws PaymentPluginApiException {
+        final ImmutableList<RefundInfoPlugin> results = ImmutableList.<RefundInfoPlugin>copyOf(Iterables.<RefundInfoPlugin>filter(refunds.values(), new Predicate<RefundInfoPlugin>() {
+            @Override
+            public boolean apply(final RefundInfoPlugin input) {
+                return (input.getKbPaymentId() != null && input.getKbPaymentId().toString().equals(searchKey)) ||
+                       (input.getFirstRefundReferenceId() != null && input.getFirstRefundReferenceId().contains(searchKey)) ||
+                       (input.getSecondRefundReferenceId() != null && input.getSecondRefundReferenceId().contains(searchKey));
+            }
+        }));
+        return DefaultPagination.<RefundInfoPlugin>build(offset, limit, results);
+    }
 }

pom.xml 2(+1 -1)

diff --git a/pom.xml b/pom.xml
index 78df4c9..a91fd65 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.15</version>
+        <version>0.8.0-SNAPSHOT</version>
     </parent>
     <artifactId>killbill</artifactId>
     <version>0.8.8-SNAPSHOT</version>
diff --git a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
index 8c30cba..667c77d 100644
--- a/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
+++ b/server/src/main/java/com/ning/billing/server/modules/KillbillServerModule.java
@@ -19,10 +19,6 @@ package com.ning.billing.server.modules;
 import javax.servlet.ServletContext;
 import javax.sql.DataSource;
 
-import com.ning.billing.clock.Clock;
-import com.ning.billing.clock.ClockMock;
-import com.ning.billing.currency.glue.CurrencyModule;
-import com.ning.billing.entitlement.glue.DefaultEntitlementModule;
 import org.skife.config.ConfigSource;
 import org.skife.config.SimplePropertyConfigSource;
 import org.skife.jdbi.v2.DBI;
@@ -31,21 +27,26 @@ import org.skife.jdbi.v2.IDBI;
 import com.ning.billing.account.glue.DefaultAccountModule;
 import com.ning.billing.beatrix.glue.BeatrixModule;
 import com.ning.billing.catalog.glue.CatalogModule;
-import com.ning.billing.jaxrs.resources.SubscriptionResource;
-import com.ning.billing.jaxrs.resources.TestResource;
-import com.ning.billing.subscription.glue.DefaultSubscriptionModule;
+import com.ning.billing.clock.Clock;
+import com.ning.billing.clock.ClockMock;
+import com.ning.billing.currency.glue.CurrencyModule;
+import com.ning.billing.entitlement.glue.DefaultEntitlementModule;
 import com.ning.billing.invoice.glue.DefaultInvoiceModule;
 import com.ning.billing.jaxrs.resources.AccountResource;
 import com.ning.billing.jaxrs.resources.BundleResource;
 import com.ning.billing.jaxrs.resources.CatalogResource;
+import com.ning.billing.jaxrs.resources.CustomFieldResource;
 import com.ning.billing.jaxrs.resources.ExportResource;
 import com.ning.billing.jaxrs.resources.InvoiceResource;
 import com.ning.billing.jaxrs.resources.PaymentMethodResource;
 import com.ning.billing.jaxrs.resources.PaymentResource;
 import com.ning.billing.jaxrs.resources.PluginResource;
 import com.ning.billing.jaxrs.resources.RefundResource;
+import com.ning.billing.jaxrs.resources.SubscriptionResource;
+import com.ning.billing.jaxrs.resources.TagDefinitionResource;
 import com.ning.billing.jaxrs.resources.TagResource;
 import com.ning.billing.jaxrs.resources.TenantResource;
+import com.ning.billing.jaxrs.resources.TestResource;
 import com.ning.billing.jaxrs.util.KillbillEventHandler;
 import com.ning.billing.junction.glue.DefaultJunctionModule;
 import com.ning.billing.osgi.glue.DefaultOSGIModule;
@@ -54,6 +55,7 @@ import com.ning.billing.payment.glue.PaymentModule;
 import com.ning.billing.server.DefaultServerService;
 import com.ning.billing.server.ServerService;
 import com.ning.billing.server.notifications.PushNotificationListener;
+import com.ning.billing.subscription.glue.DefaultSubscriptionModule;
 import com.ning.billing.tenant.glue.TenantModule;
 import com.ning.billing.usage.glue.UsageModule;
 import com.ning.billing.util.email.EmailModule;
@@ -115,7 +117,9 @@ public class KillbillServerModule extends AbstractModule {
         bind(BundleResource.class).asEagerSingleton();
         bind(SubscriptionResource.class).asEagerSingleton();
         bind(InvoiceResource.class).asEagerSingleton();
+        bind(CustomFieldResource.class).asEagerSingleton();
         bind(TagResource.class).asEagerSingleton();
+        bind(TagDefinitionResource.class).asEagerSingleton();
         bind(CatalogResource.class).asEagerSingleton();
         bind(PaymentMethodResource.class).asEagerSingleton();
         bind(PaymentResource.class).asEagerSingleton();
diff --git a/util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldUserApi.java b/util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldUserApi.java
index e0e543d..903659d 100644
--- a/util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldUserApi.java
+++ b/util/src/main/java/com/ning/billing/util/customfield/api/DefaultCustomFieldUserApi.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.util.customfield.api;
 
+import java.util.Collection;
 import java.util.List;
 import java.util.UUID;
 
@@ -29,14 +30,25 @@ import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.customfield.StringCustomField;
 import com.ning.billing.util.customfield.dao.CustomFieldDao;
 import com.ning.billing.util.customfield.dao.CustomFieldModelDao;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 
 import com.google.common.base.Function;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.inject.Inject;
 
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
 public class DefaultCustomFieldUserApi implements CustomFieldUserApi {
 
+    private static final Function<CustomFieldModelDao, CustomField> CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION = new Function<CustomFieldModelDao, CustomField>() {
+        @Override
+        public CustomField apply(final CustomFieldModelDao input) {
+            return new StringCustomField(input);
+        }
+    };
+
     private final InternalCallContextFactory internalCallContextFactory;
     private final CustomFieldDao customFieldDao;
 
@@ -47,6 +59,30 @@ public class DefaultCustomFieldUserApi implements CustomFieldUserApi {
     }
 
     @Override
+    public Pagination<CustomField> searchCustomFields(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<CustomFieldModelDao, CustomFieldApiException>() {
+                                                  @Override
+                                                  public Pagination<CustomFieldModelDao> build() {
+                                                      return customFieldDao.searchCustomFields(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION);
+    }
+
+    @Override
+    public Pagination<CustomField> getCustomFields(final Long offset, final Long limit, final TenantContext context) {
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<CustomFieldModelDao, CustomFieldApiException>() {
+                                                  @Override
+                                                  public Pagination<CustomFieldModelDao> build() {
+                                                      return customFieldDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION);
+    }
+
+    @Override
     public void addCustomFields(final List<CustomField> fields, final CallContext context) throws CustomFieldApiException {
         // TODO make it transactional
         for (final CustomField cur : fields) {
@@ -69,13 +105,7 @@ public class DefaultCustomFieldUserApi implements CustomFieldUserApi {
         return withCustomFieldsTransform(customFieldDao.getCustomFieldsForAccount(internalCallContextFactory.createInternalTenantContext(accountId, context)));
     }
 
-    private List<CustomField> withCustomFieldsTransform(List<CustomFieldModelDao> input) {
-        return ImmutableList.<CustomField>copyOf(Collections2.transform(input, new Function<CustomFieldModelDao, CustomField>() {
-            @Override
-            public CustomField apply(final CustomFieldModelDao input) {
-                return new StringCustomField(input);
-            }
-        }));
+    private List<CustomField> withCustomFieldsTransform(final Collection<CustomFieldModelDao> input) {
+        return ImmutableList.<CustomField>copyOf(Collections2.transform(input, CUSTOM_FIELD_MODEL_DAO_CUSTOM_FIELD_FUNCTION));
     }
-
 }
diff --git a/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldDao.java b/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldDao.java
index b64b5b6..20e962f 100644
--- a/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldDao.java
+++ b/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldDao.java
@@ -20,13 +20,16 @@ import java.util.List;
 import java.util.UUID;
 
 import com.ning.billing.ObjectType;
-import com.ning.billing.util.api.CustomFieldApiException;
 import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.api.CustomFieldApiException;
 import com.ning.billing.util.customfield.CustomField;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.entity.dao.EntityDao;
 
 public interface CustomFieldDao extends EntityDao<CustomFieldModelDao, CustomField, CustomFieldApiException> {
 
+    public Pagination<CustomFieldModelDao> searchCustomFields(String searchKey, Long offset, Long limit, InternalTenantContext context);
+
     public List<CustomFieldModelDao> getCustomFieldsForObject(final UUID objectId, final ObjectType objectType, final InternalTenantContext context);
 
     public List<CustomFieldModelDao> getCustomFieldsForAccountType(final ObjectType objectType, final InternalTenantContext context);
diff --git a/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.java b/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.java
index 6f11bcf..b54503c 100644
--- a/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.java
@@ -16,12 +16,15 @@
 
 package com.ning.billing.util.customfield.dao;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
 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.customizers.Define;
+import org.skife.jdbi.v2.sqlobject.customizers.FetchSize;
 
 import com.ning.billing.ObjectType;
 import com.ning.billing.callcontext.InternalTenantContext;
@@ -36,4 +39,13 @@ public interface CustomFieldSqlDao extends EntitySqlDao<CustomFieldModelDao, Cus
     List<CustomFieldModelDao> getCustomFieldsForObject(@Bind("objectId") UUID objectId,
                                                        @Bind("objectType") ObjectType objectType,
                                                        @BindBean InternalTenantContext internalTenantContext);
+
+    @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<CustomFieldModelDao> searchCustomFields(@Define("searchKey") final String searchKey,
+                                                            @Bind("offset") final Long offset,
+                                                            @Bind("rowCount") final Long rowCount,
+                                                            @BindBean final InternalTenantContext context);
 }
diff --git a/util/src/main/java/com/ning/billing/util/customfield/dao/DefaultCustomFieldDao.java b/util/src/main/java/com/ning/billing/util/customfield/dao/DefaultCustomFieldDao.java
index 0aa1076..d3642a4 100644
--- a/util/src/main/java/com/ning/billing/util/customfield/dao/DefaultCustomFieldDao.java
+++ b/util/src/main/java/com/ning/billing/util/customfield/dao/DefaultCustomFieldDao.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.util.customfield.dao;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -29,22 +30,24 @@ import com.ning.billing.BillingExceptionBase;
 import com.ning.billing.ErrorCode;
 import com.ning.billing.ObjectType;
 import com.ning.billing.bus.api.PersistentBus;
-import com.ning.billing.util.api.CustomFieldApiException;
-import com.ning.billing.util.audit.ChangeType;
-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.events.BusInternalEvent;
+import com.ning.billing.util.api.CustomFieldApiException;
+import com.ning.billing.util.audit.ChangeType;
+import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.customfield.CustomField;
 import com.ning.billing.util.customfield.api.DefaultCustomFieldCreationEvent;
 import com.ning.billing.util.customfield.api.DefaultCustomFieldDeletionEvent;
 import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionalJdbiWrapper;
 import com.ning.billing.util.entity.dao.EntitySqlDaoWrapperFactory;
-import com.ning.billing.events.BusInternalEvent;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
@@ -53,7 +56,7 @@ import com.google.inject.Inject;
 
 public class DefaultCustomFieldDao extends EntityDaoBase<CustomFieldModelDao, CustomField, CustomFieldApiException> implements CustomFieldDao {
 
-    private final static Logger log = LoggerFactory.getLogger(DefaultCustomFieldDao.class);
+    private static final Logger log = LoggerFactory.getLogger(DefaultCustomFieldDao.class);
 
     private final PersistentBus bus;
 
@@ -127,4 +130,17 @@ public class DefaultCustomFieldDao extends EntityDaoBase<CustomFieldModelDao, Cu
 
     }
 
+    @Override
+    public Pagination<CustomFieldModelDao> searchCustomFields(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        return paginationHelper.getPagination(CustomFieldSqlDao.class,
+                                              new PaginationIteratorBuilder<CustomFieldModelDao, CustomField, CustomFieldSqlDao>() {
+                                                  @Override
+                                                  public Iterator<CustomFieldModelDao> build(final CustomFieldSqlDao customFieldSqlDao, final Long limit) {
+                                                      return customFieldSqlDao.searchCustomFields(searchKey, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
+    }
 }
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationHelper.java b/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationHelper.java
new file mode 100644
index 0000000..1383955
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationHelper.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.entity.dao;
+
+import java.util.LinkedList;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ning.billing.BillingExceptionBase;
+import com.ning.billing.util.customfield.ShouldntHappenException;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+
+public class DefaultPaginationHelper {
+
+    private static final Logger log = LoggerFactory.getLogger(DefaultPaginationHelper.class);
+
+    public abstract static class EntityPaginationBuilder<E extends Entity, T extends BillingExceptionBase> {
+
+        public abstract Pagination<E> build(final Long offset, final Long limit, final String pluginName) throws T;
+    }
+
+    public static <E extends Entity, T extends BillingExceptionBase> Pagination<E> getEntityPaginationFromPlugins(final Iterable<String> plugins, final Long offset, final Long limit, final EntityPaginationBuilder<E, T> entityPaginationBuilder) {
+        // 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<E> allResults = new LinkedList<E>();
+        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 : plugins) {
+            try {
+                final Pagination<E> pages;
+                if (allResults.size() >= limit) {
+                    // We have enough results, we just keep going (limit 1) to get the stats
+                    pages = entityPaginationBuilder.build(firstSearch ? offset : 0L, 1L, pluginName);
+                    // Required to close database connections
+                    ImmutableList.<E>copyOf(pages);
+                } else {
+                    pages = entityPaginationBuilder.build(firstSearch ? offset : 0L, limit - allResults.size(), pluginName);
+                    allResults.addAll(ImmutableList.<E>copyOf(pages));
+                }
+                firstSearch = false;
+                totalNbRecords += pages.getTotalNbRecords();
+                maxNbRecords += pages.getMaxNbRecords();
+            } catch (final BillingExceptionBase e) {
+                log.warn("Error while searching plugin " + pluginName, e);
+                // Non-fatal, continue to search other plugins
+            }
+        }
+
+        return new DefaultPagination<E>(offset, limit, totalNbRecords, maxNbRecords, allResults.iterator());
+    }
+
+    public abstract static class SourcePaginationBuilder<O, T extends BillingExceptionBase> {
+
+        public abstract Pagination<O> build() throws T;
+    }
+
+    public static <E extends Entity, O, T extends BillingExceptionBase> Pagination<E> getEntityPagination(final Long limit,
+                                                                                                          final SourcePaginationBuilder<O, T> sourcePaginationBuilder,
+                                                                                                          final Function<O, E> function) throws T {
+        final Pagination<O> modelsDao = sourcePaginationBuilder.build();
+
+        return new DefaultPagination<E>(modelsDao,
+                                        limit,
+                                        Iterators.<E>filter(Iterators.<O, E>transform(modelsDao.iterator(), function),
+                                                            Predicates.<E>notNull()));
+    }
+
+    public static <E extends Entity, O, T extends BillingExceptionBase> Pagination<E> getEntityPaginationNoException(final Long limit,
+                                                                                                                     final SourcePaginationBuilder<O, T> sourcePaginationBuilder,
+                                                                                                                     final Function<O, E> function) {
+        try {
+            return getEntityPagination(limit, sourcePaginationBuilder, function);
+        } catch (final BillingExceptionBase e) {
+            throw new ShouldntHappenException("No exception expected" + e);
+        }
+    }
+}
diff --git a/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java b/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
new file mode 100644
index 0000000..e245ffd
--- /dev/null
+++ b/util/src/main/java/com/ning/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2010-2014 Ning, Inc.
+ *
+ * Ning licenses this file to you under the Apache License, version 2.0
+ * (the "License"); you may not use this file except in compliance with the
+ * License.  You may obtain a copy of the License at:
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+package com.ning.billing.util.entity.dao;
+
+import java.util.Iterator;
+
+import com.ning.billing.callcontext.InternalTenantContext;
+import com.ning.billing.util.entity.DefaultPagination;
+import com.ning.billing.util.entity.Entity;
+import com.ning.billing.util.entity.Pagination;
+
+public class DefaultPaginationSqlDaoHelper {
+
+    private final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+
+    public DefaultPaginationSqlDaoHelper(final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao) {
+        this.transactionalSqlDao = transactionalSqlDao;
+    }
+
+    public <E extends Entity, M extends EntityModelDao<E>, S extends EntitySqlDao<M, E>> Pagination<M> getPagination(final Class<? extends EntitySqlDao<M, E>> sqlDaoClazz,
+                                                                                                                     final PaginationIteratorBuilder<M, E, S> paginationIteratorBuilder,
+                                                                                                                     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(sqlDaoClazz);
+                // TODO lame cast, but couldn't make sqlDaoClazz a Class<? extends S>
+                final Iterator<M> dumbIterator = paginationIteratorBuilder.build((S) sqlDao, 1L);
+                // 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(sqlDaoClazz);
+        final Long totalCount = sqlDao.getCount(context);
+        final Iterator<M> results = paginationIteratorBuilder.build((S) sqlDao, limit);
+
+        return new DefaultPagination<M>(offset, limit, count, totalCount, results);
+    }
+
+    public abstract static class PaginationIteratorBuilder<M extends EntityModelDao<E>, E extends Entity, S extends EntitySqlDao<M, E>> {
+
+        public abstract Iterator<M> build(final S sqlDao,
+                                          final Long limit);
+    }
+}
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 d494c4e..a0721bc 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
@@ -26,16 +26,19 @@ import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.entity.DefaultPagination;
 import com.ning.billing.util.entity.Entity;
 import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
 
 public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entity, U extends BillingExceptionBase> implements EntityDao<M, E, U> {
 
     protected final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao;
+    protected final DefaultPaginationSqlDaoHelper paginationHelper;
 
     private final Class<? extends EntitySqlDao<M, E>> realSqlDao;
 
     public EntityDaoBase(final EntitySqlDaoTransactionalJdbiWrapper transactionalSqlDao, final Class<? extends EntitySqlDao<M, E>> realSqlDao) {
         this.transactionalSqlDao = transactionalSqlDao;
         this.realSqlDao = realSqlDao;
+        this.paginationHelper = new DefaultPaginationSqlDaoHelper(transactionalSqlDao);
     }
 
     @Override
@@ -130,30 +133,16 @@ public abstract class EntityDaoBase<M extends EntityModelDao<E>, E extends Entit
 
     @Override
     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 totalCount = sqlDao.getCount(context);
-        final Iterator<M> results = sqlDao.get(offset, limit, getNaturalOrderingColumns(), context);
-
-        return new DefaultPagination<M>(offset, limit, count, totalCount, results);
+        return paginationHelper.getPagination(realSqlDao,
+                                              new PaginationIteratorBuilder<M, E, EntitySqlDao<M, E>>() {
+                                                  @Override
+                                                  public Iterator<M> build(final EntitySqlDao<M, E> sqlDao, final Long limit) {
+                                                      return sqlDao.get(offset, limit, getNaturalOrderingColumns(), context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
     }
 
     @Override
diff --git a/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java b/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
index daf484e..50df856 100644
--- a/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
+++ b/util/src/main/java/com/ning/billing/util/tag/api/DefaultTagUserApi.java
@@ -29,6 +29,8 @@ import com.ning.billing.util.api.TagUserApi;
 import com.ning.billing.util.callcontext.CallContext;
 import com.ning.billing.util.callcontext.InternalCallContextFactory;
 import com.ning.billing.util.callcontext.TenantContext;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationHelper.SourcePaginationBuilder;
 import com.ning.billing.util.tag.ControlTagType;
 import com.ning.billing.util.tag.DefaultControlTag;
 import com.ning.billing.util.tag.DefaultTagDefinition;
@@ -46,8 +48,19 @@ import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 import com.google.inject.Inject;
 
+import static com.ning.billing.util.entity.dao.DefaultPaginationHelper.getEntityPaginationNoException;
+
 public class DefaultTagUserApi implements TagUserApi {
 
+    private static final Function<TagModelDao, Tag> TAG_MODEL_DAO_TAG_FUNCTION = new Function<TagModelDao, Tag>() {
+        @Override
+        public Tag apply(final TagModelDao input) {
+            return TagModelDaoHelper.isControlTag(input.getTagDefinitionId()) ?
+                   new DefaultControlTag(input.getId(), ControlTagType.getTypeFromId(input.getTagDefinitionId()), input.getObjectType(), input.getObjectId(), input.getCreatedDate()) :
+                   new DescriptiveTag(input.getId(), input.getTagDefinitionId(), input.getObjectType(), input.getObjectId(), input.getCreatedDate());
+        }
+    };
+
     private final InternalCallContextFactory internalCallContextFactory;
     private final TagDefinitionDao tagDefinitionDao;
     private final TagDao tagDao;
@@ -127,6 +140,30 @@ public class DefaultTagUserApi implements TagUserApi {
     }
 
     @Override
+    public Pagination<Tag> searchTags(final String searchKey, final Long offset, final Long limit, final TenantContext context) {
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<TagModelDao, TagApiException>() {
+                                                  @Override
+                                                  public Pagination<TagModelDao> build() {
+                                                      return tagDao.searchTags(searchKey, offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              TAG_MODEL_DAO_TAG_FUNCTION);
+    }
+
+    @Override
+    public Pagination<Tag> getTags(final Long offset, final Long limit, final TenantContext context) {
+        return getEntityPaginationNoException(limit,
+                                              new SourcePaginationBuilder<TagModelDao, TagApiException>() {
+                                                  @Override
+                                                  public Pagination<TagModelDao> build() {
+                                                      return tagDao.get(offset, limit, internalCallContextFactory.createInternalTenantContext(context));
+                                                  }
+                                              },
+                                              TAG_MODEL_DAO_TAG_FUNCTION);
+    }
+
+    @Override
     public void removeTags(final UUID objectId, final ObjectType objectType, final Collection<UUID> tagDefinitionIds, final CallContext context) throws TagApiException {
         // TODO: consider making this batch
         for (final UUID tagDefinitionId : tagDefinitionIds) {
@@ -156,14 +193,7 @@ public class DefaultTagUserApi implements TagUserApi {
         return withModelTransform(tagDao.getTagsForAccount(includedDeleted, internalCallContextFactory.createInternalTenantContext(accountId, context)));
     }
 
-    private List<Tag> withModelTransform(final List<TagModelDao> input) {
-        return ImmutableList.<Tag>copyOf(Collections2.transform(input, new Function<TagModelDao, Tag>() {
-            @Override
-            public Tag apply(final TagModelDao input) {
-                return TagModelDaoHelper.isControlTag(input.getTagDefinitionId()) ?
-                       new DefaultControlTag(input.getId(), ControlTagType.getTypeFromId(input.getTagDefinitionId()), input.getObjectType(), input.getObjectId(), input.getCreatedDate()) :
-                       new DescriptiveTag(input.getId(), input.getTagDefinitionId(), input.getObjectType(), input.getObjectId(), input.getCreatedDate());
-            }
-        }));
+    private List<Tag> withModelTransform(final Collection<TagModelDao> input) {
+        return ImmutableList.<Tag>copyOf(Collections2.transform(input, TAG_MODEL_DAO_TAG_FUNCTION));
     }
 }
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
index d1307ec..e559df7 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/DefaultTagDao.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.util.tag.dao;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -35,6 +36,8 @@ import com.ning.billing.util.api.TagApiException;
 import com.ning.billing.util.audit.ChangeType;
 import com.ning.billing.util.cache.CacheControllerDispatcher;
 import com.ning.billing.util.dao.NonEntityDao;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.DefaultPaginationSqlDaoHelper.PaginationIteratorBuilder;
 import com.ning.billing.util.entity.dao.EntityDaoBase;
 import com.ning.billing.util.entity.dao.EntitySqlDao;
 import com.ning.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
@@ -213,4 +216,18 @@ public class DefaultTagDao extends EntityDaoBase<TagModelDao, Tag, TagApiExcepti
         });
 
     }
+
+    @Override
+    public Pagination<TagModelDao> searchTags(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        return paginationHelper.getPagination(TagSqlDao.class,
+                                              new PaginationIteratorBuilder<TagModelDao, Tag, TagSqlDao>() {
+                                                  @Override
+                                                  public Iterator<TagModelDao> build(final TagSqlDao tagSqlDao, final Long limit) {
+                                                      return tagSqlDao.searchTags(searchKey, offset, limit, context);
+                                                  }
+                                              },
+                                              offset,
+                                              limit,
+                                              context);
+    }
 }
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/TagDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/TagDao.java
index 0077072..b1b0fba 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/TagDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/TagDao.java
@@ -23,14 +23,15 @@ import com.ning.billing.ObjectType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.api.TagApiException;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.EntityDao;
+import com.ning.billing.util.tag.Tag;
 
-public interface TagDao {
-
-    void create(TagModelDao tag, InternalCallContext context) throws TagApiException;
+public interface TagDao extends EntityDao<TagModelDao, Tag, TagApiException> {
 
     void deleteTag(UUID objectId, ObjectType objectType, UUID tagDefinition, InternalCallContext context) throws TagApiException;
 
-    TagModelDao getById(UUID tagId, InternalTenantContext context);
+    Pagination<TagModelDao> searchTags(String searchKey, Long offset, Long limit, InternalTenantContext context);
 
     List<TagModelDao> getTagsForObject(UUID objectId, ObjectType objectType, boolean includedDeleted, InternalTenantContext internalTenantContext);
 
diff --git a/util/src/main/java/com/ning/billing/util/tag/dao/TagSqlDao.java b/util/src/main/java/com/ning/billing/util/tag/dao/TagSqlDao.java
index 26caab0..53ebbc5 100644
--- a/util/src/main/java/com/ning/billing/util/tag/dao/TagSqlDao.java
+++ b/util/src/main/java/com/ning/billing/util/tag/dao/TagSqlDao.java
@@ -16,6 +16,7 @@
 
 package com.ning.billing.util.tag.dao;
 
+import java.util.Iterator;
 import java.util.List;
 import java.util.UUID;
 
@@ -23,6 +24,8 @@ 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.ObjectType;
 import com.ning.billing.callcontext.InternalCallContext;
@@ -50,4 +53,13 @@ public interface TagSqlDao extends EntitySqlDao<TagModelDao, Tag> {
     List<TagModelDao> getTagsForObjectIncludedDeleted(@Bind("objectId") UUID objectId,
                                                       @Bind("objectType") ObjectType objectType,
                                                       @BindBean InternalTenantContext internalTenantContext);
+
+    @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<TagModelDao> searchTags(@Define("searchKey") final String searchKey,
+                                            @Bind("offset") final Long offset,
+                                            @Bind("rowCount") final Long rowCount,
+                                            @BindBean final InternalTenantContext context);
 }
diff --git a/util/src/main/resources/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg b/util/src/main/resources/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg
index 1a91d2f..ee146da 100644
--- a/util/src/main/resources/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg
+++ b/util/src/main/resources/com/ning/billing/util/customfield/dao/CustomFieldSqlDao.sql.stg
@@ -39,3 +39,19 @@ and object_type = :objectType
 ;
 >>
 
+searchCustomFields(searchKey, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields("t.")>
+from <tableName()> t
+where 1 = 1
+and (
+     <idField("t.")> = '<searchKey>'
+  or t.object_type like '%<searchKey>%'
+  or t.field_name like '%<searchKey>%'
+  or t.field_value like '%<searchKey>%'
+)
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+limit :offset, :rowCount
+;
+>>
diff --git a/util/src/main/resources/com/ning/billing/util/tag/dao/TagSqlDao.sql.stg b/util/src/main/resources/com/ning/billing/util/tag/dao/TagSqlDao.sql.stg
index ae38127..7b14556 100644
--- a/util/src/main/resources/com/ning/billing/util/tag/dao/TagSqlDao.sql.stg
+++ b/util/src/main/resources/com/ning/billing/util/tag/dao/TagSqlDao.sql.stg
@@ -57,3 +57,21 @@ and t.object_type = :objectType
 <AND_CHECK_TENANT("t.")>
 ;
 >>
+
+searchTags(searchKey, offset, rowCount) ::= <<
+select SQL_CALC_FOUND_ROWS
+<allTableFields("t.")>
+from <tableName()> t
+join tag_definitions td on td.id = t.tag_definition_id
+where 1 = 1
+and (
+     <idField("t.")> = '<searchKey>'
+  or t.object_type like '%<searchKey>%'
+  or td.name like '%<searchKey>%'
+  or td.description like '%<searchKey>%'
+)
+<AND_CHECK_TENANT("t.")>
+order by <recordIdField("t.")> ASC
+limit :offset, :rowCount
+;
+>>
diff --git a/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java b/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
index 8521cb9..fd0c414 100644
--- a/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
+++ b/util/src/test/java/com/ning/billing/util/customfield/dao/MockCustomFieldDao.java
@@ -24,6 +24,7 @@ import com.ning.billing.ObjectType;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.api.CustomFieldApiException;
 import com.ning.billing.util.customfield.CustomField;
+import com.ning.billing.util.entity.Pagination;
 import com.ning.billing.util.entity.dao.MockEntityDaoBase;
 
 public class MockCustomFieldDao extends MockEntityDaoBase<CustomFieldModelDao, CustomField, CustomFieldApiException> implements CustomFieldDao {
@@ -49,4 +50,9 @@ public class MockCustomFieldDao extends MockEntityDaoBase<CustomFieldModelDao, C
     public List<CustomFieldModelDao> getCustomFieldsForAccount(final InternalTenantContext context) {
         throw new UnsupportedOperationException();
     }
+
+    @Override
+    public Pagination<CustomFieldModelDao> searchCustomFields(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        throw new UnsupportedOperationException();
+    }
 }
diff --git a/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDao.java b/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDao.java
index 890febb..7c8630d 100644
--- a/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDao.java
+++ b/util/src/test/java/com/ning/billing/util/tag/dao/MockTagDao.java
@@ -27,12 +27,15 @@ import com.ning.billing.ObjectType;
 import com.ning.billing.callcontext.InternalCallContext;
 import com.ning.billing.callcontext.InternalTenantContext;
 import com.ning.billing.util.api.TagApiException;
+import com.ning.billing.util.entity.Pagination;
+import com.ning.billing.util.entity.dao.MockEntityDaoBase;
+import com.ning.billing.util.tag.Tag;
 
 import com.google.common.base.Predicate;
 import com.google.common.collect.Collections2;
 import com.google.common.collect.ImmutableList;
 
-public class MockTagDao implements TagDao {
+public class MockTagDao extends MockEntityDaoBase<TagModelDao, Tag, TagApiException> implements TagDao {
 
     private final Map<UUID, List<TagModelDao>> tagStore = new HashMap<UUID, List<TagModelDao>>();
 
@@ -60,6 +63,11 @@ public class MockTagDao implements TagDao {
     }
 
     @Override
+    public Pagination<TagModelDao> searchTags(final String searchKey, final Long offset, final Long limit, final InternalTenantContext context) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
     public TagModelDao getById(final UUID tagId, final InternalTenantContext context) {
         throw new UnsupportedOperationException();
     }