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:40 UTC

[3/6] lucene-solr:branch_6x: 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.

 Conflicts:
	solr/core/src/java/org/apache/solr/schema/CurrencyField.java
	solr/core/src/test-files/solr/collection1/conf/schema.xml


Project: http://git-wip-us.apache.org/repos/asf/lucene-solr/repo
Commit: http://git-wip-us.apache.org/repos/asf/lucene-solr/commit/5232b2ce
Tree: http://git-wip-us.apache.org/repos/asf/lucene-solr/tree/5232b2ce
Diff: http://git-wip-us.apache.org/repos/asf/lucene-solr/diff/5232b2ce

Branch: refs/heads/branch_6x
Commit: 5232b2ce5a6c92f57a75031ffb273c799ab7f5c7
Parents: 2ad8a6a
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:11 2017 -0400

----------------------------------------------------------------------
 solr/CHANGES.txt                                |    6 +
 .../org/apache/solr/schema/CurrencyField.java   | 1001 +-----------------
 .../apache/solr/schema/CurrencyFieldType.java   |  831 +++++++++++++++
 .../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, 2333 insertions(+), 1540 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/lucene-solr/blob/5232b2ce/solr/CHANGES.txt
----------------------------------------------------------------------
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 2403042..094f680 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -41,6 +41,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
 ----------------------
 
@@ -188,6 +190,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/5232b2ce/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 0b31123..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,191 +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.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, float boost) {
-    CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
-
-    List<IndexableField> f = new ArrayList<>();
-    SchemaField amountField = getAmountField(field);
-    f.add(amountField.createField(String.valueOf(value.getAmount()), amountField.indexed() && !amountField.omitNorms() ? boost : 1F));
-    SchemaField currencyField = getCurrencyField(field);
-    f.add(currencyField.createField(value.getCurrencyCode(), currencyField.indexed() && !currencyField.omitNorms() ? boost : 1F));
-
-    if (field.stored()) {
-      org.apache.lucene.document.FieldType customType = new org.apache.lucene.document.FieldType();
-      assert !customType.omitNorms();
-      customType.setStored(true);
-      String storedValue = externalVal.toString().trim();
-      if (storedValue.indexOf(",") < 0) {
-        storedValue += "," + defaultCurrency;
-      }
-      f.add(createField(field.getName(), storedValue, customType, 1F));
-    }
-
-    return f;
-  }
-
-  private SchemaField getAmountField(SchemaField field) {
-    return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
-  }
+    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) {
@@ -214,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) {
-          return amounts.exists(doc);
-        }
-        @Override
-        public long longVal(int doc) {
-          return (long) doubleVal(doc);
-        }
-        @Override
-        public int intVal(int doc) {
-          return (int) doubleVal(doc);
-        }
-
-        @Override
-        public double doubleVal(int doc) {
-          return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor;
-        }
-
-        @Override
-        public float floatVal(int doc) {
-          return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor);
-        }
-
-        @Override
-        public String strVal(int doc) {
-          return Double.toString(doubleVal(doc));
-        }
-
-        @Override
-        public String toString(int doc) {
-          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 final int MAX_CURRENCIES_TO_CACHE = 256;
-        private final int[] fractionDigitCache = new int[MAX_CURRENCIES_TO_CACHE];
-        private final String[] currencyOrdToCurrencyCache = new String[MAX_CURRENCIES_TO_CACHE];
-        private final double[] exchangeRateCache = new double[MAX_CURRENCIES_TO_CACHE];
-        private int targetFractionDigits = -1;
-        private int targetCurrencyOrd = -1;
-        private boolean initializedCache;
-
-        private String getDocCurrencyCode(int doc, int currencyOrd) {
-          if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
-            String currency = currencyOrdToCurrencyCache[currencyOrd];
-
-            if (currency == null) {
-              currencyOrdToCurrencyCache[currencyOrd] = currency = currencies.strVal(doc);
-            }
-            
-            if (currency == null) {
-              currency = defaultCurrency;
-            }
-
-            if (targetCurrencyOrd == -1 && 
-                currency.equals(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) {
-          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) {
-          return amounts.exists(doc);
-        }
-        
-        @Override
-        public long longVal(int doc) {
-          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) {
-          return (int) longVal(doc);
-        }
-
-        @Override
-        public double doubleVal(int doc) {
-          return (double) longVal(doc);
-        }
-
-        @Override
-        public float floatVal(int doc) {
-          return (float) longVal(doc);
-        }
-
-        @Override
-        public String strVal(int doc) {
-          return Long.toString(longVal(doc));
-        }
-
-        @Override
-        public String toString(int doc) {
-          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 &lt;amount&gt;,&lt;currency code&gt;,
-   * 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/5232b2ce/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..4bfffc9
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java
@@ -0,0 +1,831 @@
+/*
+ * 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.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, float boost) {
+    CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
+
+    List<IndexableField> f = new ArrayList<>();
+    SchemaField amountField = getAmountField(field);
+    f.add(amountField.createField(String.valueOf(value.getAmount()), amountField.indexed() && !amountField.omitNorms() ? boost : 1F));
+    SchemaField currencyField = getCurrencyField(field);
+    f.add(currencyField.createField(value.getCurrencyCode(), currencyField.indexed() && !currencyField.omitNorms() ? boost : 1F));
+
+    if (field.stored()) {
+      org.apache.lucene.document.FieldType customType = new org.apache.lucene.document.FieldType();
+      assert !customType.omitNorms();
+      customType.setStored(true);
+      String storedValue = externalVal.toString().trim();
+      if (storedValue.indexOf(",") < 0) {
+        storedValue += "," + defaultCurrency;
+      }
+      f.add(createField(field.getName(), storedValue, customType, 1F));
+    }
+
+    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) {
+          return amounts.exists(doc);
+        }
+        @Override
+        public long longVal(int doc) {
+          return (long) doubleVal(doc);
+        }
+        @Override
+        public int intVal(int doc) {
+          return (int) doubleVal(doc);
+        }
+
+        @Override
+        public double doubleVal(int doc) {
+          return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor;
+        }
+
+        @Override
+        public float floatVal(int doc) {
+          return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor);
+        }
+
+        @Override
+        public String strVal(int doc) {
+          return Double.toString(doubleVal(doc));
+        }
+
+        @Override
+        public String toString(int doc) {
+          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) {
+          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) {
+          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) {
+          return amounts.exists(doc);
+        }
+
+        @Override
+        public long longVal(int doc) {
+          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) {
+          return (int) longVal(doc);
+        }
+
+        @Override
+        public double doubleVal(int doc) {
+          return (double) longVal(doc);
+        }
+
+        @Override
+        public float floatVal(int doc) {
+          return (float) longVal(doc);
+        }
+
+        @Override
+        public String strVal(int doc) {
+          return Long.toString(longVal(doc));
+        }
+
+        @Override
+        public String toString(int doc) {
+          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 &lt;amount&gt;,&lt;currency code&gt;,
+     * 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/5232b2ce/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();
+  }
+}