You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by sa...@apache.org on 2017/06/23 14:14:43 UTC
[6/6] lucene-solr:master: 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.
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.
Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/a6f4f2dd
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/a6f4f2dd
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/a6f4f2dd
Branch: refs/heads/master
Commit: a6f4f2dd9ce33a0aa3597e73a5ced0b96302c847
Parents: af1ee47
Author: Steve Rowe <sa...@apache.org>
Authored: Thu Jun 22 21:22:42 2017 -0400
Committer: Steve Rowe <sa...@apache.org>
Committed: Fri Jun 23 10:14:28 2017 -0400
----------------------------------------------------------------------
solr/CHANGES.txt | 6 +
.../org/apache/solr/schema/CurrencyField.java | 999 +------------------
.../apache/solr/schema/CurrencyFieldType.java | 829 +++++++++++++++
.../solr/schema/FileExchangeRateProvider.java | 252 +++++
.../schema/OpenExchangeRatesOrgProvider.java | 2 +-
.../apache/solr/search/ValueSourceParser.java | 8 +-
.../bad-schema-currency-ft-amount-suffix.xml | 34 +
.../conf/bad-schema-currency-ft-code-suffix.xml | 33 +
.../conf/bad-schema-currency-ft-oer-norates.xml | 2 +-
...ma-currencyfieldtype-bogus-amount-suffix.xml | 34 +
...hema-currencyfieldtype-bogus-code-suffix.xml | 35 +
...ma-currencyfieldtype-dynamic-multivalued.xml | 36 +
...a-currencyfieldtype-ft-bogus-code-in-xml.xml | 41 +
...-currencyfieldtype-ft-bogus-default-code.xml | 41 +
...-schema-currencyfieldtype-ft-multivalued.xml | 36 +
...-schema-currencyfieldtype-ft-oer-norates.xml | 40 +
...-currencyfieldtype-missing-amount-suffix.xml | 34 +
...ma-currencyfieldtype-missing-code-suffix.xml | 35 +
...bad-schema-currencyfieldtype-multivalued.xml | 36 +
...schema-currencyfieldtype-wrong-amount-ft.xml | 36 +
...d-schema-currencyfieldtype-wrong-code-ft.xml | 35 +
.../test-files/solr/collection1/conf/schema.xml | 16 +
.../solr/schema/AbstractCurrencyFieldTest.java | 426 --------
.../apache/solr/schema/BadIndexSchemaTest.java | 41 +-
.../schema/CurrencyFieldOpenExchangeTest.java | 27 -
.../solr/schema/CurrencyFieldTypeTest.java | 494 +++++++++
.../solr/schema/CurrencyFieldXmlFileTest.java | 42 -
.../OpenExchangeRatesOrgProviderTest.java | 2 +-
.../example-DIH/solr/db/conf/managed-schema | 27 +-
.../example-DIH/solr/mail/conf/managed-schema | 27 +-
.../example-DIH/solr/solr/conf/currency.xml | 2 +-
.../example-DIH/solr/solr/conf/managed-schema | 27 +-
solr/example/files/conf/managed-schema | 4 +-
.../basic_configs/conf/managed-schema | 31 +-
.../conf/currency.xml | 2 +-
.../conf/managed-schema | 31 +-
.../conf/managed-schema | 27 +-
.../src/field-types-included-with-solr.adoc | 3 +-
.../src/updating-parts-of-documents.adoc | 2 +-
...king-with-currencies-and-exchange-rates.adoc | 34 +-
40 files changed, 2331 insertions(+), 1538 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a6f4f2dd/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 6477ada..85535c3 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
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a6f4f2dd/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
----------------------------------------------------------------------
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 286d2c1..e2676fe 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.
* <p>
* See <a href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org/solr/CurrencyField</a>
+ * @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 <code>Currency.getInstance</code> that returns null
- * instead of throwing <code>IllegalArgumentException</code>
- * 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<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;
+ // Fail if amountLongSuffix or codeStrSuffix are specified
+ List<String> unknownParams = new ArrayList<>();
+ fieldSuffixAmountRaw = args.get(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
+ if (fieldSuffixAmountRaw != null) {
+ unknownParams.add(PARAM_FIELD_SUFFIX_AMOUNT_RAW);
}
-
- if (null == getCurrency(this.defaultCurrency)) {
- throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Default currency code is not supported by this JVM: " + this.defaultCurrency);
+ 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);
+ }
+
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<String,String> 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<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 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<IndexableField> createFields(SchemaField field, Object externalVal) {
- CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
-
- List<IndexableField> 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);
- }
+ fieldTypeCurrency.setTypeName(FIELD_TYPE_CURRENCY);
+ fieldTypeCurrency.init(schema, Collections.emptyMap());
+ fieldSuffixCurrency = FIELD_SUFFIX_CURRENCY;
- 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);
- }
-
- /**
- * <p>
- * 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.
- * </p>
- * <p>
- * For example: If the default Currency specified for a field is
- * <code>USD</code>, 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 <code>USD</code> is "<code>2</code>".
- * So for a document whose indexed value was currently equivilent to
- * "<code>5.43,USD</code>" using the the exchange provider for this field,
- * this ValueSource would return a value of "<code>543</code>"
- * </p>
- *
- * @see #PARAM_DEFAULT_CURRENCY
- * @see #DEFAULT_DEFAULT_CURRENCY
- * @see Currency#getDefaultFractionDigits
- * @see #getConvertedValueSource
- */
- public RawCurrencyValueSource getValueSource(SchemaField field,
- QParser parser) {
- getAmountField(field).checkFieldCacheSource();
- getCurrencyField(field).checkFieldCacheSource();
- return new RawCurrencyValueSource(field, defaultCurrency, parser);
- }
-
- /**
- * <p>
- * Returns a ValueSource over this field in which the numeric value for
- * each document represents the value from the underlying
- * <code>RawCurrencyValueSource</code> as converted to the specified target
- * Currency.
- * </p>
- * <p>
- * For example: If the <code>targetCurrencyCode</code> param is set to
- * <code>USD</code>, then the values returned by this value source would
- * represent the equivilent number of dollars after converting each
- * document's raw value to <code>USD</code>. So for a document whose
- * indexed value was currently equivilent to "<code>5.43,USD</code>"
- * using the the exchange provider for this field, this ValueSource would
- * return a value of "<code>5.43</code>"
- * </p>
- *
- * @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;
- }
-
- /**
- * <p>
- * A value source whose values represent the "normal" values
- * in the specified target currency.
- * </p>
- * @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;
- }
- }
-
- /**
- * <p>
- * A value source whose values represent the "raw" (ie: normalized using
- * the number of default fractional digits) values in the specified
- * target currency).
- * </p>
- * <p>
- * For example: if the specified target currency is "<code>USD</code>"
- * then the numeric values are the number of pennies in the value
- * (ie: <code>$n * 100</code>) since the number of defalt fractional
- * digits for <code>USD</code> is "<code>2</code>")
- * </p>
- * @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<String, Map<String, Double>> 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<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<>();
- 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<String> listAvailableCurrencies() {
- Set<String> 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<String, Map<String, Double>> 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<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.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.
- * <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 = 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);
}
}
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a6f4f2dd/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
----------------------------------------------------------------------
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 0000000..0ace37e
--- /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.
+ * <p>
+ * See <a href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org/solr/CurrencyField</a>
+ */
+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 <code>Currency.getInstance</code> that returns null
+ * instead of throwing <code>IllegalArgumentException</code>
+ * 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<String, String> 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<IndexableField> createFields(SchemaField field, Object externalVal) {
+ CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
+
+ List<IndexableField> 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);
+ }
+
+ /**
+ * <p>
+ * 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.
+ * </p>
+ * <p>
+ * For example: If the default Currency specified for a field is
+ * <code>USD</code>, 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 <code>USD</code> is "<code>2</code>".
+ * So for a document whose indexed value was currently equivilent to
+ * "<code>5.43,USD</code>" using the the exchange provider for this field,
+ * this ValueSource would return a value of "<code>543</code>"
+ * </p>
+ *
+ * @see #PARAM_DEFAULT_CURRENCY
+ * @see #DEFAULT_DEFAULT_CURRENCY
+ * @see Currency#getDefaultFractionDigits
+ * @see #getConvertedValueSource
+ */
+ public RawCurrencyValueSource getValueSource(SchemaField field,
+ QParser parser) {
+ getAmountField(field).checkFieldCacheSource();
+ getCurrencyField(field).checkFieldCacheSource();
+ return new RawCurrencyValueSource(field, defaultCurrency, parser);
+ }
+
+ /**
+ * <p>
+ * Returns a ValueSource over this field in which the numeric value for
+ * each document represents the value from the underlying
+ * <code>RawCurrencyValueSource</code> as converted to the specified target
+ * Currency.
+ * </p>
+ * <p>
+ * For example: If the <code>targetCurrencyCode</code> param is set to
+ * <code>USD</code>, then the values returned by this value source would
+ * represent the equivilent number of dollars after converting each
+ * document's raw value to <code>USD</code>. So for a document whose
+ * indexed value was currently equivilent to "<code>5.43,USD</code>"
+ * using the the exchange provider for this field, this ValueSource would
+ * return a value of "<code>5.43</code>"
+ * </p>
+ *
+ * @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;
+ }
+
+ /**
+ * <p>
+ * A value source whose values represent the "normal" values
+ * in the specified target currency.
+ * </p>
+ * @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;
+ }
+ }
+
+ /**
+ * <p>
+ * A value source whose values represent the "raw" (ie: normalized using
+ * the number of default fractional digits) values in the specified
+ * target currency).
+ * </p>
+ * <p>
+ * For example: if the specified target currency is "<code>USD</code>"
+ * then the numeric values are the number of pennies in the value
+ * (ie: <code>$n * 100</code>) since the number of default fractional
+ * digits for <code>USD</code> is "<code>2</code>")
+ * </p>
+ * @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.
+ * <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 = 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;
+ }
+ }
+}
+
http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/a6f4f2dd/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java
----------------------------------------------------------------------
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 0000000..0148617
--- /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<String, Map<String, Double>> 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<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<>();
+ 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<String> listAvailableCurrencies() {
+ Set<String> 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<String, Map<String, Double>> 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<String,String> 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();
+ }
+}