killbill-memoizeit
Changes
catalog/src/test/resources/com/acme/SpyCarCustom.xml 189(+189 -0)
invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java 2(+1 -1)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java 85(+55 -30)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java 28(+25 -3)
invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java 8(+2 -6)
invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java 197(+189 -8)
pom.xml 2(+1 -1)
profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java 7(+5 -2)
profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java 4(+2 -2)
util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java 12(+6 -6)
Details
diff --git a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
index f3a8a7a..f71adbf 100644
--- a/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
+++ b/beatrix/src/test/java/org/killbill/billing/beatrix/integration/TestWithTaxItems.java
@@ -194,7 +194,7 @@ public class TestWithTaxItems extends TestIntegrationBase {
}
@Override
- public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
+ public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> pluginProperties, final CallContext callContext) {
return addTaxItem.compareAndSet(true, false) ? ImmutableList.<InvoiceItem>of(createTaxInvoiceItem(invoice)) : ImmutableList.<InvoiceItem>of();
}
@@ -205,5 +205,6 @@ public class TestWithTaxItems extends TestIntegrationBase {
public void addTaxItem() {
this.addTaxItem.set(true);
}
+
}
}
diff --git a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
index 5d0e820..3404ec7 100644
--- a/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
+++ b/catalog/src/main/java/org/killbill/billing/catalog/io/VersionedCatalogLoader.java
@@ -63,20 +63,7 @@ public class VersionedCatalogLoader implements CatalogLoader {
List<URI> xmlURIs;
if (uriString.endsWith(XML_EXTENSION)) { // Assume its an xml file
xmlURIs = new ArrayList<URI>();
- URI uri = new URI(uriString);
-
- // Try to expand the full path, if possible
- final String schemeSpecificPart = uri.getSchemeSpecificPart();
- if (schemeSpecificPart != null) {
- final String[] split = schemeSpecificPart.split("/");
- final String fileName = split[split.length - 1];
- try {
- uri = new URI(Resources.getResource(fileName).toExternalForm());
- } catch (IllegalArgumentException ignored) {
- }
- }
-
- xmlURIs.add(uri);
+ xmlURIs.add(new URI(uriString));
} else { // Assume its a directory
final String directoryContents = UriAccessor.accessUriAsString(uriString);
xmlURIs = findXmlReferences(directoryContents, new URL(uriString));
diff --git a/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
index d6f212e..ef0d645 100644
--- a/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
+++ b/catalog/src/test/java/org/killbill/billing/catalog/io/TestVersionedCatalogLoader.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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.catalog.io;
+import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
@@ -39,6 +40,7 @@ import org.testng.Assert;
import org.testng.annotations.Test;
import org.xml.sax.SAXException;
+import com.google.common.io.Files;
import com.google.common.io.Resources;
public class TestVersionedCatalogLoader extends CatalogTestSuiteNoDB {
@@ -126,4 +128,42 @@ public class TestVersionedCatalogLoader extends CatalogTestSuiteNoDB {
dt = new DateTime("2011-03-03T00:00:00+00:00");
Assert.assertEquals(it.next().getEffectiveDate(), dt.toDate());
}
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromClasspathResourceFolder() throws CatalogApiException {
+ final VersionedCatalog c = loader.loadDefaultCatalog("SpyCarBasic.xml");
+ Assert.assertEquals(c.size(), 1);
+ final DateTime dt = new DateTime("2013-02-08T00:00:00+00:00");
+ Assert.assertEquals(c.getEffectiveDate(), dt.toDate());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarBasic");
+ }
+
+ @Test(groups = "fast", expectedExceptions = CatalogApiException.class)
+ public void testLoadCatalogFromClasspathResourceBadFolder() throws CatalogApiException {
+ loader.loadDefaultCatalog("SpyCarCustom.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromInsideResourceFolder() throws CatalogApiException, URISyntaxException, IOException {
+ final VersionedCatalog c = loader.loadDefaultCatalog("com/acme/SpyCarCustom.xml");
+ Assert.assertEquals(c.size(), 1);
+ final DateTime dt = new DateTime("2015-10-04T00:00:00+00:00");
+ Assert.assertEquals(c.getEffectiveDate(), dt.toDate());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarCustom");
+ }
+
+ @Test(groups = "fast", expectedExceptions = CatalogApiException.class)
+ public void testLoadCatalogFromInsideResourceWithBadFolderName() throws CatalogApiException {
+ loader.loadDefaultCatalog("com/acme2/SpyCarCustom.xml");
+ }
+
+ @Test(groups = "fast")
+ public void testLoadCatalogFromExternalFile() throws CatalogApiException, IOException, URISyntaxException {
+ final File originFile = new File(Resources.getResource("SpyCarBasic.xml").toURI());
+ final File destinationFile = new File(Files.createTempDir().toString() + "/SpyCarBasicRelocated.xml");
+ destinationFile.deleteOnExit();
+ Files.copy(originFile, destinationFile);
+ final VersionedCatalog c = loader.loadDefaultCatalog(destinationFile.toURI().toString());
+ Assert.assertEquals(c.getCatalogName(), "SpyCarBasic");
+ }
}
catalog/src/test/resources/com/acme/SpyCarCustom.xml 189(+189 -0)
diff --git a/catalog/src/test/resources/com/acme/SpyCarCustom.xml b/catalog/src/test/resources/com/acme/SpyCarCustom.xml
new file mode 100644
index 0000000..2d29b02
--- /dev/null
+++ b/catalog/src/test/resources/com/acme/SpyCarCustom.xml
@@ -0,0 +1,189 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!--
+ ~ Copyright 2014-2015 Groupon, Inc
+ ~ Copyright 2014-2015 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
+ ~ 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.
+ -->
+
+<catalog xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:noNamespaceSchemaLocation="CatalogSchema.xsd ">
+
+ <effectiveDate>2015-10-04T00:00:00+00:00</effectiveDate>
+ <catalogName>SpyCarCustom</catalogName>
+
+ <recurringBillingMode>IN_ADVANCE</recurringBillingMode>
+
+ <currencies>
+ <currency>USD</currency>
+ <currency>GBP</currency>
+ </currencies>
+
+ <products>
+ <product name="Standard">
+ <category>BASE</category>
+ </product>
+ <product name="Sports">
+ <category>BASE</category>
+ </product>
+ <product name="Super">
+ <category>BASE</category>
+ </product>
+ </products>
+
+ <rules>
+ <changePolicy>
+ <changePolicyCase>
+ <policy>IMMEDIATE</policy>
+ </changePolicyCase>
+ </changePolicy>
+ <changeAlignment>
+ <changeAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </changeAlignmentCase>
+ </changeAlignment>
+ <cancelPolicy>
+ <cancelPolicyCase>
+ <policy>IMMEDIATE</policy>
+ </cancelPolicyCase>
+ </cancelPolicy>
+ <createAlignment>
+ <createAlignmentCase>
+ <alignment>START_OF_BUNDLE</alignment>
+ </createAlignmentCase>
+ </createAlignment>
+ <billingAlignment>
+ <billingAlignmentCase>
+ <alignment>ACCOUNT</alignment>
+ </billingAlignmentCase>
+ </billingAlignment>
+ <priceList>
+ <priceListCase>
+ <toPriceList>DEFAULT</toPriceList>
+ </priceListCase>
+ </priceList>
+ </rules>
+
+ <plans>
+ <plan name="standard-monthly">
+ <product>Standard</product>
+ <initialPhases>
+ <phase type="TRIAL">
+ <duration>
+ <unit>DAYS</unit>
+ <number>30</number>
+ </duration>
+ <fixed>
+ <fixedPrice> <!-- empty price implies $0 -->
+ </fixedPrice>
+
+ </fixed>
+ </phase>
+ </initialPhases>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>GBP</currency>
+ <value>75.00</value>
+ </price>
+ <price>
+ <currency>USD</currency>
+ <value>111.00</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
+ <plan name="sports-monthly">
+ <product>Sports</product>
+ <initialPhases>
+ <phase type="TRIAL">
+ <duration>
+ <unit>DAYS</unit>
+ <number>30</number>
+ </duration>
+ <fixed>
+ <fixedPrice> <!-- empty price implies $0 -->
+ </fixedPrice>
+ </fixed>
+ </phase>
+ </initialPhases>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>GBP</currency>
+ <value>375.00</value>
+ </price>
+ <price>
+ <currency>USD</currency>
+ <value>511.00</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
+ <plan name="super-monthly">
+ <product>Super</product>
+ <initialPhases>
+ <phase type="TRIAL">
+ <duration>
+ <unit>DAYS</unit>
+ <number>30</number>
+ </duration>
+ <fixed>
+ <fixedPrice> <!-- empty price implies $0 -->
+ </fixedPrice>
+
+ </fixed>
+ </phase>
+ </initialPhases>
+ <finalPhase type="EVERGREEN">
+ <duration>
+ <unit>UNLIMITED</unit>
+ </duration>
+ <recurring>
+ <billingPeriod>MONTHLY</billingPeriod>
+ <recurringPrice>
+ <price>
+ <currency>GBP</currency>
+ <value>750.00</value>
+ </price>
+ <price>
+ <currency>USD</currency>
+ <value>1111.00</value>
+ </price>
+ </recurringPrice>
+ </recurring>
+ </finalPhase>
+ </plan>
+ </plans>
+ <priceLists>
+ <defaultPriceList name="DEFAULT">
+ <plans>
+ <plan>standard-monthly</plan>
+ <plan>sports-monthly</plan>
+ <plan>super-monthly</plan>
+ </plans>
+ </defaultPriceList>
+ </priceLists>
+</catalog>
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
index 30b968e..b6d3059 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/InvoiceApiHelper.java
@@ -73,7 +73,7 @@ public class InvoiceApiHelper {
this.internalCallContextFactory = internalCallContextFactory;
}
- public List<InvoiceItem> dispatchToInvoicePluginsAndInsertItems(final UUID accountId, final WithAccountLock withAccountLock, final CallContext context) throws InvoiceApiException {
+ public List<InvoiceItem> dispatchToInvoicePluginsAndInsertItems(final UUID accountId, final boolean isDryRun, final WithAccountLock withAccountLock, final CallContext context) throws InvoiceApiException {
GlobalLock lock = null;
try {
lock = locker.lockWithNumberOfTries(LockerType.ACCNT_INV_PAY.toString(), accountId.toString(), invoiceConfig.getMaxGlobalLockRetries());
@@ -83,7 +83,7 @@ public class InvoiceApiHelper {
final List<InvoiceModelDao> invoiceModelDaos = new LinkedList<InvoiceModelDao>();
for (final Invoice invoiceForPlugin : invoicesForPlugins) {
// Call plugin
- final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, context);
+ final List<InvoiceItem> additionalInvoiceItems = invoicePluginDispatcher.getAdditionalInvoiceItems(invoiceForPlugin, isDryRun, context);
invoiceForPlugin.addInvoiceItems(additionalInvoiceItems);
// Transformation to InvoiceModelDao
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
index 07f788b..b3da4f0 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/svcs/DefaultInvoiceInternalApi.java
@@ -143,7 +143,7 @@ public class DefaultInvoiceInternalApi implements InvoiceInternalApi {
return ImmutableList.<Invoice>of(invoice);
}
};
- final List<InvoiceItem> createdInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, callContext);
+ final List<InvoiceItem> createdInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, callContext);
return new DefaultInvoicePayment(refund);
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
index dfe060f..0c2b5b3 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/api/user/DefaultInvoiceUserApi.java
@@ -309,7 +309,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
};
- return invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ return invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
}
@Override
@@ -365,7 +365,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
};
- final List<InvoiceItem> creditInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ final List<InvoiceItem> creditInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
Preconditions.checkState(creditInvoiceItems.size() == 1, "Should have created a single credit invoice item: " + creditInvoiceItems);
return creditInvoiceItems.get(0);
@@ -401,7 +401,7 @@ public class DefaultInvoiceUserApi implements InvoiceUserApi {
}
};
- final List<InvoiceItem> adjustmentInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, withAccountLock, context);
+ final List<InvoiceItem> adjustmentInvoiceItems = invoiceApiHelper.dispatchToInvoicePluginsAndInsertItems(accountId, false, withAccountLock, context);
Preconditions.checkState(adjustmentInvoiceItems.size() == 1, "Should have created a single adjustment item: " + adjustmentInvoiceItems);
return adjustmentInvoiceItems.get(0);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
index 49ec8d8..8ddc243 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoiceDispatcher.java
@@ -345,7 +345,7 @@ public class InvoiceDispatcher {
// Ask external invoice plugins if additional items (tax, etc) shall be added to the invoice
//
final CallContext callContext = buildCallContext(context);
- invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, callContext));
+ invoice.addInvoiceItems(invoicePluginDispatcher.getAdditionalInvoiceItems(invoice, isDryRun, callContext));
if (!isDryRun) {
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
index 396942e..d05b13a 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/InvoicePluginDispatcher.java
@@ -58,13 +58,13 @@ public class InvoicePluginDispatcher {
// If we have multiple plugins there is a question of plugin ordering and also a 'product' questions to decide whether
// subsequent plugins should have access to items added by previous plugins
//
- public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice originalInvoice, final CallContext callContext) throws InvoiceApiException {
+ public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice originalInvoice, final boolean isDryRun, final CallContext callContext) throws InvoiceApiException {
// We clone the original invoice so plugins don't remove/add items
final Invoice clonedInvoice = (Invoice) ((DefaultInvoice) originalInvoice).clone();
final List<InvoiceItem> additionalInvoiceItems = new LinkedList<InvoiceItem>();
final List<InvoicePluginApi> invoicePlugins = getInvoicePlugins();
for (final InvoicePluginApi invoicePlugin : invoicePlugins) {
- final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, ImmutableList.<PluginProperty>of(), callContext);
+ final List<InvoiceItem> items = invoicePlugin.getAdditionalInvoiceItems(clonedInvoice, isDryRun, ImmutableList.<PluginProperty>of(), callContext);
if (items != null) {
for (final InvoiceItem item : items) {
validateInvoiceItemFromPlugin(item, invoicePlugin);
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
index 826782c..6b2883d 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/provider/DefaultNoOpInvoiceProviderPlugin.java
@@ -38,7 +38,7 @@ public class DefaultNoOpInvoiceProviderPlugin implements NoOpInvoicePluginApi {
}
@Override
- public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, Iterable<PluginProperty> properties, CallContext context) {
+ public List<InvoiceItem> getAdditionalInvoiceItems(final Invoice invoice, final boolean isDryRun, final Iterable<PluginProperty> properties, CallContext context) {
return ImmutableList.<InvoiceItem>of();
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
index 8ae278a..7ebb8d2 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatter.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
*
@@ -17,24 +19,22 @@
package org.killbill.billing.invoice.template.formatters;
import java.math.BigDecimal;
-import java.text.NumberFormat;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
import java.util.UUID;
+import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.killbill.billing.callcontext.InternalTenantContext;
-import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
-import org.killbill.billing.tenant.api.TenantInternalApi;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import org.killbill.billing.catalog.api.Currency;
import org.killbill.billing.currency.api.CurrencyConversion;
import org.killbill.billing.currency.api.CurrencyConversionApi;
@@ -45,23 +45,24 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePayment;
import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
+import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
import org.killbill.billing.invoice.model.DefaultInvoice;
import org.killbill.billing.util.template.translation.TranslatorConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
-import com.google.common.base.Objects;
+import com.google.common.base.MoreObjects;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
-import static org.killbill.billing.util.DefaultAmountFormatter.round;
-
/**
* Format invoice fields
*/
public class DefaultInvoiceFormatter implements InvoiceFormatter {
- private final static Logger logger = LoggerFactory.getLogger(DefaultInvoiceFormatter.class);
+ private static final Logger logger = LoggerFactory.getLogger(DefaultInvoiceFormatter.class);
private final TranslatorConfig config;
private final Invoice invoice;
@@ -70,8 +71,11 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
private final CurrencyConversionApi currencyConversionApi;
private final InternalTenantContext context;
private final ResourceBundleFactory bundleFactory;
+ private final Map<java.util.Currency, Locale> currencyLocaleMap;
- public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory, final InternalTenantContext context) {
+ public DefaultInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale,
+ final CurrencyConversionApi currencyConversionApi, final ResourceBundleFactory bundleFactory,
+ final InternalTenantContext context, final Map<java.util.Currency, Locale> currencyLocaleMap) {
this.config = config;
this.invoice = invoice;
this.dateFormatter = DateTimeFormat.mediumDate().withLocale(locale);
@@ -79,11 +83,12 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
this.currencyConversionApi = currencyConversionApi;
this.bundleFactory = bundleFactory;
this.context = context;
+ this.currencyLocaleMap = currencyLocaleMap;
}
@Override
public Integer getInvoiceNumber() {
- return Objects.firstNonNull(invoice.getInvoiceNumber(), 0);
+ return MoreObjects.firstNonNull(invoice.getInvoiceNumber(), 0);
}
@Override
@@ -163,7 +168,7 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
@Override
public <T extends InvoiceItem> List<InvoiceItem> getInvoiceItems(final Class<T> clazz) {
- return Objects.firstNonNull(invoice.getInvoiceItems(clazz), ImmutableList.<InvoiceItem>of());
+ return MoreObjects.firstNonNull(invoice.getInvoiceItems(clazz), ImmutableList.<InvoiceItem>of());
}
@Override
@@ -183,7 +188,7 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
@Override
public List<InvoicePayment> getPayments() {
- return Objects.firstNonNull(invoice.getPayments(), ImmutableList.<InvoicePayment>of());
+ return MoreObjects.firstNonNull(invoice.getPayments(), ImmutableList.<InvoicePayment>of());
}
@Override
@@ -198,35 +203,55 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
@Override
public BigDecimal getChargedAmount() {
- return round(Objects.firstNonNull(invoice.getChargedAmount(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getChargedAmount(), BigDecimal.ZERO);
}
@Override
public BigDecimal getOriginalChargedAmount() {
- return round(Objects.firstNonNull(invoice.getOriginalChargedAmount(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getOriginalChargedAmount(), BigDecimal.ZERO);
}
@Override
public BigDecimal getBalance() {
- return round(Objects.firstNonNull(invoice.getBalance(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getBalance(), BigDecimal.ZERO);
}
@Override
public String getFormattedChargedAmount() {
- final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
- return number.format(getChargedAmount().doubleValue());
+ return getFormattedAmountByLocaleAndInvoiceCurrency(getChargedAmount());
}
@Override
public String getFormattedPaidAmount() {
- final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
- return number.format(getPaidAmount().doubleValue());
+ return getFormattedAmountByLocaleAndInvoiceCurrency(getPaidAmount());
}
@Override
public String getFormattedBalance() {
- final NumberFormat number = NumberFormat.getCurrencyInstance(locale);
- return number.format(getBalance().doubleValue());
+ return getFormattedAmountByLocaleAndInvoiceCurrency(getBalance());
+ }
+
+ // Returns the formatted amount with the correct currency symbol that is get from the invoice currency.
+ private String getFormattedAmountByLocaleAndInvoiceCurrency(final BigDecimal amount) {
+ final String invoiceCurrencyCode = invoice.getCurrency().toString();
+ final CurrencyUnit currencyUnit = CurrencyUnit.of(invoiceCurrencyCode);
+
+ final DecimalFormat numberFormatter = (DecimalFormat) DecimalFormat.getCurrencyInstance(locale);
+ final DecimalFormatSymbols dfs = numberFormatter.getDecimalFormatSymbols();
+ dfs.setInternationalCurrencySymbol(currencyUnit.getCurrencyCode());
+
+ try {
+ final java.util.Currency currency = java.util.Currency.getInstance(invoiceCurrencyCode);
+ dfs.setCurrencySymbol(currency.getSymbol(currencyLocaleMap.get(currency)));
+ } catch (final IllegalArgumentException e) {
+ dfs.setCurrencySymbol(currencyUnit.getSymbol(locale));
+ }
+
+ numberFormatter.setDecimalFormatSymbols(dfs);
+ numberFormatter.setMinimumFractionDigits(currencyUnit.getDefaultFractionDigits());
+ numberFormatter.setMaximumFractionDigits(currencyUnit.getDefaultFractionDigits());
+
+ return numberFormatter.format(amount.doubleValue());
}
@Override
@@ -244,7 +269,7 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
}
// If there were multiple payments (and refunds) we pick chose the last one
DateTime latestPaymentDate = null;
- final Iterator<InvoicePayment> paymentIterator = ((DefaultInvoice) invoice).getPayments().iterator();
+ final Iterator<InvoicePayment> paymentIterator = invoice.getPayments().iterator();
while (paymentIterator.hasNext()) {
final InvoicePayment cur = paymentIterator.next();
latestPaymentDate = latestPaymentDate != null && latestPaymentDate.isAfter(cur.getPaymentDate()) ?
@@ -253,12 +278,12 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
}
try {
final CurrencyConversion conversion = currencyConversionApi.getCurrencyConversion(currency, latestPaymentDate);
- for (Rate rate : conversion.getRates()) {
+ for (final Rate rate : conversion.getRates()) {
if (rate.getCurrency() == getCurrency()) {
return rate.getValue().toString();
}
}
- } catch (CurrencyConversionException e) {
+ } catch (final CurrencyConversionException e) {
logger.warn("Failed to retrieve currency conversion rates for currency = " + currency + " and date = " + latestPaymentDate, e);
return null;
}
@@ -288,7 +313,7 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
@Override
public BigDecimal getPaidAmount() {
- return round(Objects.firstNonNull(invoice.getPaidAmount(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getPaidAmount(), BigDecimal.ZERO);
}
@Override
@@ -339,11 +364,11 @@ public class DefaultInvoiceFormatter implements InvoiceFormatter {
@Override
public BigDecimal getCreditedAmount() {
- return round(Objects.firstNonNull(invoice.getCreditedAmount(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getCreditedAmount(), BigDecimal.ZERO);
}
@Override
public BigDecimal getRefundedAmount() {
- return round(Objects.firstNonNull(invoice.getRefundedAmount(), BigDecimal.ZERO));
+ return MoreObjects.firstNonNull(invoice.getRefundedAmount(), BigDecimal.ZERO);
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
index 890fb30..b9e405b 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceFormatterFactory.java
@@ -16,7 +16,10 @@
package org.killbill.billing.invoice.template.formatters;
+import java.util.Currency;
+import java.util.HashMap;
import java.util.Locale;
+import java.util.Map;
import org.killbill.billing.callcontext.InternalTenantContext;
import org.killbill.billing.currency.api.CurrencyConversionApi;
@@ -24,14 +27,33 @@ import org.killbill.billing.invoice.api.Invoice;
import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory;
-import org.killbill.billing.tenant.api.TenantInternalApi;
import org.killbill.billing.util.template.translation.TranslatorConfig;
+import com.google.common.annotations.VisibleForTesting;
+
public class DefaultInvoiceFormatterFactory implements InvoiceFormatterFactory {
+ private final Map<Currency, Locale> currencyLocaleMap = new HashMap<Currency, Locale>();
+
+ public DefaultInvoiceFormatterFactory() {
+ // This initialization relies on System.currentTimeMillis() instead of the Kill Bill clock (it won't be accurate when moving the clock)
+ for (final Locale localeItem : Locale.getAvailableLocales()) {
+ try {
+ final java.util.Currency currency = java.util.Currency.getInstance(localeItem);
+ currencyLocaleMap.put(currency, localeItem);
+ } catch (final Exception ignored) {
+ }
+ }
+ }
+
@Override
- public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, CurrencyConversionApi currencyConversionApi,
+ public InvoiceFormatter createInvoiceFormatter(final TranslatorConfig config, final Invoice invoice, final Locale locale, final CurrencyConversionApi currencyConversionApi,
final ResourceBundleFactory bundleFactory, final InternalTenantContext context) {
- return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context);
+ return new DefaultInvoiceFormatter(config, invoice, locale, currencyConversionApi, bundleFactory, context, currencyLocaleMap);
+ }
+
+ @VisibleForTesting
+ Map<Currency, Locale> getCurrencyLocaleMap() {
+ return currencyLocaleMap;
}
}
diff --git a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
index 51ddcc6..5516c8c 100644
--- a/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
+++ b/invoice/src/main/java/org/killbill/billing/invoice/template/formatters/DefaultInvoiceItemFormatter.java
@@ -36,14 +36,10 @@ import org.killbill.billing.util.LocaleUtils;
import org.killbill.billing.util.template.translation.DefaultCatalogTranslator;
import org.killbill.billing.util.template.translation.Translator;
import org.killbill.billing.util.template.translation.TranslatorConfig;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
-import static org.killbill.billing.util.DefaultAmountFormatter.round;
-
/**
* Format invoice item fields
*/
@@ -71,7 +67,7 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
@Override
public BigDecimal getAmount() {
- return round(Objects.firstNonNull(item.getAmount(), BigDecimal.ZERO));
+ return Objects.firstNonNull(item.getAmount(), BigDecimal.ZERO);
}
@Override
@@ -168,7 +164,7 @@ public class DefaultInvoiceItemFormatter implements InvoiceItemFormatter {
@Override
public BigDecimal getRate() {
- return round(BigDecimal.ZERO);
+ return BigDecimal.ZERO;
}
@Override
diff --git a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
index df0a9d7..32b9b86 100644
--- a/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
+++ b/invoice/src/test/java/org/killbill/billing/invoice/template/formatters/TestDefaultInvoiceFormatter.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
@@ -34,7 +34,6 @@ import org.killbill.billing.invoice.api.InvoiceItem;
import org.killbill.billing.invoice.api.InvoiceItemType;
import org.killbill.billing.invoice.api.InvoicePaymentType;
import org.killbill.billing.invoice.api.formatters.InvoiceFormatter;
-import org.killbill.billing.invoice.api.formatters.InvoiceFormatterFactory;
import org.killbill.billing.invoice.api.formatters.ResourceBundleFactory.ResourceBundleType;
import org.killbill.billing.invoice.model.CreditAdjInvoiceItem;
import org.killbill.billing.invoice.model.CreditBalanceAdjInvoiceItem;
@@ -55,12 +54,14 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
private TranslatorConfig config;
private MustacheTemplateEngine templateEngine;
+ private DefaultInvoiceFormatterFactory defaultInvoiceFormatterFactory;
@BeforeClass(groups = "fast")
public void beforeClass() throws Exception {
super.beforeClass();
config = new ConfigurationObjectFactory(skifeConfigSource).build(TranslatorConfig.class);
templateEngine = new MustacheTemplateEngine();
+ defaultInvoiceFormatterFactory = new DefaultInvoiceFormatterFactory();
}
@Test(groups = "fast")
@@ -89,7 +90,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
Assert.assertEquals(invoice.getCreditedAmount().doubleValue(), 0.00);
// Verify the merge
- final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext);
+ final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap());
final List<InvoiceItem> invoiceItems = formatter.getInvoiceItems();
Assert.assertEquals(invoiceItems.size(), 1);
Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
@@ -143,7 +144,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
Assert.assertEquals(invoice.getRefundedAmount().doubleValue(), -1.00);
// Verify the merge
- final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext);
+ final InvoiceFormatter formatter = new DefaultInvoiceFormatter(config, invoice, Locale.US, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap());
final List<InvoiceItem> invoiceItems = formatter.getInvoiceItems();
Assert.assertEquals(invoiceItems.size(), 4);
Assert.assertEquals(invoiceItems.get(0).getInvoiceItemType(), InvoiceItemType.FIXED);
@@ -157,7 +158,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
}
@Test(groups = "fast")
- public void testFormattedAmount() throws Exception {
+ public void testFormattedAmountFranceAndEUR() throws Exception {
final FixedPriceInvoiceItem fixedItemEUR = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
new LocalDate(), new BigDecimal("1499.95"), Currency.EUR);
@@ -187,6 +188,186 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
}
@Test(groups = "fast")
+ public void testFormattedAmountFranceAndOMR() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("1499.958"), Currency.OMR);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.OMR);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>1 499,958 ر.ع.\u200F</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>0,000 ر.ع.\u200F</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>1 499,958 ر.ع.\u200F</strong></td>\n" +
+ "</tr>",
+ Locale.FRANCE);
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmountFranceAndJPY() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("1500.00"), Currency.JPY);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.JPY);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>1 500 ¥</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>0 ¥</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>1 500 ¥</strong></td>\n" +
+ "</tr>",
+ Locale.FRANCE);
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmountUSAndBTC() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("1105.28843439"), Currency.BTC);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BTC);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>BTC1,105.28843439</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>BTC0.00000000</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>BTC1,105.28843439</strong></td>\n" +
+ "</tr>",
+ Locale.US);
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmountUSAndEUR() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("2635.14"), Currency.EUR);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.EUR);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>€2,635.14</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>€0.00</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>€2,635.14</strong></td>\n" +
+ "</tr>",
+ Locale.US);
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmountUSAndBRL() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("2635.14"), Currency.BRL);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.BRL);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>R$2,635.14</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>R$0.00</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>R$2,635.14</strong></td>\n" +
+ "</tr>",
+ Locale.US);
+ }
+
+ @Test(groups = "fast")
+ public void testFormattedAmountUSAndGBP() throws Exception {
+ final FixedPriceInvoiceItem fixedItem = new FixedPriceInvoiceItem(UUID.randomUUID(), UUID.randomUUID(), null, null,
+ UUID.randomUUID().toString(), UUID.randomUUID().toString(),
+ new LocalDate(), new BigDecimal("1499.95"), Currency.GBP);
+ final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), new LocalDate(), new LocalDate(), Currency.GBP);
+ invoice.addInvoiceItem(fixedItem);
+
+ checkOutput(invoice,
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedChargedAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedPaidAmount}}</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>{{invoice.formattedBalance}}</strong></td>\n" +
+ "</tr>",
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>£1,499.95</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>£0.00</strong></td>\n" +
+ "</tr>\n" +
+ "<tr>\n" +
+ " <td class=\"amount\"><strong>£1,499.95</strong></td>\n" +
+ "</tr>",
+ Locale.US);
+ }
+
+ @Test(groups = "fast")
public void testProcessedCurrencyExists() throws Exception {
final Invoice invoice = new DefaultInvoice(UUID.randomUUID(), clock.getUTCNow(), UUID.randomUUID(), new Integer(234), new LocalDate(), new LocalDate(), Currency.BRL, Currency.USD, false);
@@ -329,7 +510,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
data.put("text", translator);
- data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi, resourceBundleFactory, internalCallContext));
+ data.put("invoice", new DefaultInvoiceFormatter(config, invoice, Locale.US, currencyConversionApi, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap()));
final String formattedText = templateEngine.executeTemplateText(template, data);
@@ -338,7 +519,7 @@ public class TestDefaultInvoiceFormatter extends InvoiceTestSuiteNoDB {
private void checkOutput(final Invoice invoice, final String template, final String expected, final Locale locale) {
final Map<String, Object> data = new HashMap<String, Object>();
- data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null, resourceBundleFactory, internalCallContext));
+ data.put("invoice", new DefaultInvoiceFormatter(config, invoice, locale, null, resourceBundleFactory, internalCallContext, defaultInvoiceFormatterFactory.getCurrencyLocaleMap()));
final String formattedText = templateEngine.executeTemplateText(template, data);
Assert.assertEquals(formattedText, expected);
pom.xml 2(+1 -1)
diff --git a/pom.xml b/pom.xml
index 064ea57..472c3bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -21,7 +21,7 @@
<parent>
<artifactId>killbill-oss-parent</artifactId>
<groupId>org.kill-bill.billing</groupId>
- <version>0.48</version>
+ <version>0.52</version>
</parent>
<artifactId>killbill</artifactId>
<version>0.15.6-SNAPSHOT</version>
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
index 8955201..85b17d9 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/KillbillJdbcTenantRealm.java
@@ -27,6 +27,7 @@ import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.codec.Base64;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.util.ByteSource;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher;
/**
@@ -37,11 +38,13 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
private static final String KILLBILL_AUTHENTICATION_QUERY = "select api_secret, api_salt from tenants where api_key = ?";
private final DataSource dataSource;
+ private final SecurityConfig securityConfig;
- public KillbillJdbcTenantRealm(final DataSource dataSource) {
+ public KillbillJdbcTenantRealm(final DataSource dataSource, final SecurityConfig securityConfig) {
super();
this.dataSource = dataSource;
+ this.securityConfig = securityConfig;
configureSecurity();
configureQueries();
@@ -61,7 +64,7 @@ public class KillbillJdbcTenantRealm extends JdbcRealm {
private void configureSecurity() {
setSaltStyle(SaltStyle.COLUMN);
- setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher());
+ setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig));
}
private void configureQueries() {
diff --git a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
index 4fb769e..97e2bed 100644
--- a/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
+++ b/profiles/killbill/src/main/java/org/killbill/billing/server/security/TenantFilter.java
@@ -44,6 +44,7 @@ import org.killbill.billing.server.modules.KillbillPlatformModule;
import org.killbill.billing.tenant.api.Tenant;
import org.killbill.billing.tenant.api.TenantApiException;
import org.killbill.billing.tenant.api.TenantUserApi;
+import org.killbill.billing.util.config.SecurityConfig;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -59,6 +60,8 @@ public class TenantFilter implements Filter {
@Inject
protected TenantUserApi tenantUserApi;
+ @Inject
+ protected SecurityConfig securityConfig;
@Inject
@Named(KillbillPlatformModule.SHIRO_DATA_SOURCE_ID_NAMED)
@@ -68,7 +71,7 @@ public class TenantFilter implements Filter {
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
- final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource);
+ final Realm killbillJdbcTenantRealm = new KillbillJdbcTenantRealm(dataSource, securityConfig);
// We use Shiro to verify the api credentials - but the Shiro Subject is only used for RBAC
modularRealmAuthenticator = new ModularRealmAuthenticator();
modularRealmAuthenticator.setRealms(ImmutableList.<Realm>of(killbillJdbcTenantRealm));
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
index bdfaa5d..fa48420 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/jaxrs/TestJaxrsBase.java
@@ -54,6 +54,7 @@ import org.killbill.billing.server.listeners.KillbillGuiceListener;
import org.killbill.billing.server.modules.KillbillServerModule;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.config.PaymentConfig;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.bus.api.PersistentBus;
import org.killbill.commons.jdbi.guice.DaoConfig;
import org.skife.config.ConfigurationObjectFactory;
@@ -96,6 +97,9 @@ public class TestJaxrsBase extends KillbillClient {
@Named(KillbillServerModule.SHIRO_DATA_SOURCE_ID)
protected DataSource shiroDataSource;
+ @Inject
+ protected SecurityConfig securityConfig;
+
protected DaoConfig daoConfig;
protected KillbillServerConfig serverConfig;
diff --git a/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java b/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java
index b3899fd..a82fe1e 100644
--- a/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java
+++ b/profiles/killbill/src/test/java/org/killbill/billing/server/security/TestKillbillJdbcTenantRealm.java
@@ -49,7 +49,7 @@ public class TestKillbillJdbcTenantRealm extends TestJaxrsBase {
super.beforeMethod();
// Create the tenant
- final DefaultTenantDao tenantDao = new DefaultTenantDao(dbi, clock, cacheControllerDispatcher, new DefaultNonEntityDao(dbi));
+ final DefaultTenantDao tenantDao = new DefaultTenantDao(dbi, clock, cacheControllerDispatcher, new DefaultNonEntityDao(dbi), securityConfig);
tenant = new DefaultTenant(UUID.randomUUID(), null, null, UUID.randomUUID().toString(),
UUID.randomUUID().toString(), UUID.randomUUID().toString());
tenantDao.create(new TenantModelDao(tenant), internalCallContext);
@@ -60,7 +60,7 @@ public class TestKillbillJdbcTenantRealm extends TestJaxrsBase {
dbConfig.setUsername(helper.getUsername());
dbConfig.setPassword(helper.getPassword());
- final KillbillJdbcTenantRealm jdbcRealm = new KillbillJdbcTenantRealm(shiroDataSource);
+ final KillbillJdbcTenantRealm jdbcRealm = new KillbillJdbcTenantRealm(shiroDataSource, securityConfig);
jdbcRealm.setDataSource(new HikariDataSource(dbConfig));
securityManager = new DefaultSecurityManager(jdbcRealm);
diff --git a/profiles/killbill/src/test/resources/killbill.properties b/profiles/killbill/src/test/resources/killbill.properties
index 470500c..08ec536 100644
--- a/profiles/killbill/src/test/resources/killbill.properties
+++ b/profiles/killbill/src/test/resources/killbill.properties
@@ -27,4 +27,4 @@ org.killbill.payment.retry.days=8,8,8
org.killbill.osgi.bundle.install.dir=/var/tmp/somethingthatdoesnotexist
# Speed up from the (more secure) default
-org.killbill.server.multitenant.hash_iterations=10
+org.killbill.security.shiroNbHashIterations=10
diff --git a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
index 14ada6f..4d929d4 100644
--- a/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
+++ b/tenant/src/main/java/org/killbill/billing/tenant/dao/DefaultTenantDao.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
*
@@ -35,6 +37,7 @@ import org.killbill.billing.tenant.api.TenantApiException;
import org.killbill.billing.tenant.api.TenantKV.TenantKey;
import org.killbill.billing.util.UUIDs;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.billing.util.dao.NonEntityDao;
import org.killbill.billing.util.entity.dao.EntityDaoBase;
import org.killbill.billing.util.entity.dao.EntitySqlDaoTransactionWrapper;
@@ -56,9 +59,12 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
private final RandomNumberGenerator rng = new SecureRandomNumberGenerator();
+ private final SecurityConfig securityConfig;
+
@Inject
- public DefaultTenantDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao) {
+ public DefaultTenantDao(final IDBI dbi, final Clock clock, final CacheControllerDispatcher cacheControllerDispatcher, final NonEntityDao nonEntityDao, final SecurityConfig securityConfig) {
super(new EntitySqlDaoTransactionalJdbiWrapper(dbi, clock, cacheControllerDispatcher, nonEntityDao), TenantSqlDao.class);
+ this.securityConfig = securityConfig;
}
@Override
@@ -82,7 +88,7 @@ public class DefaultTenantDao extends EntityDaoBase<TenantModelDao, Tenant, Tena
final ByteSource salt = rng.nextBytes();
// Hash the plain-text password with the random salt and multiple iterations and then Base64-encode the value (requires less space than Hex)
final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
- entity.getApiSecret(), salt, KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64();
+ entity.getApiSecret(), salt, securityConfig.getShiroNbHashIterations()).toBase64();
transactionalSqlDao.execute(new EntitySqlDaoTransactionWrapper<Void>() {
@Override
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java
index d23e86a..112e3ba 100644
--- a/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java
+++ b/tenant/src/test/java/org/killbill/billing/tenant/dao/TestDefaultTenantDao.java
@@ -45,11 +45,11 @@ public class TestDefaultTenantDao extends TenantTestSuiteWithEmbeddedDb {
// Good combo
final AuthenticationToken goodToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret());
- Assert.assertTrue(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(goodToken, authenticationInfo));
+ Assert.assertTrue(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig).doCredentialsMatch(goodToken, authenticationInfo));
// Bad combo
final AuthenticationToken badToken = new UsernamePasswordToken(tenant.getApiKey(), tenant.getApiSecret() + "T");
- Assert.assertFalse(KillbillCredentialsMatcher.getCredentialsMatcher().doCredentialsMatch(badToken, authenticationInfo));
+ Assert.assertFalse(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig).doCredentialsMatch(badToken, authenticationInfo));
}
@Test(groups = "slow")
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java
index c2acf95..0bae0d1 100644
--- a/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java
+++ b/tenant/src/test/java/org/killbill/billing/tenant/glue/TestTenantModuleWithEmbeddedDB.java
@@ -1,7 +1,7 @@
/*
* Copyright 2010-2013 Ning, Inc.
- * Copyright 2014 Groupon, Inc
- * Copyright 2014 The Billing Project, LLC
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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
@@ -21,6 +21,8 @@ package org.killbill.billing.tenant.glue;
import org.killbill.billing.GuicyKillbillTestWithEmbeddedDBModule;
import org.killbill.billing.platform.api.KillbillConfigSource;
import org.killbill.billing.util.glue.NonEntityDaoModule;
+import org.killbill.billing.util.glue.SecurityModule;
+import org.killbill.billing.util.glue.TestUtilModuleNoDB.ShiroModuleNoDB;
public class TestTenantModuleWithEmbeddedDB extends TestTenantModule {
@@ -34,5 +36,7 @@ public class TestTenantModuleWithEmbeddedDB extends TestTenantModule {
install(new GuicyKillbillTestWithEmbeddedDBModule(configSource));
install(new NonEntityDaoModule(configSource));
+ install(new SecurityModule(configSource));
+ install(new ShiroModuleNoDB(configSource));
}
}
diff --git a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java
index 48b6287..2c4f78f 100644
--- a/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java
+++ b/tenant/src/test/java/org/killbill/billing/tenant/TenantTestSuiteWithEmbeddedDb.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
*
@@ -18,18 +20,17 @@ package org.killbill.billing.tenant;
import javax.inject.Named;
+import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
import org.killbill.billing.tenant.api.TenantUserApi;
-import org.killbill.billing.tenant.dao.NoCachingTenantBroadcastDao;
+import org.killbill.billing.tenant.dao.DefaultTenantDao;
import org.killbill.billing.tenant.dao.TenantBroadcastDao;
import org.killbill.billing.tenant.glue.DefaultTenantModule;
+import org.killbill.billing.tenant.glue.TestTenantModuleWithEmbeddedDB;
+import org.killbill.billing.util.config.SecurityConfig;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.BeforeMethod;
-import org.killbill.billing.GuicyKillbillTestSuiteWithEmbeddedDB;
-import org.killbill.billing.tenant.dao.DefaultTenantDao;
-import org.killbill.billing.tenant.glue.TestTenantModuleWithEmbeddedDB;
-
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
@@ -49,6 +50,9 @@ public class TenantTestSuiteWithEmbeddedDb extends GuicyKillbillTestSuiteWithEmb
@Inject
protected TenantBroadcastDao tenantBroadcastDao;
+ @Inject
+ protected SecurityConfig securityConfig;
+
@BeforeClass(groups = "slow")
protected void beforeClass() throws Exception {
final Injector injector = Guice.createInjector(new TestTenantModuleWithEmbeddedDB(configSource));
diff --git a/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java
index d53b6c6..3b2382d 100644
--- a/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java
+++ b/util/src/main/java/org/killbill/billing/util/config/SecurityConfig.java
@@ -28,6 +28,11 @@ public interface SecurityConfig extends KillbillConfig {
@Description("Path to the shiro.ini file (classpath, url or file resource)")
public String getShiroResourcePath();
+ @Config("org.killbill.security.shiroNbHashIterations")
+ @Default("200000")
+ @Description("Sets the number of times submitted credentials will be hashed before comparing to the credentials stored in the system")
+ public Integer getShiroNbHashIterations();
+
// LDAP Realm
@Config("org.killbill.security.ldap.userDnTemplate")
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java
index 92cc4f9..28b9ca3 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/dao/DefaultUserDao.java
@@ -28,6 +28,7 @@ import org.apache.shiro.util.ByteSource;
import org.joda.time.DateTime;
import org.killbill.billing.ErrorCode;
import org.killbill.billing.security.SecurityApiException;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher;
import org.killbill.clock.Clock;
import org.killbill.commons.jdbi.mapper.LowerToCamelBeanMapperFactory;
@@ -43,17 +44,19 @@ import com.google.common.collect.Iterables;
public class DefaultUserDao implements UserDao {
private static final RandomNumberGenerator rng = new SecureRandomNumberGenerator();
+
private final IDBI dbi;
private final Clock clock;
+ private final SecurityConfig securityConfig;
@Inject
- public DefaultUserDao(final IDBI dbi, final Clock clock) {
+ public DefaultUserDao(final IDBI dbi, final Clock clock, final SecurityConfig securityConfig) {
this.dbi = dbi;
this.clock = clock;
+ this.securityConfig = securityConfig;
((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(UserModelDao.class));
((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(UserRolesModelDao.class));
((DBI) dbi).registerMapper(new LowerToCamelBeanMapperFactory(RolesPermissionsModelDao.class));
-
}
@Override
@@ -61,7 +64,7 @@ public class DefaultUserDao implements UserDao {
final ByteSource salt = rng.nextBytes();
final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
- password, salt.toBase64(), KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64();
+ password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64();
final DateTime createdDate = clock.getUTCNow();
dbi.inTransaction(new TransactionCallback<Void>() {
@@ -136,7 +139,7 @@ public class DefaultUserDao implements UserDao {
final ByteSource salt = rng.nextBytes();
final String hashedPasswordBase64 = new SimpleHash(KillbillCredentialsMatcher.HASH_ALGORITHM_NAME,
- password, salt.toBase64(), KillbillCredentialsMatcher.HASH_ITERATIONS).toBase64();
+ password, salt.toBase64(), securityConfig.getShiroNbHashIterations()).toBase64();
dbi.inTransaction(new TransactionCallback<Void>() {
@Override
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java b/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java
index 7b5bc12..1830226 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/KillbillCredentialsMatcher.java
@@ -1,7 +1,9 @@
/*
* Copyright 2010-2013 Ning, Inc.
+ * Copyright 2014-2015 Groupon, Inc
+ * Copyright 2014-2015 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:
*
@@ -19,23 +21,21 @@ package org.killbill.billing.util.security.shiro;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.crypto.hash.Sha512Hash;
+import org.killbill.billing.util.config.SecurityConfig;
public class KillbillCredentialsMatcher {
- public static final String KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY = "org.killbill.server.multitenant.hash_iterations";
-
// See http://www.stormpath.com/blog/strong-password-hashing-apache-shiro and https://issues.apache.org/jira/browse/SHIRO-290
public static final String HASH_ALGORITHM_NAME = Sha512Hash.ALGORITHM_NAME;
- public static final Integer HASH_ITERATIONS = Integer.parseInt(System.getProperty(KILLBILL_TENANT_HASH_ITERATIONS_PROPERTY, "200000"));
private KillbillCredentialsMatcher() {}
- public static CredentialsMatcher getCredentialsMatcher() {
+ public static CredentialsMatcher getCredentialsMatcher(final SecurityConfig securityConfig) {
// This needs to be in sync with DefaultTenantDao
final HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(HASH_ALGORITHM_NAME);
// base64 encoding, not hex
credentialsMatcher.setStoredCredentialsHexEncoded(false);
- credentialsMatcher.setHashIterations(HASH_ITERATIONS);
+ credentialsMatcher.setHashIterations(securityConfig.getShiroNbHashIterations());
return credentialsMatcher;
}
diff --git a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
index 9806abe..380258b 100644
--- a/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
+++ b/util/src/main/java/org/killbill/billing/util/security/shiro/realm/KillBillJdbcRealm.java
@@ -24,6 +24,7 @@ import javax.sql.DataSource;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.killbill.billing.platform.glue.KillBillPlatformModuleBase;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.billing.util.security.shiro.KillbillCredentialsMatcher;
public class KillBillJdbcRealm extends JdbcRealm {
@@ -33,11 +34,13 @@ public class KillBillJdbcRealm extends JdbcRealm {
protected static final String KILLBILL_PERMISSIONS_QUERY = "select permission from roles_permissions where role_name = ? and is_active";
private final DataSource dataSource;
+ private final SecurityConfig securityConfig;
@Inject
- public KillBillJdbcRealm(@Named(KillBillPlatformModuleBase.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource) {
+ public KillBillJdbcRealm(@Named(KillBillPlatformModuleBase.SHIRO_DATA_SOURCE_ID_NAMED) final DataSource dataSource, final SecurityConfig securityConfig) {
super();
this.dataSource = dataSource;
+ this.securityConfig = securityConfig;
// Tweak JdbcRealm defaults
setPermissionsLookupEnabled(true);
@@ -56,7 +59,7 @@ public class KillBillJdbcRealm extends JdbcRealm {
private void configureSecurity() {
setSaltStyle(SaltStyle.COLUMN);
- setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher());
+ setCredentialsMatcher(KillbillCredentialsMatcher.getCredentialsMatcher(securityConfig));
}
private void configureDataSource() {
diff --git a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java
index 540f375..931e546 100644
--- a/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java
+++ b/util/src/test/java/org/killbill/billing/util/security/shiro/realm/TestKillBillJdbcRealm.java
@@ -50,7 +50,7 @@ public class TestKillBillJdbcRealm extends UtilTestSuiteWithEmbeddedDB {
@BeforeMethod(groups = "slow")
public void beforeMethod() throws Exception {
super.beforeMethod();
- final KillBillJdbcRealm realm = new KillBillJdbcRealm(helper.getDataSource());
+ final KillBillJdbcRealm realm = new KillBillJdbcRealm(helper.getDataSource(), securityConfig);
securityManager = new DefaultSecurityManager(realm);
SecurityUtils.setSecurityManager(securityManager);
}
diff --git a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
index df64546..6bdbbb3 100644
--- a/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
+++ b/util/src/test/java/org/killbill/billing/util/UtilTestSuiteWithEmbeddedDB.java
@@ -26,6 +26,7 @@ import org.killbill.billing.security.api.SecurityApi;
import org.killbill.billing.util.audit.dao.AuditDao;
import org.killbill.billing.util.cache.CacheControllerDispatcher;
import org.killbill.billing.util.callcontext.InternalCallContextFactory;
+import org.killbill.billing.util.config.SecurityConfig;
import org.killbill.billing.util.customfield.api.DefaultCustomFieldUserApi;
import org.killbill.billing.util.customfield.dao.CustomFieldDao;
import org.killbill.billing.util.dao.NonEntityDao;
@@ -84,6 +85,8 @@ public abstract class UtilTestSuiteWithEmbeddedDB extends GuicyKillbillTestSuite
protected TestApiListener eventsListener;
@Inject
protected SecurityApi securityApi;
+ @Inject
+ protected SecurityConfig securityConfig;
@BeforeClass(groups = "slow")
public void beforeClass() throws Exception {