killbill-memoizeit

Details

diff --git a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
index f78480d..4729e6d 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/StandaloneCatalogWithPriceOverride.java
@@ -19,6 +19,7 @@ package org.killbill.billing.catalog;
 
 import java.util.regex.Matcher;
 
+import org.killbill.billing.ErrorCode;
 import org.killbill.billing.callcontext.InternalCallContext;
 import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.catalog.api.CatalogApiException;
@@ -68,7 +69,6 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
         return internalCallContextFactory;
     }
 
-
     @Override
     public Plan createOrFindCurrentPlan(final PlanSpecifier spec, final PlanPhasePriceOverridesWithCallContext overrides) throws CatalogApiException {
         final Plan defaultPlan = super.createOrFindCurrentPlan(spec, null);
@@ -86,8 +86,10 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
     public DefaultPlan findCurrentPlan(final String planName) throws CatalogApiException {
         final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
         if (m.matches()) {
-            final InternalTenantContext internalTenantContext = createInternalTenantContext();
-            return priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
+            final DefaultPlan plan = maybeGetOverriddenPlan(planName);
+            if (plan != null) {
+                return plan;
+            }
         }
         return super.findCurrentPlan(planName);
     }
@@ -102,13 +104,32 @@ public class StandaloneCatalogWithPriceOverride extends StandaloneCatalog implem
         final String planName = DefaultPlanPhase.planName(phaseName);
         final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(planName);
         if (m.matches()) {
-            final InternalTenantContext internalTenantContext = createInternalTenantContext();
-            final Plan plan = priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
-            return plan.findPhase(phaseName);
+            final DefaultPlan plan = maybeGetOverriddenPlan(planName);
+            if (plan != null) {
+                return plan.findPhase(phaseName);
+            }
         }
         return super.findCurrentPhase(phaseName);
     }
 
+    private DefaultPlan maybeGetOverriddenPlan(final String planName) throws CatalogApiException {
+        final InternalTenantContext internalTenantContext = createInternalTenantContext();
+
+        try {
+            return priceOverride.getOverriddenPlan(planName, this, internalTenantContext);
+        } catch (final RuntimeException e) {
+            if (e.getCause() == null ||
+                e.getCause().getCause() == null ||
+                !(e.getCause().getCause() instanceof CatalogApiException) ||
+                ((CatalogApiException) e.getCause().getCause()).getCode() != ErrorCode.CAT_NO_SUCH_PLAN.getCode()) {
+                throw e;
+            } else {
+                // Otherwise, ambiguous name? See https://github.com/killbill/killbill/issues/842.
+                return null;
+            }
+        }
+    }
+
     private InternalTenantContext createInternalTenantContext() {
         return internalCallContextFactory.createInternalTenantContext(tenantRecordId, null);
     }
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
index 585e2b0..48feb2b 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestCatalogUpdater.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -31,10 +31,12 @@ import org.killbill.billing.catalog.api.Currency;
 import org.killbill.billing.catalog.api.MutableStaticCatalog;
 import org.killbill.billing.catalog.api.PhaseType;
 import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhase;
 import org.killbill.billing.catalog.api.PriceList;
 import org.killbill.billing.catalog.api.Product;
 import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.catalog.api.SimplePlanDescriptor;
+import org.killbill.billing.catalog.api.StaticCatalog;
 import org.killbill.billing.catalog.api.TimeUnit;
 import org.killbill.billing.catalog.api.user.DefaultSimplePlanDescriptor;
 import org.killbill.xmlloader.XMLLoader;
@@ -62,6 +64,37 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         assertEquals(catalog.getCurrentPlans().size(), 0);
     }
 
+    @Test(groups = "fast", description = "https://github.com/killbill/killbill/issues/842")
+    public void testCreateAmbiguousPlan() throws CatalogApiException {
+        final DateTime now = clock.getUTCNow();
+        final SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("foo-monthly-12345", "Foo", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.UNLIMITED, ImmutableList.<String>of());
+
+        final CatalogUpdater catalogUpdater = new CatalogUpdater(BillingMode.IN_ADVANCE, now, desc.getCurrency());
+        catalogUpdater.addSimplePlanDescriptor(desc);
+        final StandaloneCatalog catalog = catalogUpdater.getCatalog();
+
+        assertEquals(catalog.getCurrentPlans().size(), 1);
+
+        final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+                                                                                                        priceOverride,
+                                                                                                        internalCallContext.getTenantRecordId(),
+                                                                                                        internalCallContextFactory);
+
+        final Plan plan = catalog.findCurrentPlan("foo-monthly-12345");
+        assertEquals(plan.getName(), "foo-monthly-12345");
+
+        // Verify PriceOverride logic
+        final Plan plan2 = standaloneCatalogWithPriceOverride.findCurrentPlan("foo-monthly-12345");
+        assertEquals(plan2.getName(), "foo-monthly-12345");
+
+        final PlanPhase planPhase = catalog.findCurrentPhase("foo-monthly-12345-evergreen");
+        assertEquals(planPhase.getName(), "foo-monthly-12345-evergreen");
+
+        // Verify PriceOverride logic
+        final PlanPhase phase2 = standaloneCatalogWithPriceOverride.findCurrentPhase("foo-monthly-12345-evergreen");
+        assertEquals(phase2.getName(), "foo-monthly-12345-evergreen");
+    }
+
     @Test(groups = "fast")
     public void testAddNoTrialPlanOnFirstCatalog() throws CatalogApiException {
 
@@ -102,7 +135,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         assertEquals(priceList.getPlans().iterator().next().getName(), "foo-monthly");
     }
 
-
     @Test(groups = "fast")
     public void testAddTrialPlanOnFirstCatalog() throws CatalogApiException {
 
@@ -149,8 +181,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         assertEquals(priceList.getPlans().iterator().next().getName(), "foo-monthly");
     }
 
-
-
     @Test(groups = "fast")
     public void testAddPlanOnExistingCatalog() throws Exception {
 
@@ -185,8 +215,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         assertEquals(priceList.getPlans().size(), 4);
     }
 
-
-
     @Test(groups = "fast")
     public void testAddExistingPlanWithNewCurrency() throws Exception {
         final StandaloneCatalog originalCatalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
@@ -231,7 +259,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         SimplePlanDescriptor desc = new DefaultSimplePlanDescriptor("standard-monthly", "Standard", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 0, TimeUnit.DAYS, ImmutableList.<String>of());
         addBadSimplePlanDescriptor(catalogUpdater, desc);
 
-
         // Existing Plan has a 30 days trial => try different trial length
         desc = new DefaultSimplePlanDescriptor("standard-monthly", "Standard", ProductCategory.BASE, Currency.EUR, BigDecimal.TEN, BillingPeriod.MONTHLY, 14, TimeUnit.DAYS, ImmutableList.<String>of());
         addBadSimplePlanDescriptor(catalogUpdater, desc);
@@ -257,11 +284,9 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         addBadSimplePlanDescriptor(catalogUpdater, desc);
     }
 
-
     @Test(groups = "fast")
     public void testPlanWithNonFinalFixedTermPhase() throws Exception {
 
-
         final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarBasic.xml").toExternalForm(), StandaloneCatalog.class);
 
         final MutableStaticCatalog mutableCatalog = new DefaultMutableStaticCatalog(catalog);
@@ -272,7 +297,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         newProduct.initialize((StandaloneCatalog) mutableCatalog, null);
         mutableCatalog.addProduct(newProduct);
 
-
         final DefaultPlanPhase trialPhase = new DefaultPlanPhase();
         trialPhase.setPhaseType(PhaseType.TRIAL);
         trialPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.DAYS).setNumber(14));
@@ -284,24 +308,22 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         fixedTermPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
         fixedTermPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
 
-
         final DefaultPlanPhase evergreenPhase = new DefaultPlanPhase();
         evergreenPhase.setPhaseType(PhaseType.EVERGREEN);
         evergreenPhase.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(1));
         evergreenPhase.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
 
-
         final DefaultPlan newPlan = new DefaultPlan();
         newPlan.setName("something-with-fixed-term");
         newPlan.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
         newPlan.setProduct(newProduct);
-        newPlan.setInitialPhases(new DefaultPlanPhase[] {trialPhase, fixedTermPhase});
+        newPlan.setInitialPhases(new DefaultPlanPhase[]{trialPhase, fixedTermPhase});
         newPlan.setFinalPhase(fixedTermPhase);
         mutableCatalog.addPlan(newPlan);
         newPlan.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
 
         final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
-        final StandaloneCatalog newCatalog =  XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
+        final StandaloneCatalog newCatalog = XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
 
         final DefaultPlan targetPlan = newCatalog.findCurrentPlan("something-with-fixed-term");
         Assert.assertEquals(targetPlan.getInitialPhases().length, 2);
@@ -309,8 +331,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
 
     }
 
-
-
     @Test(groups = "fast")
     public void testVerifyXML() throws Exception {
 
@@ -559,7 +579,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         System.err.println(catalogUpdater.getCatalogXML());
     }
 
-
     private StandaloneCatalog enhanceOriginalCatalogForInvalidTestCases(final String catalogName) throws Exception {
 
         final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource(catalogName).toExternalForm(), StandaloneCatalog.class);
@@ -592,7 +611,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         mutableCatalog.addPlan(newPlan1);
         newPlan1.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
 
-
         final DefaultProduct newProduct2 = new DefaultProduct();
         newProduct2.setName("SuperDynamic");
         newProduct2.setCatagory(ProductCategory.BASE);
@@ -605,7 +623,6 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         fixedterm2.setDuration(new DefaultDuration().setUnit(TimeUnit.MONTHS).setNumber(3));
         fixedterm2.setRecurring(new DefaultRecurring().setBillingPeriod(BillingPeriod.MONTHLY).setRecurringPrice(new DefaultInternationalPrice().setPrices(new DefaultPrice[]{new DefaultPrice().setCurrency(Currency.USD).setValue(BigDecimal.TEN)})));
 
-
         final DefaultPlan newPlan2 = new DefaultPlan();
         newPlan2.setName("superdynamic-fixedterm");
         newPlan2.setPriceListName(DefaultPriceListSet.DEFAULT_PRICELIST_NAME);
@@ -614,12 +631,10 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
         mutableCatalog.addPlan(newPlan2);
         newPlan2.initialize((StandaloneCatalog) mutableCatalog, new URI("dummy"));
 
-
         final String newCatalogStr = XMLWriter.writeXML((StandaloneCatalog) mutableCatalog, StandaloneCatalog.class);
         return XMLLoader.getObjectFromStream(new URI("dummy"), new ByteArrayInputStream(newCatalogStr.getBytes(Charset.forName("UTF-8"))), StandaloneCatalog.class);
     }
 
-
     private void addBadSimplePlanDescriptor(final CatalogUpdater catalogUpdater, final SimplePlanDescriptor desc) {
         try {
             catalogUpdater.addSimplePlanDescriptor(desc);
@@ -628,4 +643,4 @@ public class TestCatalogUpdater extends CatalogTestSuiteNoDB {
             assertEquals(e.getCode(), ErrorCode.CAT_FAILED_SIMPLE_PLAN_VALIDATION.getCode());
         }
     }
-}
\ No newline at end of file
+}
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
index 2a70ca6..218aeda 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/TestStandaloneCatalogWithPriceOverride.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -17,19 +17,66 @@
 
 package org.killbill.billing.catalog;
 
+import java.math.BigDecimal;
+import java.util.regex.Matcher;
+
 import org.killbill.billing.ErrorCode;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.CatalogApiException;
+import org.killbill.billing.catalog.api.Currency;
+import org.killbill.billing.catalog.api.Plan;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
+import org.killbill.billing.catalog.api.PlanPhasePriceOverridesWithCallContext;
 import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.StaticCatalog;
+import org.killbill.billing.catalog.override.DefaultPriceOverride;
 import org.killbill.xmlloader.XMLLoader;
+import org.mockito.Mockito;
 import org.testng.Assert;
 import org.testng.annotations.Test;
 
+import com.google.common.collect.ImmutableList;
 import com.google.common.io.Resources;
 
 public class TestStandaloneCatalogWithPriceOverride extends CatalogTestSuiteWithEmbeddedDB {
 
+    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/842")
+    public void testCreateAmbiguousPlan() throws Exception {
+        final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
+        final StaticCatalog standaloneCatalogWithPriceOverride = new StandaloneCatalogWithPriceOverride(catalog,
+                                                                                                        priceOverride,
+                                                                                                        internalCallContext.getTenantRecordId(),
+                                                                                                        internalCallContextFactory);
+
+        // Create ambiguous plan name
+        final PlanSpecifier spec = new PlanSpecifier("standard-monthly-67890");
+        final PlanPhasePriceOverridesWithCallContext overrides = Mockito.mock(PlanPhasePriceOverridesWithCallContext.class);
+        Mockito.when(overrides.getCallContext()).thenReturn(callContext);
+        final PlanPhasePriceOverride override = new DefaultPlanPhasePriceOverride("standard-monthly-evergreen", Currency.USD, null, BigDecimal.ONE);
+        Mockito.when(overrides.getOverrides()).thenReturn(ImmutableList.of(override));
+        final Plan plan = standaloneCatalogWithPriceOverride.createOrFindCurrentPlan(spec, overrides);
+        Assert.assertTrue(plan.getName().startsWith("standard-monthly-67890-"));
+        final Matcher m = DefaultPriceOverride.CUSTOM_PLAN_NAME_PATTERN.matcher(plan.getName());
+        Assert.assertTrue(m.matches());
+
+        // From the catalog
+        Assert.assertNotNull(catalog.findCurrentPlan("standard-monthly"));
+        Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly"));
+
+        // Created on the fly
+        try {
+            catalog.findCurrentPlan(plan.getName());
+            Assert.fail();
+        } catch (final CatalogApiException e) {
+            Assert.assertEquals(e.getCode(), ErrorCode.CAT_NO_SUCH_PLAN.getCode());
+        }
+        Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly-1"));
+
+        // From the catalog
+        Assert.assertNotNull(catalog.findCurrentPlan("standard-monthly-12345"));
+        Assert.assertNotNull(standaloneCatalogWithPriceOverride.findCurrentPlan("standard-monthly-12345"));
+    }
+
     @Test(groups = "slow")
     public void testCreatePlanNoProduct() throws Exception {
         final StandaloneCatalog catalog = XMLLoader.getObjectFromString(Resources.getResource("SpyCarAdvanced.xml").toExternalForm(), StandaloneCatalog.class);
diff --git a/catalog/src/test/resources/catalogTest.xml b/catalog/src/test/resources/catalogTest.xml
index f8d9f93..e4969b1 100644
--- a/catalog/src/test/resources/catalogTest.xml
+++ b/catalog/src/test/resources/catalogTest.xml
@@ -1,8 +1,10 @@
 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
 <!--
   ~ Copyright 2010-2013 Ning, Inc.
+  ~ Copyright 2014-2018 Groupon, Inc
+  ~ Copyright 2014-2018 The Billing Project, LLC
   ~
-  ~ Ning licenses this file to you under the Apache License, version 2.0
+  ~ The Billing Project licenses this file to you under the Apache License, version 2.0
   ~ (the "License"); you may not use this file except in compliance with the
   ~ License.  You may obtain a copy of the License at:
   ~
@@ -42,6 +44,9 @@
     </units>
 
     <products>
+        <product name="Knife">
+            <category>STANDALONE</category>
+        </product>
         <product name="Blowdart">
             <category>BASE</category>
         </product>
@@ -191,6 +196,31 @@
     </rules>
 
     <plans>
+        <plan name="knife-monthly-notrial">
+            <product>Knife</product>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <recurring>
+                    <billingPeriod>MONTHLY</billingPeriod>
+                    <recurringPrice>
+                        <price>
+                            <currency>USD</currency>
+                            <value>29.95</value>
+                        </price>
+                        <price>
+                            <currency>EUR</currency>
+                            <value>29.95</value>
+                        </price>
+                        <price>
+                            <currency>GBP</currency>
+                            <value>29.95</value>
+                        </price>
+                    </recurringPrice>
+                </recurring>
+            </finalPhase>
+        </plan>
         <plan name="blowdart-monthly-notrial">
             <product>Blowdart</product>
             <finalPhase type="EVERGREEN">
@@ -216,7 +246,6 @@
                 </recurring>
             </finalPhase>
         </plan>
-
         <plan name="pistol-monthly-notrial">
             <product>Pistol</product>
             <finalPhase type="EVERGREEN">
@@ -1373,6 +1402,7 @@
         </childPriceList>
         <childPriceList name="notrial">
             <plans>
+                <plan>knife-monthly-notrial</plan>
                 <plan>blowdart-monthly-notrial</plan>
                 <plan>pistol-monthly-notrial</plan>
             </plans>
diff --git a/catalog/src/test/resources/SpyCarAdvanced.xml b/catalog/src/test/resources/SpyCarAdvanced.xml
index a08c425..bfc21c4 100644
--- a/catalog/src/test/resources/SpyCarAdvanced.xml
+++ b/catalog/src/test/resources/SpyCarAdvanced.xml
@@ -247,6 +247,39 @@
                 </recurring>
             </finalPhase>
         </plan>
+        <plan name="standard-monthly-12345">
+            <product>Standard</product>
+            <finalPhase type="EVERGREEN">
+                <duration>
+                    <unit>UNLIMITED</unit>
+                </duration>
+                <recurring>
+                    <billingPeriod>MONTHLY</billingPeriod>
+                    <recurringPrice>
+                        <price>
+                            <currency>GBP</currency>
+                            <value>75.00</value>
+                        </price>
+                        <price>
+                            <currency>EUR</currency>
+                            <value>85.00</value>
+                        </price>
+                        <price>
+                            <currency>USD</currency>
+                            <value>100.00</value>
+                        </price>
+                        <price>
+                            <currency>JPY</currency>
+                            <value>10.00</value>
+                        </price>
+                        <price>
+                            <currency>BTC</currency>
+                            <value>0.1</value>
+                        </price>
+                    </recurringPrice>
+                </recurring>
+            </finalPhase>
+        </plan>
         <plan name="sports-monthly">
             <product>Sports</product>
             <initialPhases>
@@ -967,5 +1000,10 @@
                 <plan>cia-sports-monthly</plan>
             </plans>
         </childPriceList>
+        <childPriceList name="ambiguous">
+            <plans>
+                <plan>standard-monthly-12345</plan>
+            </plans>
+        </childPriceList>
     </priceLists>
 </catalog>
diff --git a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
index 7c84dac..878f1a0 100644
--- a/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
+++ b/entitlement/src/main/java/org/killbill/billing/entitlement/engine/core/DefaultEventsStream.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2016 Groupon, Inc
- * Copyright 2014-2016 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -111,7 +111,8 @@ public class DefaultEventsStream implements EventsStream {
                               @Nullable final SubscriptionBaseBundle bundle,
                               @Nullable final SubscriptionBase baseSubscription,
                               @Nullable final SubscriptionBase subscription) {
-        for (final Object object : new Object[]{account, bundle, baseSubscription, subscription}) {
+        // baseSubscription can be null for STANDALONE products (https://github.com/killbill/killbill/issues/840)
+        for (final Object object : new Object[]{account, bundle, subscription}) {
             Preconditions.checkNotNull(object,
                                        "accountId='%s', bundleId='%s', baseSubscriptionId='%s', subscriptionId='%s'",
                                        account != null ? account.getId() : null,
diff --git a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
index c5cc731..a3175f3 100644
--- a/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
+++ b/entitlement/src/test/java/org/killbill/billing/entitlement/api/TestDefaultEntitlement.java
@@ -24,13 +24,11 @@ import org.killbill.billing.ErrorCode;
 import org.killbill.billing.account.api.Account;
 import org.killbill.billing.account.api.AccountApiException;
 import org.killbill.billing.api.TestApiListener.NextEvent;
-import org.killbill.billing.catalog.api.BillingActionPolicy;
 import org.killbill.billing.catalog.api.BillingPeriod;
 import org.killbill.billing.catalog.api.PlanPhasePriceOverride;
 import org.killbill.billing.catalog.api.PlanPhaseSpecifier;
 import org.killbill.billing.catalog.api.PlanSpecifier;
 import org.killbill.billing.catalog.api.PriceListSet;
-import org.killbill.billing.catalog.api.ProductCategory;
 import org.killbill.billing.entitlement.EntitlementTestSuiteWithEmbeddedDB;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementActionPolicy;
 import org.killbill.billing.entitlement.api.Entitlement.EntitlementSourceType;
@@ -140,6 +138,39 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
         assertEquals(entitlement3.getState(), EntitlementState.ACTIVE);
     }
 
+    @Test(groups = "slow", description = "https://github.com/killbill/killbill/issues/840")
+    public void testUncancelEntitlementFor_STANDALONE_Product() throws AccountApiException, EntitlementApiException {
+        final LocalDate initialDate = new LocalDate(2013, 8, 7);
+        clock.setDay(initialDate);
+
+        final Account account = createAccount(getAccountData(7));
+
+        final PlanPhaseSpecifier spec = new PlanPhaseSpecifier("Knife", BillingPeriod.MONTHLY, "notrial", null);
+
+        // Create entitlement and check each field
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
+        final Entitlement entitlement = entitlementApi.createBaseEntitlement(account.getId(), spec, account.getExternalKey(), null, null, null, false, ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+        assertEquals(entitlement.getState(), EntitlementState.ACTIVE);
+
+        clock.addDays(5);
+
+        final LocalDate cancelDate = new LocalDate(clock.getUTCToday().plusDays(1));
+        entitlement.cancelEntitlementWithDate(cancelDate, true, ImmutableList.<PluginProperty>of(), callContext);
+
+        final Entitlement entitlement2 = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
+        assertEquals(entitlement2.getState(), EntitlementState.ACTIVE);
+        assertEquals(entitlement2.getEffectiveEndDate(), cancelDate);
+
+        testListener.pushExpectedEvents(NextEvent.UNCANCEL);
+        entitlement2.uncancelEntitlement(ImmutableList.<PluginProperty>of(), callContext);
+        assertListenerStatus();
+
+        clock.addDays(1);
+        final Entitlement entitlement3 = entitlementApi.getEntitlementForId(entitlement.getId(), callContext);
+        assertEquals(entitlement3.getState(), EntitlementState.ACTIVE);
+    }
+
     @Test(groups = "slow")
     public void testCancelWithEntitlementPolicyEOTAndNOCTD() throws AccountApiException, EntitlementApiException, SubscriptionApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
@@ -283,11 +314,10 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
     }
 
     @Test(groups = "slow")
-        public void testEntitlementChangePlanOnPendingEntitlement() throws AccountApiException, EntitlementApiException {
+    public void testEntitlementChangePlanOnPendingEntitlement() throws AccountApiException, EntitlementApiException {
         final LocalDate initialDate = new LocalDate(2013, 8, 7);
         clock.setDay(initialDate);
 
-
         final LocalDate startDate = initialDate.plusDays(10);
 
         final Account account = accountApi.createAccount(getAccountData(7), callContext);
@@ -316,7 +346,7 @@ public class TestDefaultEntitlement extends EntitlementTestSuiteWithEmbeddedDB {
 
         entitlement.changePlanWithDate(spec2, ImmutableList.<PlanPhasePriceOverride>of(), startDate, ImmutableList.<PluginProperty>of(), callContext);
 
-        testListener.pushExpectedEvents(NextEvent.CREATE,  NextEvent.BLOCK);
+        testListener.pushExpectedEvents(NextEvent.CREATE, NextEvent.BLOCK);
         clock.addDays(10);
         assertListenerStatus();
 
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
index f85946b..6e84d68 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/AdminResource.java
@@ -1,6 +1,6 @@
 /*
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -17,6 +17,7 @@
 
 package org.killbill.billing.jaxrs.resources;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.net.URI;
@@ -300,8 +301,13 @@ public class AdminResource extends JaxRsResourceBase {
                     generator.close();
                 } finally {
                     // In case the client goes away (IOException), make sure to close the underlying DB connection
-                    while (iterator.hasNext()) {
-                        iterator.next();
+                    if (tags instanceof Closeable) {
+                        ((Closeable) tags).close();
+                    } else {
+                        // TODO 0.20.x (https://github.com/killbill/killbill/issues/558)
+                        while (iterator.hasNext()) {
+                            iterator.next();
+                        }
                     }
                 }
             }
diff --git a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
index 0fc62db..8a45b83 100644
--- a/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
+++ b/jaxrs/src/main/java/org/killbill/billing/jaxrs/resources/JaxRsResourceBase.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -18,6 +18,7 @@
 
 package org.killbill.billing.jaxrs.resources;
 
+import java.io.Closeable;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.math.BigDecimal;
@@ -349,8 +350,13 @@ public abstract class JaxRsResourceBase implements JaxrsResource {
                     generator.close();
                 } finally {
                     // In case the client goes away (IOException), make sure to close the underlying DB connection
-                    while (iterator.hasNext()) {
-                        iterator.next();
+                    if (entities instanceof Closeable) {
+                        ((Closeable) entities).close();
+                    } else {
+                        // TODO 0.20.x (https://github.com/killbill/killbill/issues/558)
+                        while (iterator.hasNext()) {
+                            iterator.next();
+                        }
                     }
                 }
             }
diff --git a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
index 7f059fc..fcb43c1 100644
--- a/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
+++ b/util/src/main/java/org/killbill/billing/util/cache/EhCacheBasedCacheController.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2012 Ning, Inc.
- * Copyright 2014-2017 Groupon, Inc
- * Copyright 2014-2017 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -128,7 +128,8 @@ public class EhCacheBasedCacheController<K, V> implements CacheController<K, V> 
         try {
             value = baseCacheLoader.compute(key, cacheLoaderArgument);
         } catch (final Exception e) {
-            logger.warn("Unable to compute cached value for key='{}' and cacheLoaderArgument='{}'", key, cacheLoaderArgument, e);
+            // Remove noisy log (might be expected, see https://github.com/killbill/killbill/issues/842)
+            //logger.warn("Unable to compute cached value for key='{}' and cacheLoaderArgument='{}'", key, cacheLoaderArgument, e);
             throw new RuntimeException(e);
         }
 
diff --git a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
index 574d3b0..d359a19 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/dao/DefaultPaginationSqlDaoHelper.java
@@ -1,7 +1,7 @@
 /*
  * Copyright 2010-2014 Ning, Inc.
- * Copyright 2014-2015 Groupon, Inc
- * Copyright 2014-2015 The Billing Project, LLC
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
  * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
@@ -18,6 +18,8 @@
 
 package org.killbill.billing.util.entity.dao;
 
+import java.io.Closeable;
+import java.io.IOException;
 import java.util.Iterator;
 
 import javax.annotation.Nullable;
@@ -26,9 +28,13 @@ import org.killbill.billing.callcontext.InternalTenantContext;
 import org.killbill.billing.util.entity.DefaultPagination;
 import org.killbill.billing.util.entity.Entity;
 import org.killbill.billing.util.entity.Pagination;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 public class DefaultPaginationSqlDaoHelper {
 
+    private static final Logger logger = LoggerFactory.getLogger(DefaultPaginationSqlDaoHelper.class);
+
     // Number large enough so that small installations have access to an accurate count
     // but small enough to not impact very large deployments
     // TODO Should this be configurable per tenant?
@@ -66,14 +72,35 @@ public class DefaultPaginationSqlDaoHelper {
         // 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.onDemandForStreamingResults(sqlDaoClazz);
-        // The count to get maxNbRecords can be expensive on very large datasets. As a heuristic to check how large that number is,
-        // we retrieve 1 record at offset SIMPLE_PAGINATION_THRESHOLD (pretty fast). If we've found a record, that means the count is larger
-        // than this threshold and we don't issue the full count query
         final Long maxNbRecords;
-        if (context == null || paginationIteratorBuilder.build((S) sqlDao, SIMPLE_PAGINATION_THRESHOLD, 1L, ordering, context).hasNext()) {
+        if (context == null) {
             maxNbRecords = null;
         } else {
-            maxNbRecords = sqlDao.getCount(context);
+            // The count to get maxNbRecords can be expensive on very large datasets. As a heuristic to check how large that number is,
+            // we retrieve 1 record at offset SIMPLE_PAGINATION_THRESHOLD (pretty fast). If we've found a record, that means the count is larger
+            // than this threshold and we don't issue the full count query
+            final Iterator<M> simplePaginationIterator = paginationIteratorBuilder.build((S) sqlDao, SIMPLE_PAGINATION_THRESHOLD, 1L, ordering, context);
+            final boolean veryLargeDataSet = simplePaginationIterator.hasNext();
+
+            // Make sure to free resources (https://github.com/killbill/killbill/issues/853)
+            if (simplePaginationIterator instanceof Closeable) {
+                // Always the case with the current implementation (simplePaginationIterator is a org.skife.jdbi.v2.ResultIterator)
+                try {
+                    ((Closeable) simplePaginationIterator).close();
+                } catch (final IOException e) {
+                    logger.warn("Unable to close iterator", e);
+                }
+            } else {
+                while (simplePaginationIterator.hasNext()) {
+                    simplePaginationIterator.next();
+                }
+            }
+
+            if (veryLargeDataSet) {
+                maxNbRecords = null;
+            } else {
+                maxNbRecords = sqlDao.getCount(context);
+            }
         }
         final Iterator<M> results = paginationIteratorBuilder.build((S) sqlDao, offset, limit, ordering, context);
 
diff --git a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
index bdd5e16..7ea60aa 100644
--- a/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
+++ b/util/src/main/java/org/killbill/billing/util/entity/DefaultPagination.java
@@ -1,7 +1,9 @@
 /*
  * Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2018 Groupon, Inc
+ * Copyright 2014-2018 The Billing Project, LLC
  *
- * Ning licenses this file to you under the Apache License, version 2.0
+ * The Billing Project licenses this file to you under the Apache License, version 2.0
  * (the "License"); you may not use this file except in compliance with the
  * License.  You may obtain a copy of the License at:
  *
@@ -16,6 +18,8 @@
 
 package org.killbill.billing.util.entity;
 
+import java.io.Closeable;
+import java.io.IOException;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
@@ -25,7 +29,7 @@ import javax.annotation.Nullable;
 import com.google.common.collect.ImmutableList;
 
 // Assumes the original offset starts at zero.
-public class DefaultPagination<T> implements Pagination<T> {
+public class DefaultPagination<T> implements Pagination<T>, Closeable {
 
     private final Long currentOffset;
     private final Long limit;
@@ -75,6 +79,18 @@ public class DefaultPagination<T> implements Pagination<T> {
     }
 
     @Override
+    public void close() throws IOException {
+        if (delegateIterator instanceof Closeable) {
+            // Always the case with the current implementation (delegateIterator is a org.skife.jdbi.v2.ResultIterator)
+            ((Closeable) delegateIterator).close();
+        } else {
+            while (delegateIterator.hasNext()) {
+                delegateIterator.next();
+            }
+        }
+    }
+
+    @Override
     public Iterator<T> iterator() {
         return delegateIterator;
     }