diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 6477ada483e..85535c3a668 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -343,6 +343,8 @@ Upgrade Notes
* SOLR-10379: ManagedSynonymFilterFactory has been deprecated in favor of ManagedSynonymGraphFilterFactory.
+* SOLR-10503: CurrencyField has been deprecated in favor of new CurrencyFieldType.
+
New Features
----------------------
@@ -490,6 +492,10 @@ Other Changes
rendered visibly in the PDF. Also add .adoc file checks to the top-level validate target, including
for the invisible substitutions PDF problem. (Steve Rowe)
+* SOLR-10503,SOLR-10502: Deprecate CurrencyField in favor of new CurrencyFieldType, which works
+ with point fields and provides control over dynamic fields used for the raw amount and currency
+ code sub-fields. (hossman, Steve Rowe)
+
================== 6.6.1 ==================
Bug Fixes
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyField.java b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
index 286d2c12c8b..e2676fe45d2 100644
--- a/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
@@ -14,189 +14,74 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.solr.schema;
-import java.io.IOException;
-import java.io.InputStream;
-import java.lang.invoke.MethodHandles;
+
import java.util.ArrayList;
-import java.util.Currency;
+import java.util.Collections;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Map;
-import java.util.Set;
-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 org.apache.lucene.analysis.util.ResourceLoader;
import org.apache.lucene.analysis.util.ResourceLoaderAware;
-import org.apache.lucene.document.StoredField;
-import org.apache.lucene.index.LeafReaderContext;
-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.BooleanClause.Occur;
-import org.apache.lucene.search.BooleanQuery;
-import org.apache.lucene.search.FieldValueQuery;
-import org.apache.lucene.search.Query;
-import org.apache.lucene.search.SortField;
-import org.apache.solr.uninverting.UninvertingReader.Type;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
-import org.apache.solr.response.TextResponseWriter;
-import org.apache.solr.search.Filter;
-import org.apache.solr.search.QParser;
-import org.apache.solr.search.QueryWrapperFilter;
-import org.apache.solr.search.SolrConstantScoreQuery;
-import org.apache.solr.search.function.ValueSourceRangeFilter;
-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;
+
/**
* Field type for support of monetary values.
*
* See http://wiki.apache.org/solr/CurrencyField
+ * @deprecated Use {@link CurrencyFieldType}
*/
-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;
- private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
-
- /**
- * A wrapper arround Currency.getInstance that returns null
- * instead of throwing IllegalArgumentException
- * if the specified Currency does not exist in this JVM.
- *
- * @see Currency#getInstance(String)
- */
- public static Currency getCurrency(final String code) {
- try {
- return Currency.getInstance(code);
- } catch (IllegalArgumentException e) {
- /* :NOOP: */
- }
- return null;
- }
+@Deprecated
+public class CurrencyField extends CurrencyFieldType implements SchemaAware, ResourceLoaderAware {
+ protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw";
+ protected static final String FIELD_SUFFIX_CURRENCY = "_currency";
+ protected static final String FIELD_TYPE_AMOUNT_RAW = "amount_raw_type_long";
+ protected static final String FIELD_TYPE_CURRENCY = "currency_type_string";
+ protected static final String PARAM_PRECISION_STEP = "precisionStep";
+ protected static final String DEFAULT_PRECISION_STEP = "0";
@Override
protected void init(IndexSchema schema, Map args) {
- super.init(schema, args);
- if (this.isMultiValued()) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
- "CurrencyField types can not be multiValued: " +
- this.typeName);
+
+ // Fail if amountLongSuffix or codeStrSuffix are specified
+ List unknownParams = new ArrayList<>();
+ fieldSuffixAmountRaw = args.get(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
+ if (fieldSuffixAmountRaw != null) {
+ unknownParams.add(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
}
- 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;
+ fieldSuffixCurrency = args.get(PARAM_FIELD_SUFFIX_CURRENCY);
+ if (fieldSuffixCurrency != null) {
+ unknownParams.add(PARAM_FIELD_SUFFIX_CURRENCY);
+ }
+ if ( ! unknownParams.isEmpty()) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Unknown parameter(s): " + unknownParams);
}
- if (this.exchangeRateProviderClass == null) {
- this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS;
- }
-
- if (null == getCurrency(this.defaultCurrency)) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Default currency code is not supported by this JVM: " + this.defaultCurrency);
- }
-
String precisionStepString = args.get(PARAM_PRECISION_STEP);
if (precisionStepString == null) {
precisionStepString = DEFAULT_PRECISION_STEP;
+ } else {
+ args.remove(PARAM_PRECISION_STEP);
}
// Initialize field type for amount
fieldTypeAmountRaw = new TrieLongField();
- fieldTypeAmountRaw.setTypeName("amount_raw_type_tlong");
+ fieldTypeAmountRaw.setTypeName(FIELD_TYPE_AMOUNT_RAW);
Map map = new HashMap<>(1);
map.put("precisionStep", precisionStepString);
fieldTypeAmountRaw.init(schema, map);
-
+ fieldSuffixAmountRaw = FIELD_SUFFIX_AMOUNT_RAW;
+
// Initialize field type for currency string
fieldTypeCurrency = new StrField();
- fieldTypeCurrency.setTypeName("currency_type_string");
- fieldTypeCurrency.init(schema, new HashMap());
-
- args.remove(PARAM_RATE_PROVIDER_CLASS);
- args.remove(PARAM_DEFAULT_CURRENCY);
- args.remove(PARAM_PRECISION_STEP);
+ fieldTypeCurrency.setTypeName(FIELD_TYPE_CURRENCY);
+ fieldTypeCurrency.init(schema, Collections.emptyMap());
+ fieldSuffixCurrency = FIELD_SUFFIX_CURRENCY;
- 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 instantiating exchange rate provider "+exchangeRateProviderClass+": " + e.getMessage(), 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 List createFields(SchemaField field, Object externalVal) {
- CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
-
- List f = new ArrayList<>();
- SchemaField amountField = getAmountField(field);
- f.add(amountField.createField(String.valueOf(value.getAmount())));
- SchemaField currencyField = getCurrencyField(field);
- f.add(currencyField.createField(value.getCurrencyCode()));
-
- if (field.stored()) {
- String storedValue = externalVal.toString().trim();
- if (storedValue.indexOf(",") < 0) {
- storedValue += "," + defaultCurrency;
- }
- f.add(createField(field.getName(), storedValue, StoredField.TYPE));
- }
-
- 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);
+ super.init(schema, args); // Must be called last so that field types are not doubly created
}
private void createDynamicCurrencyField(String suffix, FieldType type) {
@@ -212,825 +97,15 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa
/**
* When index schema is informed, add dynamic fields "*____currency" and "*____amount_raw".
- *
+ *
* {@inheritDoc}
- *
+ *
* @param schema {@inheritDoc}
*/
@Override
public void inform(IndexSchema schema) {
- this.schema = schema;
createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, fieldTypeCurrency);
createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, fieldTypeAmountRaw);
- }
-
- /**
- * Load the currency config when resource loader initialized.
- *
- * @param resourceLoader The resource loader.
- */
- @Override
- 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);
- }
-
- /**
- *
- * Returns a ValueSource over this field in which the numeric value for
- * each document represents the indexed value as converted to the default
- * currency for the field, normalized to its most granular form based
- * on the default fractional digits.
- *
- *
- * For example: If the default Currency specified for a field is
- * USD, then the values returned by this value source would
- * represent the equivilent number of "cents" (ie: value in dollars * 100)
- * after converting each document's native currency to USD -- because the
- * default fractional digits for USD is "2".
- * So for a document whose indexed value was currently equivilent to
- * "5.43,USD" using the the exchange provider for this field,
- * this ValueSource would return a value of "543"
- *
- * Returns a ValueSource over this field in which the numeric value for
- * each document represents the value from the underlying
- * RawCurrencyValueSource as converted to the specified target
- * Currency.
- *
- *
- * For example: If the targetCurrencyCode param is set to
- * USD, then the values returned by this value source would
- * represent the equivilent number of dollars after converting each
- * document's raw value to USD. So for a document whose
- * indexed value was currently equivilent to "5.43,USD"
- * using the the exchange provider for this field, this ValueSource would
- * return a value of "5.43"
- *
- *
- * @param targetCurrencyCode The target currency for the resulting value source, if null the defaultCurrency for this field type will be used
- * @param source the raw ValueSource to wrap
- * @see #PARAM_DEFAULT_CURRENCY
- * @see #DEFAULT_DEFAULT_CURRENCY
- * @see #getValueSource
- */
- public ValueSource getConvertedValueSource(String targetCurrencyCode,
- RawCurrencyValueSource source) {
- if (null == targetCurrencyCode) {
- targetCurrencyCode = defaultCurrency;
- }
- return new ConvertedCurrencyValueSource(targetCurrencyCode,
- source);
- }
-
- @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;
-
- // ValueSourceRangeFilter doesn't check exists(), so we have to
- final Filter docsWithValues = new QueryWrapperFilter(new FieldValueQuery(getAmountField(field).getName()));
- final Filter vsRangeFilter = new ValueSourceRangeFilter
- (new RawCurrencyValueSource(field, currencyCode, parser),
- p1 == null ? null : p1.getAmount() + "",
- p2 == null ? null : p2.getAmount() + "",
- minInclusive, maxInclusive);
- final BooleanQuery.Builder docsInRange = new BooleanQuery.Builder();
- docsInRange.add(docsWithValues, Occur.FILTER);
- docsInRange.add(vsRangeFilter, Occur.FILTER);
-
- return new SolrConstantScoreQuery(new QueryWrapperFilter(docsInRange.build()));
- }
-
- @Override
- public SortField getSortField(SchemaField field, boolean reverse) {
- // Convert all values to default currency for sorting.
- return (new RawCurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
- }
-
- @Override
- public Type getUninversionType(SchemaField sf) {
- return null;
- }
-
- @Override
- public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException {
- writer.writeStr(name, field.stringValue(), true);
- }
-
- public ExchangeRateProvider getProvider() {
- return provider;
- }
-
- /**
- *
- * A value source whose values represent the "normal" values
- * in the specified target currency.
- *
- * @see RawCurrencyValueSource
- */
- class ConvertedCurrencyValueSource extends ValueSource {
- private final Currency targetCurrency;
- private final RawCurrencyValueSource source;
- private final double rate;
- public ConvertedCurrencyValueSource(String targetCurrencyCode,
- RawCurrencyValueSource source) {
- this.source = source;
- this.targetCurrency = getCurrency(targetCurrencyCode);
- if (null == targetCurrency) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode);
- }
- // the target digits & currency of our source,
- // become the source digits & currency of ourselves
- this.rate = provider.getExchangeRate
- (source.getTargetCurrency().getCurrencyCode(),
- targetCurrency.getCurrencyCode());
- }
-
- @Override
- public FunctionValues getValues(Map context, LeafReaderContext reader)
- throws IOException {
- final FunctionValues amounts = source.getValues(context, reader);
- // the target digits & currency of our source,
- // become the source digits & currency of ourselves
- final String sourceCurrencyCode = source.getTargetCurrency().getCurrencyCode();
- final int sourceFractionDigits = source.getTargetCurrency().getDefaultFractionDigits();
- final double divisor = Math.pow(10D, targetCurrency.getDefaultFractionDigits());
- return new FunctionValues() {
- @Override
- public boolean exists(int doc) throws IOException {
- return amounts.exists(doc);
- }
- @Override
- public long longVal(int doc) throws IOException {
- return (long) doubleVal(doc);
- }
- @Override
- public int intVal(int doc) throws IOException {
- return (int) doubleVal(doc);
- }
-
- @Override
- public double doubleVal(int doc) throws IOException {
- return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor;
- }
-
- @Override
- public float floatVal(int doc) throws IOException {
- return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor);
- }
-
- @Override
- public String strVal(int doc) throws IOException {
- return Double.toString(doubleVal(doc));
- }
-
- @Override
- public String toString(int doc) throws IOException {
- return name() + '(' + strVal(doc) + ')';
- }
- };
- }
- public String name() {
- return "currency";
- }
-
- @Override
- public String description() {
- return name() + "(" + source.getField().getName() + "," + targetCurrency.getCurrencyCode()+")";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o;
-
- return !(source != null ? !source.equals(that.source) : that.source != null) &&
- (rate == that.rate) &&
- !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null);
-
- }
-
- @Override
- public int hashCode() {
- int result = targetCurrency != null ? targetCurrency.hashCode() : 0;
- result = 31 * result + (source != null ? source.hashCode() : 0);
- result = 31 * (int) Double.doubleToLongBits(rate);
- return result;
- }
- }
-
- /**
- *
- * A value source whose values represent the "raw" (ie: normalized using
- * the number of default fractional digits) values in the specified
- * target currency).
- *
- *
- * For example: if the specified target currency is "USD"
- * then the numeric values are the number of pennies in the value
- * (ie: $n * 100) since the number of defalt fractional
- * digits for USD is "2")
- *
- * @see ConvertedCurrencyValueSource
- */
- class RawCurrencyValueSource extends ValueSource {
- private static final long serialVersionUID = 1L;
- private final Currency targetCurrency;
- private ValueSource currencyValues;
- private ValueSource amountValues;
- private final SchemaField sf;
-
- public RawCurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
- this.sf = sfield;
- this.targetCurrency = getCurrency(targetCurrencyCode);
- if (null == targetCurrency) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode);
- }
-
- SchemaField amountField = getAmountField(sf);
- SchemaField currencyField = getCurrencyField(sf);
-
- currencyValues = currencyField.getType().getValueSource(currencyField, parser);
- amountValues = amountField.getType().getValueSource(amountField, parser);
- }
-
- public SchemaField getField() { return sf; }
- public Currency getTargetCurrency() { return targetCurrency; }
-
- @Override
- public FunctionValues getValues(Map context, LeafReaderContext reader) throws IOException {
- final FunctionValues amounts = amountValues.getValues(context, reader);
- final FunctionValues currencies = currencyValues.getValues(context, reader);
-
- return new FunctionValues() {
- private static 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) throws IOException {
- 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(targetCurrency.getCurrencyCode() )) {
- targetCurrencyOrd = currencyOrd;
- }
-
- return currency;
- } else {
- return currencies.strVal(doc);
- }
- }
- /** throws a (Server Error) SolrException if the code is not valid */
- private Currency getDocCurrency(int doc, int currencyOrd) throws IOException {
- String code = getDocCurrencyCode(doc, currencyOrd);
- Currency c = getCurrency(code);
- if (null == c) {
- throw new SolrException
- (SolrException.ErrorCode.SERVER_ERROR,
- "Currency code of document is not supported by this JVM: "+code);
- }
- return c;
- }
-
- @Override
- public boolean exists(int doc) throws IOException {
- return amounts.exists(doc);
- }
-
- @Override
- public long longVal(int doc) throws IOException {
- long amount = amounts.longVal(doc);
- // bail fast using whatever amounts defaults to if no value
- // (if we don't do this early, currencyOrd may be < 0,
- // causing index bounds exception
- if ( ! exists(doc) ) {
- return amount;
- }
-
- if (!initializedCache) {
- for (int i = 0; i < fractionDigitCache.length; i++) {
- fractionDigitCache[i] = -1;
- }
-
- initializedCache = true;
- }
-
- int currencyOrd = currencies.ordVal(doc);
-
- if (currencyOrd == targetCurrencyOrd) {
- return amount;
- }
-
- double exchangeRate;
- int sourceFractionDigits;
-
- if (targetFractionDigits == -1) {
- targetFractionDigits = targetCurrency.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, targetCurrency.getCurrencyCode());
- }
-
- sourceFractionDigits = fractionDigitCache[currencyOrd];
-
- if (sourceFractionDigits == -1) {
- sourceFractionDigits = fractionDigitCache[currencyOrd] = getDocCurrency(doc, currencyOrd).getDefaultFractionDigits();
- }
- } else {
- Currency source = getDocCurrency(doc, currencyOrd);
- exchangeRate = provider.getExchangeRate(source.getCurrencyCode(), targetCurrency.getCurrencyCode());
- sourceFractionDigits = source.getDefaultFractionDigits();
- }
-
- return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits);
- }
-
- @Override
- public int intVal(int doc) throws IOException {
- return (int) longVal(doc);
- }
-
- @Override
- public double doubleVal(int doc) throws IOException {
- return (double) longVal(doc);
- }
-
- @Override
- public float floatVal(int doc) throws IOException {
- return (float) longVal(doc);
- }
-
- @Override
- public String strVal(int doc) throws IOException {
- return Long.toString(longVal(doc));
- }
-
- @Override
- public String toString(int doc) throws IOException {
- return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')';
- }
- };
- }
-
- public String name() {
- return "rawcurrency";
- }
-
- @Override
- public String description() {
- return name() + "(" + sf.getName() +
- ",target="+targetCurrency.getCurrencyCode()+")";
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- RawCurrencyValueSource that = (RawCurrencyValueSource) o;
-
- return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) &&
- !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) &&
- !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null);
-
- }
-
- @Override
- public int hashCode() {
- int result = targetCurrency != null ? targetCurrency.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 {
- private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
- protected static final String PARAM_CURRENCY_CONFIG = "currencyConfig";
-
- // Exchange rate map, maps Currency Code -> Currency Code -> Rate
- private Map> rates = new HashMap<>();
-
- 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
- */
- @Override
- 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 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> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) {
- Map rhs = ratesMap.get(sourceCurrencyCode);
-
- if (rhs == null) {
- rhs = new HashMap<>();
- 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;
- }
-
- @Override
- public String toString() {
- return "["+this.getClass().getName()+" : " + rates.size() + " rates.]";
- }
-
- @Override
- public Set listAvailableCurrencies() {
- Set currencies = new HashSet<>();
- 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> tmpRates = new HashMap<>();
- try {
- log.debug("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.SERVER_ERROR, "Exchange rate missing attributes (required: from, to, rate) " + rateNode);
- }
-
- String fromCurrency = from.getNodeValue();
- String toCurrency = to.getNodeValue();
- Double exchangeRate;
-
- if (null == CurrencyField.getCurrency(fromCurrency)) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'from' currency not supported in this JVM: " + fromCurrency);
- }
- if (null == CurrencyField.getCurrency(toCurrency)) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'to' currency not supported in this JVM: " + toCurrency);
- }
-
- try {
- exchangeRate = Double.parseDouble(rate.getNodeValue());
- } catch (NumberFormatException e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Could not parse exchange rate: " + rateNode, e);
- }
-
- addRate(tmpRates, fromCurrency, toCurrency, exchangeRate);
- }
- } catch (SAXException | XPathExpressionException | ParserConfigurationException | IOException e) {
- throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error parsing currency config.", e);
- }
- } catch (IOException e) {
- throw new SolrException(ErrorCode.SERVER_ERROR, "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 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.SERVER_ERROR, "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.
- *
- * Currency values are expected to be in the format <amount>,<currency code>,
- * for example, "500,USD" would represent 5 U.S. Dollars.
- *
- * 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 = CurrencyField.getCurrency(code);
-
- if (currency == null) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + 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);
- }
-
- @Override
- public String toString() {
- return String.valueOf(amount) + "," + currencyCode;
+ super.inform(schema);
}
}
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
new file mode 100644
index 00000000000..0ace37e2772
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
@@ -0,0 +1,829 @@
+/*
+ * 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.
+ */
+
+package org.apache.solr.schema;
+
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Currency;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.lucene.analysis.util.ResourceLoaderAware;
+import org.apache.lucene.document.StoredField;
+import org.apache.lucene.index.LeafReaderContext;
+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.BooleanClause.Occur;
+import org.apache.lucene.search.BooleanQuery;
+import org.apache.lucene.search.FieldValueQuery;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.uninverting.UninvertingReader.Type;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.search.Filter;
+import org.apache.solr.search.QParser;
+import org.apache.solr.search.QueryWrapperFilter;
+import org.apache.solr.search.SolrConstantScoreQuery;
+import org.apache.solr.search.function.ValueSourceRangeFilter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Field type for support of monetary values.
+ *
+ * See http://wiki.apache.org/solr/CurrencyField
+ */
+public class CurrencyFieldType extends FieldType implements SchemaAware, ResourceLoaderAware {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency";
+ protected static final String DEFAULT_DEFAULT_CURRENCY = "USD";
+ protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass";
+ protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider";
+ protected static final String PARAM_FIELD_SUFFIX_AMOUNT_RAW = "amountLongSuffix";
+ protected static final String PARAM_FIELD_SUFFIX_CURRENCY = "codeStrSuffix";
+
+ protected IndexSchema schema;
+ protected FieldType fieldTypeCurrency;
+ protected FieldType fieldTypeAmountRaw;
+ protected String fieldSuffixAmountRaw;
+ protected String fieldSuffixCurrency;
+
+ private String exchangeRateProviderClass;
+ private String defaultCurrency;
+ private ExchangeRateProvider provider;
+
+ /**
+ * A wrapper around Currency.getInstance that returns null
+ * instead of throwing IllegalArgumentException
+ * if the specified Currency does not exist in this JVM.
+ *
+ * @see Currency#getInstance(String)
+ */
+ public static Currency getCurrency(final String code) {
+ try {
+ return Currency.getInstance(code);
+ } catch (IllegalArgumentException e) {
+ /* :NOOP: */
+ }
+ return null;
+ }
+
+ @Override
+ protected void init(IndexSchema schema, Map args) {
+ super.init(schema, args);
+ if (this.isMultiValued()) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,
+ getClass().getSimpleName() + " types can not be multiValued: " + this.typeName);
+ }
+ this.schema = schema;
+
+ this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY);
+ if (this.defaultCurrency == null) {
+ this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY;
+ } else {
+ args.remove(PARAM_DEFAULT_CURRENCY);
+ }
+ if (null == getCurrency(this.defaultCurrency)) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
+ "Default currency code is not supported by this JVM: " + this.defaultCurrency);
+ }
+
+ this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS);
+ if (this.exchangeRateProviderClass == null) {
+ this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS;
+ } else {
+ args.remove(PARAM_RATE_PROVIDER_CLASS);
+ }
+ try {
+ Class extends ExchangeRateProvider> c
+ = schema.getResourceLoader().findClass(exchangeRateProviderClass, ExchangeRateProvider.class);
+ provider = c.newInstance();
+ provider.init(args);
+ } catch (Exception e) {
+ throw new SolrException(ErrorCode.SERVER_ERROR,
+ "Error instantiating exchange rate provider " + exchangeRateProviderClass + ": " + e.getMessage(), e);
+ }
+
+ if (fieldTypeAmountRaw == null) { // Don't initialize if subclass already has done so
+ fieldSuffixAmountRaw = args.get(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
+ if (fieldSuffixAmountRaw == null) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required param " + PARAM_FIELD_SUFFIX_AMOUNT_RAW);
+ } else {
+ args.remove(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
+ }
+ }
+
+ if (fieldTypeCurrency == null) { // Don't initialize if subclass already has done so
+ fieldSuffixCurrency = args.get(PARAM_FIELD_SUFFIX_CURRENCY);
+ if (fieldSuffixCurrency == null) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required param " + PARAM_FIELD_SUFFIX_CURRENCY);
+ } else {
+ args.remove(PARAM_FIELD_SUFFIX_CURRENCY);
+ }
+ }
+ }
+
+ @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,
+ getClass().getSimpleName() + " fields can not be multiValued: " + field.getName());
+ }
+ }
+
+ @Override
+ public List createFields(SchemaField field, Object externalVal) {
+ CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
+
+ List f = new ArrayList<>();
+ SchemaField amountField = getAmountField(field);
+ f.add(amountField.createField(String.valueOf(value.getAmount())));
+ SchemaField currencyField = getCurrencyField(field);
+ f.add(currencyField.createField(value.getCurrencyCode()));
+
+ if (field.stored()) {
+ String storedValue = externalVal.toString().trim();
+ if (storedValue.indexOf(",") < 0) {
+ storedValue += "," + defaultCurrency;
+ }
+ f.add(createField(field.getName(), storedValue, StoredField.TYPE));
+ }
+
+ return f;
+ }
+
+ private SchemaField getAmountField(SchemaField field) {
+ return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + fieldSuffixAmountRaw);
+ }
+
+ private SchemaField getCurrencyField(SchemaField field) {
+ return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + fieldSuffixCurrency);
+ }
+
+ /**
+ * When index schema is informed, get field types for the configured dynamic sub-fields
+ *
+ * {@inheritDoc}
+ *
+ * @param schema {@inheritDoc}
+ */
+ @Override
+ public void inform(IndexSchema schema) {
+ this.schema = schema;
+ if (null == fieldTypeAmountRaw) {
+ assert null != fieldSuffixAmountRaw : "How did we get here?";
+ SchemaField field = schema.getFieldOrNull(POLY_FIELD_SEPARATOR + fieldSuffixAmountRaw);
+ if (field == null) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName()
+ + "\": Undefined dynamic field for " + PARAM_FIELD_SUFFIX_AMOUNT_RAW + "=\"" + fieldSuffixAmountRaw + "\"");
+ }
+ fieldTypeAmountRaw = field.getType();
+ if (!(fieldTypeAmountRaw instanceof LongValueFieldType)) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName()
+ + "\": Dynamic field for " + PARAM_FIELD_SUFFIX_AMOUNT_RAW + "=\"" + fieldSuffixAmountRaw
+ + "\" must have type class extending LongValueFieldType");
+ }
+ }
+ if (null == fieldTypeCurrency) {
+ assert null != fieldSuffixCurrency : "How did we get here?";
+ SchemaField field = schema.getFieldOrNull(POLY_FIELD_SEPARATOR + fieldSuffixCurrency);
+ if (field == null) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName()
+ + "\": Undefined dynamic field for " + PARAM_FIELD_SUFFIX_CURRENCY + "=\"" + fieldSuffixCurrency + "\"");
+ }
+ fieldTypeCurrency = field.getType();
+ if (!(fieldTypeCurrency instanceof StrField)) {
+ throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName()
+ + "\": Dynamic field for " + PARAM_FIELD_SUFFIX_CURRENCY + "=\"" + fieldSuffixCurrency
+ + "\" must have type class of (or extending) StrField");
+ }
+ }
+ }
+
+ /**
+ * Load the currency config when resource loader initialized.
+ *
+ * @param resourceLoader The resource loader.
+ */
+ @Override
+ 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);
+ }
+
+ /**
+ *
+ * Returns a ValueSource over this field in which the numeric value for
+ * each document represents the indexed value as converted to the default
+ * currency for the field, normalized to its most granular form based
+ * on the default fractional digits.
+ *
+ *
+ * For example: If the default Currency specified for a field is
+ * USD, then the values returned by this value source would
+ * represent the equivilent number of "cents" (ie: value in dollars * 100)
+ * after converting each document's native currency to USD -- because the
+ * default fractional digits for USD is "2".
+ * So for a document whose indexed value was currently equivilent to
+ * "5.43,USD" using the the exchange provider for this field,
+ * this ValueSource would return a value of "543"
+ *
+ * Returns a ValueSource over this field in which the numeric value for
+ * each document represents the value from the underlying
+ * RawCurrencyValueSource as converted to the specified target
+ * Currency.
+ *
+ *
+ * For example: If the targetCurrencyCode param is set to
+ * USD, then the values returned by this value source would
+ * represent the equivilent number of dollars after converting each
+ * document's raw value to USD. So for a document whose
+ * indexed value was currently equivilent to "5.43,USD"
+ * using the the exchange provider for this field, this ValueSource would
+ * return a value of "5.43"
+ *
+ *
+ * @param targetCurrencyCode The target currency for the resulting value source, if null the defaultCurrency for this field type will be used
+ * @param source the raw ValueSource to wrap
+ * @see #PARAM_DEFAULT_CURRENCY
+ * @see #DEFAULT_DEFAULT_CURRENCY
+ * @see #getValueSource
+ */
+ public ValueSource getConvertedValueSource(String targetCurrencyCode,
+ RawCurrencyValueSource source) {
+ if (null == targetCurrencyCode) {
+ targetCurrencyCode = defaultCurrency;
+ }
+ return new ConvertedCurrencyValueSource(targetCurrencyCode,
+ source);
+ }
+
+ @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;
+
+ // ValueSourceRangeFilter doesn't check exists(), so we have to
+ final Filter docsWithValues = new QueryWrapperFilter(new FieldValueQuery(getAmountField(field).getName()));
+ final Filter vsRangeFilter = new ValueSourceRangeFilter
+ (new RawCurrencyValueSource(field, currencyCode, parser),
+ p1 == null ? null : p1.getAmount() + "",
+ p2 == null ? null : p2.getAmount() + "",
+ minInclusive, maxInclusive);
+ final BooleanQuery.Builder docsInRange = new BooleanQuery.Builder();
+ docsInRange.add(docsWithValues, Occur.FILTER);
+ docsInRange.add(vsRangeFilter, Occur.FILTER);
+
+ return new SolrConstantScoreQuery(new QueryWrapperFilter(docsInRange.build()));
+ }
+
+ @Override
+ public SortField getSortField(SchemaField field, boolean reverse) {
+ // Convert all values to default currency for sorting.
+ return (new RawCurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
+ }
+
+ @Override
+ public Type getUninversionType(SchemaField sf) {
+ return null;
+ }
+
+ @Override
+ public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException {
+ writer.writeStr(name, field.stringValue(), true);
+ }
+
+ public ExchangeRateProvider getProvider() {
+ return provider;
+ }
+
+ /**
+ *
+ * A value source whose values represent the "normal" values
+ * in the specified target currency.
+ *
+ * @see RawCurrencyValueSource
+ */
+ class ConvertedCurrencyValueSource extends ValueSource {
+ private final Currency targetCurrency;
+ private final RawCurrencyValueSource source;
+ private final double rate;
+ public ConvertedCurrencyValueSource(String targetCurrencyCode,
+ RawCurrencyValueSource source) {
+ this.source = source;
+ this.targetCurrency = getCurrency(targetCurrencyCode);
+ if (null == targetCurrency) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode);
+ }
+ // the target digits & currency of our source,
+ // become the source digits & currency of ourselves
+ this.rate = provider.getExchangeRate
+ (source.getTargetCurrency().getCurrencyCode(),
+ targetCurrency.getCurrencyCode());
+ }
+
+ @Override
+ public FunctionValues getValues(Map context, LeafReaderContext reader)
+ throws IOException {
+ final FunctionValues amounts = source.getValues(context, reader);
+ // the target digits & currency of our source,
+ // become the source digits & currency of ourselves
+ final String sourceCurrencyCode = source.getTargetCurrency().getCurrencyCode();
+ final double divisor = Math.pow(10D, targetCurrency.getDefaultFractionDigits());
+ return new FunctionValues() {
+ @Override
+ public boolean exists(int doc) throws IOException {
+ return amounts.exists(doc);
+ }
+ @Override
+ public long longVal(int doc) throws IOException {
+ return (long) doubleVal(doc);
+ }
+ @Override
+ public int intVal(int doc) throws IOException {
+ return (int) doubleVal(doc);
+ }
+
+ @Override
+ public double doubleVal(int doc) throws IOException {
+ return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor;
+ }
+
+ @Override
+ public float floatVal(int doc) throws IOException {
+ return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor);
+ }
+
+ @Override
+ public String strVal(int doc) throws IOException {
+ return Double.toString(doubleVal(doc));
+ }
+
+ @Override
+ public String toString(int doc) throws IOException {
+ return name() + '(' + strVal(doc) + ')';
+ }
+ };
+ }
+ public String name() {
+ return "currency";
+ }
+
+ @Override
+ public String description() {
+ return name() + "(" + source.getField().getName() + "," + targetCurrency.getCurrencyCode()+")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o;
+
+ return !(source != null ? !source.equals(that.source) : that.source != null) &&
+ (rate == that.rate) &&
+ !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = targetCurrency != null ? targetCurrency.hashCode() : 0;
+ result = 31 * result + (source != null ? source.hashCode() : 0);
+ result = 31 * (int) Double.doubleToLongBits(rate);
+ return result;
+ }
+ }
+
+ /**
+ *
+ * A value source whose values represent the "raw" (ie: normalized using
+ * the number of default fractional digits) values in the specified
+ * target currency).
+ *
+ *
+ * For example: if the specified target currency is "USD"
+ * then the numeric values are the number of pennies in the value
+ * (ie: $n * 100) since the number of default fractional
+ * digits for USD is "2")
+ *
+ * @see ConvertedCurrencyValueSource
+ */
+ class RawCurrencyValueSource extends ValueSource {
+ private static final long serialVersionUID = 1L;
+ private final Currency targetCurrency;
+ private ValueSource currencyValues;
+ private ValueSource amountValues;
+ private final SchemaField sf;
+
+ public RawCurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
+ this.sf = sfield;
+ this.targetCurrency = getCurrency(targetCurrencyCode);
+ if (null == targetCurrency) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode);
+ }
+
+ SchemaField amountField = getAmountField(sf);
+ SchemaField currencyField = getCurrencyField(sf);
+
+ currencyValues = currencyField.getType().getValueSource(currencyField, parser);
+ amountValues = amountField.getType().getValueSource(amountField, parser);
+ }
+
+ public SchemaField getField() { return sf; }
+ public Currency getTargetCurrency() { return targetCurrency; }
+
+ @Override
+ public FunctionValues getValues(Map context, LeafReaderContext reader) throws IOException {
+ final FunctionValues amounts = amountValues.getValues(context, reader);
+ final FunctionValues currencies = currencyValues.getValues(context, reader);
+
+ return new FunctionValues() {
+ private static 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) throws IOException {
+ 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(targetCurrency.getCurrencyCode() )) {
+ targetCurrencyOrd = currencyOrd;
+ }
+
+ return currency;
+ } else {
+ return currencies.strVal(doc);
+ }
+ }
+ /** throws a (Server Error) SolrException if the code is not valid */
+ private Currency getDocCurrency(int doc, int currencyOrd) throws IOException {
+ String code = getDocCurrencyCode(doc, currencyOrd);
+ Currency c = getCurrency(code);
+ if (null == c) {
+ throw new SolrException
+ (SolrException.ErrorCode.SERVER_ERROR,
+ "Currency code of document is not supported by this JVM: "+code);
+ }
+ return c;
+ }
+
+ @Override
+ public boolean exists(int doc) throws IOException {
+ return amounts.exists(doc);
+ }
+
+ @Override
+ public long longVal(int doc) throws IOException {
+ long amount = amounts.longVal(doc);
+ // bail fast using whatever amounts defaults to if no value
+ // (if we don't do this early, currencyOrd may be < 0,
+ // causing index bounds exception
+ if ( ! exists(doc) ) {
+ return amount;
+ }
+
+ if (!initializedCache) {
+ for (int i = 0; i < fractionDigitCache.length; i++) {
+ fractionDigitCache[i] = -1;
+ }
+
+ initializedCache = true;
+ }
+
+ int currencyOrd = currencies.ordVal(doc);
+
+ if (currencyOrd == targetCurrencyOrd) {
+ return amount;
+ }
+
+ double exchangeRate;
+ int sourceFractionDigits;
+
+ if (targetFractionDigits == -1) {
+ targetFractionDigits = targetCurrency.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, targetCurrency.getCurrencyCode());
+ }
+
+ sourceFractionDigits = fractionDigitCache[currencyOrd];
+
+ if (sourceFractionDigits == -1) {
+ sourceFractionDigits = fractionDigitCache[currencyOrd] = getDocCurrency(doc, currencyOrd).getDefaultFractionDigits();
+ }
+ } else {
+ Currency source = getDocCurrency(doc, currencyOrd);
+ exchangeRate = provider.getExchangeRate(source.getCurrencyCode(), targetCurrency.getCurrencyCode());
+ sourceFractionDigits = source.getDefaultFractionDigits();
+ }
+
+ return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits);
+ }
+
+ @Override
+ public int intVal(int doc) throws IOException {
+ return (int) longVal(doc);
+ }
+
+ @Override
+ public double doubleVal(int doc) throws IOException {
+ return (double) longVal(doc);
+ }
+
+ @Override
+ public float floatVal(int doc) throws IOException {
+ return (float) longVal(doc);
+ }
+
+ @Override
+ public String strVal(int doc) throws IOException {
+ return Long.toString(longVal(doc));
+ }
+
+ @Override
+ public String toString(int doc) throws IOException {
+ return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')';
+ }
+ };
+ }
+
+ public String name() {
+ return "rawcurrency";
+ }
+
+ @Override
+ public String description() {
+ return name() + "(" + sf.getName() +
+ ",target="+targetCurrency.getCurrencyCode()+")";
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ RawCurrencyValueSource that = (RawCurrencyValueSource) o;
+
+ return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) &&
+ !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) &&
+ !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null);
+
+ }
+
+ @Override
+ public int hashCode() {
+ int result = targetCurrency != null ? targetCurrency.hashCode() : 0;
+ result = 31 * result + (currencyValues != null ? currencyValues.hashCode() : 0);
+ result = 31 * result + (amountValues != null ? amountValues.hashCode() : 0);
+ return result;
+ }
+ }
+
+ /**
+ * Represents a Currency field value, which includes a long amount and ISO currency code.
+ */
+ static 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.
+ *
+ * Currency values are expected to be in the format <amount>,<currency code>,
+ * for example, "500,USD" would represent 5 U.S. Dollars.
+ *
+ * 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 = getCurrency(code);
+
+ if (currency == null) {
+ throw new SolrException(ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + 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(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);
+ }
+
+ @Override
+ public String toString() {
+ return String.valueOf(amount) + "," + currencyCode;
+ }
+ }
+}
+
diff --git a/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java b/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java
new file mode 100644
index 00000000000..014861793d7
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java
@@ -0,0 +1,252 @@
+/*
+ * 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.
+ */
+
+package org.apache.solr.schema;
+
+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.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.lucene.analysis.util.ResourceLoader;
+import org.apache.solr.common.SolrException;
+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;
+
+/**
+ * Configuration for currency. Provides currency exchange rates.
+ */
+class FileExchangeRateProvider implements ExchangeRateProvider {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ protected static final String PARAM_CURRENCY_CONFIG = "currencyConfig";
+
+ // Exchange rate map, maps Currency Code -> Currency Code -> Rate
+ private Map> rates = new HashMap<>();
+
+ 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
+ */
+ @Override
+ 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 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> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) {
+ Map rhs = ratesMap.get(sourceCurrencyCode);
+
+ if (rhs == null) {
+ rhs = new HashMap<>();
+ 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;
+ }
+
+ @Override
+ public String toString() {
+ return "["+this.getClass().getName()+" : " + rates.size() + " rates.]";
+ }
+
+ @Override
+ public Set listAvailableCurrencies() {
+ Set currencies = new HashSet<>();
+ 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> tmpRates = new HashMap<>();
+ try {
+ log.debug("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.SERVER_ERROR, "Exchange rate missing attributes (required: from, to, rate) " + rateNode);
+ }
+
+ String fromCurrency = from.getNodeValue();
+ String toCurrency = to.getNodeValue();
+ Double exchangeRate;
+
+ if (null == CurrencyFieldType.getCurrency(fromCurrency)) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'from' currency not supported in this JVM: " + fromCurrency);
+ }
+ if (null == CurrencyFieldType.getCurrency(toCurrency)) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'to' currency not supported in this JVM: " + toCurrency);
+ }
+
+ try {
+ exchangeRate = Double.parseDouble(rate.getNodeValue());
+ } catch (NumberFormatException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Could not parse exchange rate: " + rateNode, e);
+ }
+
+ addRate(tmpRates, fromCurrency, toCurrency, exchangeRate);
+ }
+ } catch (SAXException | XPathExpressionException | ParserConfigurationException | IOException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error parsing currency config.", e);
+ }
+ } catch (IOException e) {
+ throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "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 params) throws SolrException {
+ this.currencyConfigFile = params.get(PARAM_CURRENCY_CONFIG);
+ if(currencyConfigFile == null) {
+ throw new SolrException(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(SolrException.ErrorCode.SERVER_ERROR, "Needs ResourceLoader in order to load config file");
+ }
+ this.loader = loader;
+ reload();
+ }
+}
diff --git a/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java b/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java
index 2d16108d200..2b6cbf6b417 100644
--- a/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java
+++ b/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java
@@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory;
/**
*
- * Exchange Rates Provider for {@link CurrencyField} capable of fetching &
+ * Exchange Rates Provider for {@link CurrencyField} and {@link CurrencyFieldType} capable of fetching &
* parsing the freely available exchange rates from openexchangerates.org
*
*
diff --git a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
index b59927232a6..7d6d162ce1a 100644
--- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
+++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java
@@ -47,7 +47,7 @@ import org.apache.lucene.util.BytesRefBuilder;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.request.SolrRequestInfo;
-import org.apache.solr.schema.CurrencyField;
+import org.apache.solr.schema.CurrencyFieldType;
import org.apache.solr.schema.FieldType;
import org.apache.solr.schema.SchemaField;
import org.apache.solr.schema.StrField;
@@ -444,11 +444,11 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin {
String fieldName = fp.parseArg();
SchemaField f = fp.getReq().getSchema().getField(fieldName);
- if (! (f.getType() instanceof CurrencyField)) {
+ if (! (f.getType() instanceof CurrencyFieldType)) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
- "Currency function input must be the name of a CurrencyField: " + fieldName);
+ "Currency function input must be the name of a CurrencyFieldType: " + fieldName);
}
- CurrencyField ft = (CurrencyField) f.getType();
+ CurrencyFieldType ft = (CurrencyFieldType) f.getType();
String code = fp.hasMoreArguments() ? fp.parseArg() : null;
return ft.getConvertedValueSource(code, ft.getValueSource(f, fp));
}
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml
new file mode 100644
index 00000000000..bafdb37d722
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml
new file mode 100644
index 00000000000..06973f46fa8
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml
index 539f503eedc..a1d664cd027 100644
--- a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml
@@ -18,7 +18,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml
new file mode 100644
index 00000000000..717245533fd
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml
new file mode 100644
index 00000000000..2fba82a6aec
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml
new file mode 100644
index 00000000000..3b5c69535ac
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml
new file mode 100644
index 00000000000..27483729127
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml
new file mode 100644
index 00000000000..6afcea452e6
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml
new file mode 100644
index 00000000000..8d8533d75ed
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml
new file mode 100644
index 00000000000..77a531d5b32
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml
new file mode 100644
index 00000000000..89c70801055
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml
new file mode 100644
index 00000000000..9e95458331d
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml
new file mode 100644
index 00000000000..51881901279
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml
new file mode 100644
index 00000000000..c3eadc1e019
--- /dev/null
+++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ id
+
+
diff --git a/solr/core/src/test-files/solr/collection1/conf/schema.xml b/solr/core/src/test-files/solr/collection1/conf/schema.xml
index 23c104547f8..4aaef4842dd 100644
--- a/solr/core/src/test-files/solr/collection1/conf/schema.xml
+++ b/solr/core/src/test-files/solr/collection1/conf/schema.xml
@@ -451,6 +451,17 @@
multiValued="false"
providerClass="solr.OpenExchangeRatesOrgProvider"
ratesFileLocation="open-exchange-rates.json"/>
+
+
+
@@ -539,6 +550,9 @@
+
+
+
@@ -652,8 +666,10 @@
+
+
diff --git a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java
index b7d00a9c371..b9dc1aae0a1 100644
--- a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java
@@ -68,14 +68,22 @@ public class BadIndexSchemaTest extends AbstractBadConfigTestBase {
doTest("bad-schema-currency-ft-multivalued.xml",
"types can not be multiValued: currency");
doTest("bad-schema-currency-multivalued.xml",
- "Fields can not be multiValued: money");
+ "fields can not be multiValued: money");
doTest("bad-schema-currency-dynamic-multivalued.xml",
- "Fields can not be multiValued: *_c");
+ "fields can not be multiValued: *_c");
+ doTest("bad-schema-currencyfieldtype-ft-multivalued.xml",
+ "types can not be multiValued: currency");
+ doTest("bad-schema-currencyfieldtype-multivalued.xml",
+ "fields can not be multiValued: money");
+ doTest("bad-schema-currencyfieldtype-dynamic-multivalued.xml",
+ "fields can not be multiValued: *_c");
}
public void testCurrencyOERNoRates() throws Exception {
doTest("bad-schema-currency-ft-oer-norates.xml",
"ratesFileLocation");
+ doTest("bad-schema-currencyfieldtype-ft-oer-norates.xml",
+ "ratesFileLocation");
}
public void testCurrencyBogusCode() throws Exception {
@@ -83,6 +91,35 @@ public class BadIndexSchemaTest extends AbstractBadConfigTestBase {
"HOSS");
doTest("bad-schema-currency-ft-bogus-code-in-xml.xml",
"HOSS");
+ doTest("bad-schema-currencyfieldtype-ft-bogus-default-code.xml",
+ "HOSS");
+ doTest("bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml",
+ "HOSS");
+ }
+
+ public void testCurrencyDisallowedSuffixParams() throws Exception {
+ doTest("bad-schema-currency-ft-code-suffix.xml",
+ "Unknown parameter(s)");
+ doTest("bad-schema-currency-ft-amount-suffix.xml",
+ "Unknown parameter(s)");
+ }
+
+ public void testCurrencyBogusSuffixes() throws Exception {
+ doTest("bad-schema-currencyfieldtype-bogus-code-suffix.xml",
+ "Undefined dynamic field for codeStrSuffix");
+ doTest("bad-schema-currencyfieldtype-bogus-amount-suffix.xml",
+ "Undefined dynamic field for amountLongSuffix");
+ doTest("bad-schema-currencyfieldtype-wrong-code-ft.xml",
+ "Dynamic field for codeStrSuffix=\"_l\" must have type class of (or extending) StrField");
+ doTest("bad-schema-currencyfieldtype-wrong-amount-ft.xml",
+ "Dynamic field for amountLongSuffix=\"_s\" must have type class extending LongValueFieldType");
+ }
+
+ public void testCurrencyMissingSuffixes() throws Exception {
+ doTest("bad-schema-currencyfieldtype-missing-code-suffix.xml",
+ "Missing required param codeStrSuffix");
+ doTest("bad-schema-currencyfieldtype-missing-amount-suffix.xml",
+ "Missing required param amountLongSuffix");
}
public void testPerFieldtypeSimButNoSchemaSimFactory() throws Exception {
diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java
deleted file mode 100644
index fed51eb9aac..00000000000
--- a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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.
- */
-package org.apache.solr.schema;
-
-/**
- * Tests currency field type using OpenExchangeRatesOrgProvider.
- */
-public class CurrencyFieldOpenExchangeTest extends AbstractCurrencyFieldTest {
-
- public String field() {
- return "oer_amount";
- }
-}
diff --git a/solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
similarity index 53%
rename from solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java
rename to solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
index be719dbd8e3..c2f8f2d97e5 100644
--- a/solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java
+++ b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java
@@ -14,12 +14,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
package org.apache.solr.schema;
+
+import java.util.Arrays;
import java.util.Currency;
import java.util.List;
import java.util.Random;
import java.util.Set;
+import com.carrotsearch.randomizedtesting.annotations.ParametersFactory;
import org.apache.lucene.index.IndexableField;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.core.SolrCore;
@@ -29,13 +33,28 @@ import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
-/**
- * Tests currency field type.
- * @see #field
- */
-@Ignore("Abstract base class with test methods")
-public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 {
+/** Tests CurrencyField and CurrencyFieldType. */
+public class CurrencyFieldTypeTest extends SolrTestCaseJ4 {
+ private final String fieldName;
+ private final Class extends ExchangeRateProvider> expectedProviderClass;
+
+ public CurrencyFieldTypeTest(String fieldName, Class extends ExchangeRateProvider> expectedProviderClass) {
+ this.fieldName = fieldName;
+ this.expectedProviderClass = expectedProviderClass;
+ }
+ @ParametersFactory
+ public static Iterable