package org.apache.solr.schema; /* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF 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. */ import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.index.IndexableField; import org.apache.lucene.queries.function.FunctionValues; import org.apache.lucene.queries.function.ValueSource; import org.apache.lucene.search.Query; import org.apache.lucene.search.SortField; import org.apache.lucene.analysis.util.ResourceLoader; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.response.TextResponseWriter; import org.apache.solr.response.XMLWriter; import org.apache.solr.search.QParser; import org.apache.solr.search.SolrConstantScoreQuery; import org.apache.solr.search.function.ValueSourceRangeFilter; import org.apache.lucene.analysis.util.ResourceLoaderAware; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import java.io.IOException; import java.io.InputStream; import java.util.Currency; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; /** * Field type for support of monetary values. * <p> * See <a href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org/solr/CurrencyField</a> */ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoaderAware { protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency"; protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass"; protected static final Object PARAM_PRECISION_STEP = "precisionStep"; protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider"; protected static final String DEFAULT_DEFAULT_CURRENCY = "USD"; protected static final String DEFAULT_PRECISION_STEP = "0"; protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw"; protected static final String FIELD_SUFFIX_CURRENCY = "_currency"; private IndexSchema schema; protected FieldType fieldTypeCurrency; protected FieldType fieldTypeAmountRaw; private String exchangeRateProviderClass; private String defaultCurrency; private ExchangeRateProvider provider; public static Logger log = LoggerFactory.getLogger(CurrencyField.class); @Override protected void init(IndexSchema schema, Map<String, String> args) { super.init(schema, args); if (this.isMultiValued()) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "CurrencyField types can not be multiValued: " + this.typeName); } this.schema = schema; this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS); this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY); if (this.defaultCurrency == null) { this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY; } if (this.exchangeRateProviderClass == null) { this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS; } if (java.util.Currency.getInstance(this.defaultCurrency) == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + this.defaultCurrency); } String precisionStepString = args.get(PARAM_PRECISION_STEP); if (precisionStepString == null) { precisionStepString = DEFAULT_PRECISION_STEP; } // Initialize field type for amount fieldTypeAmountRaw = new TrieLongField(); fieldTypeAmountRaw.setTypeName("amount_raw_type_tlong"); Map<String,String> map = new HashMap<String,String>(1); map.put("precisionStep", precisionStepString); fieldTypeAmountRaw.init(schema, map); // Initialize field type for currency string fieldTypeCurrency = new StrField(); fieldTypeCurrency.setTypeName("currency_type_string"); fieldTypeCurrency.init(schema, new HashMap<String,String>()); args.remove(PARAM_RATE_PROVIDER_CLASS); args.remove(PARAM_DEFAULT_CURRENCY); args.remove(PARAM_PRECISION_STEP); try { Class<? extends ExchangeRateProvider> c = schema.getResourceLoader().findClass(exchangeRateProviderClass, ExchangeRateProvider.class); provider = c.newInstance(); provider.init(args); } catch (Exception e) { throw new SolrException(ErrorCode.BAD_REQUEST, "Error instansiating exhange rate provider "+exchangeRateProviderClass+". Please check your FieldType configuration", e); } } @Override public boolean isPolyField() { return true; } @Override public void checkSchemaField(final SchemaField field) throws SolrException { super.checkSchemaField(field); if (field.multiValued()) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "CurrencyFields can not be multiValued: " + field.getName()); } } @Override public IndexableField[] createFields(SchemaField field, Object externalVal, float boost) { CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency); IndexableField[] f = new IndexableField[field.stored() ? 3 : 2]; SchemaField amountField = getAmountField(field); f[0] = amountField.createField(String.valueOf(value.getAmount()), amountField.indexed() && !amountField.omitNorms() ? boost : 1F); SchemaField currencyField = getCurrencyField(field); f[1] = currencyField.createField(value.getCurrencyCode(), currencyField.indexed() && !currencyField.omitNorms() ? boost : 1F); if (field.stored()) { org.apache.lucene.document.FieldType customType = new org.apache.lucene.document.FieldType(); assert !customType.omitNorms(); customType.setStored(true); String storedValue = externalVal.toString().trim(); if (storedValue.indexOf(",") < 0) { storedValue += "," + defaultCurrency; } f[2] = createField(field.getName(), storedValue, customType, 1F); } return f; } private SchemaField getAmountField(SchemaField field) { return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW); } private SchemaField getCurrencyField(SchemaField field) { return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY); } private void createDynamicCurrencyField(String suffix, FieldType type) { String name = "*" + POLY_FIELD_SEPARATOR + suffix; Map<String, String> props = new HashMap<String, String>(); props.put("indexed", "true"); props.put("stored", "false"); props.put("multiValued", "false"); props.put("omitNorms", "true"); int p = SchemaField.calcProps(name, type, props); schema.registerDynamicField(SchemaField.create(name, type, p, null)); } /** * When index schema is informed, add dynamic fields. * * @param indexSchema The index schema. */ public void inform(IndexSchema indexSchema) { createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, fieldTypeCurrency); createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, fieldTypeAmountRaw); } /** * Load the currency config when resource loader initialized. * * @param resourceLoader The resource loader. */ public void inform(ResourceLoader resourceLoader) { provider.inform(resourceLoader); boolean reloaded = provider.reload(); if(!reloaded) { log.warn("Failed reloading currencies"); } } @Override public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) { CurrencyValue value = CurrencyValue.parse(externalVal, defaultCurrency); CurrencyValue valueDefault; valueDefault = value.convertTo(provider, defaultCurrency); return getRangeQuery(parser, field, valueDefault, valueDefault, true, true); } @Override public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) { final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency); final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency); if (p1 != null && p2 != null && !p1.getCurrencyCode().equals(p2.getCurrencyCode())) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot parse range query " + part1 + " to " + part2 + ": range queries only supported when upper and lower bound have same currency."); } return getRangeQuery(parser, field, p1, p2, minInclusive, maxInclusive); } public Query getRangeQuery(QParser parser, SchemaField field, final CurrencyValue p1, final CurrencyValue p2, final boolean minInclusive, final boolean maxInclusive) { String currencyCode = (p1 != null) ? p1.getCurrencyCode() : (p2 != null) ? p2.getCurrencyCode() : defaultCurrency; final CurrencyValueSource vs = new CurrencyValueSource(field, currencyCode, parser); return new SolrConstantScoreQuery(new ValueSourceRangeFilter(vs, p1 == null ? null : p1.getAmount() + "" , p2 == null ? null : p2.getAmount() + "", minInclusive, maxInclusive)); } @Override public SortField getSortField(SchemaField field, boolean reverse) { try { // Convert all values to default currency for sorting. return (new CurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse); } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); } } public void write(XMLWriter xmlWriter, String name, IndexableField field) throws IOException { xmlWriter.writeStr(name, field.stringValue(), false); } @Override public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException { writer.writeStr(name, field.stringValue(), false); } public ExchangeRateProvider getProvider() { return provider; } class CurrencyValueSource extends ValueSource { private static final long serialVersionUID = 1L; private String targetCurrencyCode; private ValueSource currencyValues; private ValueSource amountValues; private final SchemaField sf; public CurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) { this.sf = sfield; this.targetCurrencyCode = targetCurrencyCode; SchemaField amountField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW); SchemaField currencyField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY); currencyValues = currencyField.getType().getValueSource(currencyField, parser); amountValues = amountField.getType().getValueSource(amountField, parser); } public FunctionValues getValues(Map context, AtomicReaderContext reader) throws IOException { final FunctionValues amounts = amountValues.getValues(context, reader); final FunctionValues currencies = currencyValues.getValues(context, reader); return new FunctionValues() { private final int MAX_CURRENCIES_TO_CACHE = 256; private final int[] fractionDigitCache = new int[MAX_CURRENCIES_TO_CACHE]; private final String[] currencyOrdToCurrencyCache = new String[MAX_CURRENCIES_TO_CACHE]; private final double[] exchangeRateCache = new double[MAX_CURRENCIES_TO_CACHE]; private int targetFractionDigits = -1; private int targetCurrencyOrd = -1; private boolean initializedCache; private String getDocCurrencyCode(int doc, int currencyOrd) { if (currencyOrd < MAX_CURRENCIES_TO_CACHE) { String currency = currencyOrdToCurrencyCache[currencyOrd]; if (currency == null) { currencyOrdToCurrencyCache[currencyOrd] = currency = currencies.strVal(doc); } if (currency == null) { currency = defaultCurrency; } if (targetCurrencyOrd == -1 && currency.equals(targetCurrencyCode)) { targetCurrencyOrd = currencyOrd; } return currency; } else { return currencies.strVal(doc); } } public long longVal(int doc) { if (!initializedCache) { for (int i = 0; i < fractionDigitCache.length; i++) { fractionDigitCache[i] = -1; } initializedCache = true; } long amount = amounts.longVal(doc); int currencyOrd = currencies.ordVal(doc); if (currencyOrd == targetCurrencyOrd) { return amount; } double exchangeRate; int sourceFractionDigits; if (targetFractionDigits == -1) { targetFractionDigits = Currency.getInstance(targetCurrencyCode).getDefaultFractionDigits(); } if (currencyOrd < MAX_CURRENCIES_TO_CACHE) { exchangeRate = exchangeRateCache[currencyOrd]; if (exchangeRate <= 0.0) { String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd); exchangeRate = exchangeRateCache[currencyOrd] = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode); } sourceFractionDigits = fractionDigitCache[currencyOrd]; if (sourceFractionDigits == -1) { String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd); sourceFractionDigits = fractionDigitCache[currencyOrd] = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits(); } } else { String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd); exchangeRate = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode); sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits(); } return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits); } public int intVal(int doc) { return (int) longVal(doc); } public double doubleVal(int doc) { return (double) longVal(doc); } public float floatVal(int doc) { return (float) longVal(doc); } public String strVal(int doc) { return Long.toString(longVal(doc)); } public String toString(int doc) { return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')'; } }; } public String name() { return "currency"; } @Override public String description() { return name() + "(" + sf.getName() + ")"; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CurrencyValueSource that = (CurrencyValueSource) o; return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) && !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) && !(targetCurrencyCode != null ? !targetCurrencyCode.equals(that.targetCurrencyCode) : that.targetCurrencyCode != null); } @Override public int hashCode() { int result = targetCurrencyCode != null ? targetCurrencyCode.hashCode() : 0; result = 31 * result + (currencyValues != null ? currencyValues.hashCode() : 0); result = 31 * result + (amountValues != null ? amountValues.hashCode() : 0); return result; } } } /** * Configuration for currency. Provides currency exchange rates. */ class FileExchangeRateProvider implements ExchangeRateProvider { public static Logger log = LoggerFactory.getLogger(FileExchangeRateProvider.class); protected static final String PARAM_CURRENCY_CONFIG = "currencyConfig"; // Exchange rate map, maps Currency Code -> Currency Code -> Rate private Map<String, Map<String, Double>> rates = new HashMap<String, Map<String, Double>>(); private String currencyConfigFile; private ResourceLoader loader; /** * Returns the currently known exchange rate between two currencies. If a direct rate has been loaded, * it is used. Otherwise, if a rate is known to convert the target currency to the source, the inverse * exchange rate is computed. * * @param sourceCurrencyCode The source currency being converted from. * @param targetCurrencyCode The target currency being converted to. * @return The exchange rate. * @throws SolrException if the requested currency pair cannot be found */ public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) { if (sourceCurrencyCode == null || targetCurrencyCode == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null."); } if (sourceCurrencyCode.equals(targetCurrencyCode)) { return 1.0; } Double directRate = lookupRate(sourceCurrencyCode, targetCurrencyCode); if (directRate != null) { return directRate; } Double symmetricRate = lookupRate(targetCurrencyCode, sourceCurrencyCode); if (symmetricRate != null) { return 1.0 / symmetricRate; } throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No available conversion rate between " + sourceCurrencyCode + " to " + targetCurrencyCode); } /** * Looks up the current known rate, if any, between the source and target currencies. * * @param sourceCurrencyCode The source currency being converted from. * @param targetCurrencyCode The target currency being converted to. * @return The exchange rate, or null if no rate has been registered. */ private Double lookupRate(String sourceCurrencyCode, String targetCurrencyCode) { Map<String, Double> rhs = rates.get(sourceCurrencyCode); if (rhs != null) { return rhs.get(targetCurrencyCode); } return null; } /** * Registers the specified exchange rate. * * @param ratesMap The map to add rate to * @param sourceCurrencyCode The source currency. * @param targetCurrencyCode The target currency. * @param rate The known exchange rate. */ private void addRate(Map<String, Map<String, Double>> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) { Map<String, Double> rhs = ratesMap.get(sourceCurrencyCode); if (rhs == null) { rhs = new HashMap<String, Double>(); ratesMap.put(sourceCurrencyCode, rhs); } rhs.put(targetCurrencyCode, rate); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; FileExchangeRateProvider that = (FileExchangeRateProvider) o; return !(rates != null ? !rates.equals(that.rates) : that.rates != null); } @Override public int hashCode() { return rates != null ? rates.hashCode() : 0; } public String toString() { return "["+this.getClass().getName()+" : " + rates.size() + " rates.]"; } @Override public Set<String> listAvailableCurrencies() { Set<String> currencies = new HashSet<String>(); for(String from : rates.keySet()) { currencies.add(from); for(String to : rates.get(from).keySet()) { currencies.add(to); } } return currencies; } @Override public boolean reload() throws SolrException { InputStream is = null; Map<String, Map<String, Double>> tmpRates = new HashMap<String, Map<String, Double>>(); try { log.info("Reloading exchange rates from file "+this.currencyConfigFile); is = loader.openResource(currencyConfigFile); javax.xml.parsers.DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); try { dbf.setXIncludeAware(true); dbf.setNamespaceAware(true); } catch (UnsupportedOperationException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "XML parser doesn't support XInclude option", e); } try { Document doc = dbf.newDocumentBuilder().parse(is); XPathFactory xpathFactory = XPathFactory.newInstance(); XPath xpath = xpathFactory.newXPath(); // Parse exchange rates. NodeList nodes = (NodeList) xpath.evaluate("/currencyConfig/rates/rate", doc, XPathConstants.NODESET); for (int i = 0; i < nodes.getLength(); i++) { Node rateNode = nodes.item(i); NamedNodeMap attributes = rateNode.getAttributes(); Node from = attributes.getNamedItem("from"); Node to = attributes.getNamedItem("to"); Node rate = attributes.getNamedItem("rate"); if (from == null || to == null || rate == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Exchange rate missing attributes (required: from, to, rate) " + rateNode); } String fromCurrency = from.getNodeValue(); String toCurrency = to.getNodeValue(); Double exchangeRate; if (java.util.Currency.getInstance(fromCurrency) == null || java.util.Currency.getInstance(toCurrency) == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not find from currency specified in exchange rate: " + rateNode); } try { exchangeRate = Double.parseDouble(rate.getNodeValue()); } catch (NumberFormatException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not parse exchange rate: " + rateNode, e); } addRate(tmpRates, fromCurrency, toCurrency, exchangeRate); } } catch (SAXException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e); } catch (IOException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e); } catch (ParserConfigurationException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e); } catch (XPathExpressionException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e); } } catch (IOException e) { throw new SolrException(ErrorCode.BAD_REQUEST, "Error while opening Currency configuration file "+currencyConfigFile, e); } finally { try { if (is != null) { is.close(); } } catch (IOException e) { e.printStackTrace(); } } // Atomically swap in the new rates map, if it loaded successfully this.rates = tmpRates; return true; } @Override public void init(Map<String,String> params) throws SolrException { this.currencyConfigFile = params.get(PARAM_CURRENCY_CONFIG); if(currencyConfigFile == null) { throw new SolrException(ErrorCode.NOT_FOUND, "Missing required configuration "+PARAM_CURRENCY_CONFIG); } // Removing config params custom to us params.remove(PARAM_CURRENCY_CONFIG); } @Override public void inform(ResourceLoader loader) throws SolrException { if(loader == null) { throw new SolrException(ErrorCode.BAD_REQUEST, "Needs ResourceLoader in order to load config file"); } this.loader = loader; reload(); } } /** * Represents a Currency field value, which includes a long amount and ISO currency code. */ class CurrencyValue { private long amount; private String currencyCode; /** * Constructs a new currency value. * * @param amount The amount. * @param currencyCode The currency code. */ public CurrencyValue(long amount, String currencyCode) { this.amount = amount; this.currencyCode = currencyCode; } /** * Constructs a new currency value by parsing the specific input. * <p/> * Currency values are expected to be in the format <amount>,<currency code>, * for example, "500,USD" would represent 5 U.S. Dollars. * <p/> * If no currency code is specified, the default is assumed. * * @param externalVal The value to parse. * @param defaultCurrency The default currency. * @return The parsed CurrencyValue. */ public static CurrencyValue parse(String externalVal, String defaultCurrency) { if (externalVal == null) { return null; } String amount = externalVal; String code = defaultCurrency; if (externalVal.contains(",")) { String[] amountAndCode = externalVal.split(","); amount = amountAndCode[0]; code = amountAndCode[1]; } if (amount.equals("*")) { return null; } Currency currency = java.util.Currency.getInstance(code); if (currency == null) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + code); } try { double value = Double.parseDouble(amount); long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits())); return new CurrencyValue(currencyValue, code); } catch (NumberFormatException e) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e); } } /** * The amount of the CurrencyValue. * * @return The amount. */ public long getAmount() { return amount; } /** * The ISO currency code of the CurrencyValue. * * @return The currency code. */ public String getCurrencyCode() { return currencyCode; } /** * Performs a currency conversion & unit conversion. * * @param exchangeRates Exchange rates to apply. * @param sourceCurrencyCode The source currency code. * @param sourceAmount The source amount. * @param targetCurrencyCode The target currency code. * @return The converted indexable units after the exchange rate and currency fraction digits are applied. */ public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) { double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode); return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode); } /** * Performs a currency conversion & unit conversion. * * @param exchangeRate Exchange rate to apply. * @param sourceFractionDigits The fraction digits of the source. * @param sourceAmount The source amount. * @param targetFractionDigits The fraction digits of the target. * @return The converted indexable units after the exchange rate and currency fraction digits are applied. */ public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) { int digitDelta = targetFractionDigits - sourceFractionDigits; double value = ((double) sourceAmount * exchangeRate); if (digitDelta != 0) { if (digitDelta < 0) { for (int i = 0; i < -digitDelta; i++) { value *= 0.1; } } else { for (int i = 0; i < digitDelta; i++) { value *= 10.0; } } } return (long) value; } /** * Performs a currency conversion & unit conversion. * * @param exchangeRate Exchange rate to apply. * @param sourceCurrencyCode The source currency code. * @param sourceAmount The source amount. * @param targetCurrencyCode The target currency code. * @return The converted indexable units after the exchange rate and currency fraction digits are applied. */ public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) { if (targetCurrencyCode.equals(sourceCurrencyCode)) { return sourceAmount; } int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits(); Currency targetCurrency = Currency.getInstance(targetCurrencyCode); int targetFractionDigits = targetCurrency.getDefaultFractionDigits(); return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits); } /** * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency. * * @param exchangeRates The exchange rate provider. * @param targetCurrencyCode The target currency code to convert this CurrencyValue to. * @return The converted CurrencyValue. */ public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) { return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode); } public String toString() { return String.valueOf(amount) + "," + currencyCode; } }