You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@lucene.apache.org by ja...@apache.org on 2012/03/09 23:40:06 UTC

svn commit: r1299083 - in /lucene/dev/trunk/solr: ./ core/src/java/org/apache/solr/schema/ core/src/test-files/solr/conf/ core/src/test/org/apache/solr/cloud/ core/src/test/org/apache/solr/schema/ example/exampledocs/ example/solr/conf/ example/solr/co...

Author: janhoy
Date: Fri Mar  9 22:40:06 2012
New Revision: 1299083

URL: http://svn.apache.org/viewvc?rev=1299083&view=rev
Log:
SOLR-2202: Money/Currency FieldType

Added:
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/CurrencyField.java   (with props)
    lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java   (with props)
    lucene/dev/trunk/solr/core/src/test-files/solr/conf/currency.xml   (with props)
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java   (with props)
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java   (with props)
    lucene/dev/trunk/solr/example/exampledocs/money.xml   (with props)
    lucene/dev/trunk/solr/example/solr/conf/currency.xml   (with props)
Modified:
    lucene/dev/trunk/solr/CHANGES.txt
    lucene/dev/trunk/solr/core/src/test-files/solr/conf/schema.xml
    lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java
    lucene/dev/trunk/solr/example/solr/conf/schema.xml
    lucene/dev/trunk/solr/example/solr/conf/velocity/doc.vm

Modified: lucene/dev/trunk/solr/CHANGES.txt
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/CHANGES.txt?rev=1299083&r1=1299082&r2=1299083&view=diff
==============================================================================
--- lucene/dev/trunk/solr/CHANGES.txt (original)
+++ lucene/dev/trunk/solr/CHANGES.txt Fri Mar  9 22:40:06 2012
@@ -234,6 +234,9 @@ New Features
 
 * SOLR-2898: Support grouped faceting. (Martijn van Groningen)
 
+* SOLR-2202: Currency FieldType, whith support for currencies and exchange rates
+  (Greg Fodor & Andrew Morrison via janhoy, rmuir, Uwe Schindler)
+
 Optimizations
 ----------------------
 

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/CurrencyField.java?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/CurrencyField.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/CurrencyField.java Fri Mar  9 22:40:06 2012
@@ -0,0 +1,751 @@
+package org.apache.solr.schema;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.AtomicReaderContext;
+import org.apache.lucene.index.IndexableField;
+import org.apache.lucene.queries.function.FunctionValues;
+import org.apache.lucene.queries.function.ValueSource;
+import org.apache.lucene.search.Query;
+import org.apache.lucene.search.SortField;
+import org.apache.solr.common.ResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+import org.apache.solr.response.TextResponseWriter;
+import org.apache.solr.response.XMLWriter;
+import org.apache.solr.search.QParser;
+import org.apache.solr.search.SolrConstantScoreQuery;
+import org.apache.solr.search.function.ValueSourceRangeFilter;
+import org.apache.solr.util.plugin.ResourceLoaderAware;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+import org.xml.sax.SAXException;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpressionException;
+import javax.xml.xpath.XPathFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Currency;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Field type for support of monetary values.
+ * <p>
+ * See <a href="http://wiki.apache.org/solr/CurrencyField">http://wiki.apache.org/solr/CurrencyField</a>
+ */
+public class CurrencyField extends FieldType implements SchemaAware, ResourceLoaderAware {
+  protected static final String PARAM_DEFAULT_CURRENCY      = "defaultCurrency";
+  protected static final String PARAM_RATE_PROVIDER_CLASS   = "providerClass";
+  protected static final String DEFAULT_RATE_PROVIDER_CLASS = "org.apache.solr.schema.FileExchangeRateProvider";
+  protected static final String DEFAULT_DEFAULT_CURRENCY    = "USD";
+  protected static final String FIELD_SUFFIX_AMOUNT_RAW     = "_amount_raw";
+  protected static final String FIELD_SUFFIX_CURRENCY       = "_currency";
+  protected static final String FIELD_TYPE_CURRENCY         = "string";
+  protected static final String FIELD_TYPE_AMOUNT_RAW       = "tlong";
+
+  private IndexSchema schema;
+  private String exchangeRateProviderClass;
+  private String defaultCurrency;
+  private ExchangeRateProvider provider;
+  public static Logger log = LoggerFactory.getLogger(CurrencyField.class);
+
+  @Override
+  protected void init(IndexSchema schema, Map<String, String> args) {
+    super.init(schema, args);
+    this.schema = schema;
+    this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS);
+    this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY);
+
+    if (this.defaultCurrency == null) {
+      this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY;
+    }
+    
+    if (this.exchangeRateProviderClass == null) {
+      this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS;
+    }
+
+    if (java.util.Currency.getInstance(this.defaultCurrency) == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + this.defaultCurrency);
+    }
+
+    args.remove(PARAM_RATE_PROVIDER_CLASS);
+    args.remove(PARAM_DEFAULT_CURRENCY);
+    
+    try {
+      // TODO: Are we using correct classloader?
+      Class<?> c = Class.forName(exchangeRateProviderClass);
+      Object clazz = c.newInstance();
+      if (clazz instanceof ExchangeRateProvider) {
+        provider = (ExchangeRateProvider) clazz;
+        provider.init(args);
+      } else {
+        throw new SolrException(ErrorCode.BAD_REQUEST, "exchangeRateProvider "+exchangeRateProviderClass+" needs to implement ExchangeRateProvider");
+      }
+    } catch (Exception e) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Error instansiating exhange rate provider "+exchangeRateProviderClass+". Please check your FieldType configuration", e);
+    }
+  }
+
+  @Override
+  public boolean isPolyField() {
+    return true;
+  }
+
+  @Override
+  public IndexableField[] createFields(SchemaField field, Object externalVal, float boost) {
+    CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency);
+
+    IndexableField[] f = new IndexableField[field.stored() ? 3 : 2];
+    f[0] = getAmountField(field).createField(String.valueOf(value.getAmount()), boost);
+    f[1] = getCurrencyField(field).createField(value.getCurrencyCode(), boost);
+
+    if (field.stored()) {
+      org.apache.lucene.document.FieldType customType = new org.apache.lucene.document.FieldType();
+      customType.setStored(true);
+      String storedValue = externalVal.toString().trim();
+      if (storedValue.indexOf(",") < 0) {
+        storedValue += "," + defaultCurrency;
+      }
+      f[2] = createField(field.getName(), storedValue, customType, boost);
+    }
+
+    return f;
+  }
+
+  private SchemaField getAmountField(SchemaField field) {
+    return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
+  }
+
+  private SchemaField getCurrencyField(SchemaField field) {
+    return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY);
+  }
+
+  private void createDynamicCurrencyField(String suffix, String fieldType) {
+    String name = "*" + POLY_FIELD_SEPARATOR + suffix;
+    Map<String, String> props = new HashMap<String, String>();
+    props.put("indexed", "true");
+    props.put("stored", "false");
+    props.put("multiValued", "false");
+    org.apache.solr.schema.FieldType type = schema.getFieldTypeByName(fieldType);
+    int p = SchemaField.calcProps(name, type, props);
+    schema.registerDynamicField(SchemaField.create(name, type, p, null));
+  }
+
+  /**
+   * When index schema is informed, add dynamic fields.
+   *
+   * @param indexSchema The index schema.
+   */
+  public void inform(IndexSchema indexSchema) {
+    // TODO: Should we allow configurable field-types or in another way not be dependent on static type names types in schema?
+    createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, FIELD_TYPE_CURRENCY);
+    createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, FIELD_TYPE_AMOUNT_RAW);
+  }
+
+  /**
+   * Load the currency config when resource loader initialized.
+   *
+   * @param resourceLoader The resource loader.
+   */
+  public void inform(ResourceLoader resourceLoader) {
+    provider.inform(resourceLoader);
+    boolean reloaded = provider.reload();
+    if(!reloaded) {
+      log.warn("Failed reloading currencies");
+    }
+  }
+
+  @Override
+  public Query getFieldQuery(QParser parser, SchemaField field, String externalVal) {
+    CurrencyValue value = CurrencyValue.parse(externalVal, defaultCurrency);
+    CurrencyValue valueDefault;
+    valueDefault = value.convertTo(provider, defaultCurrency);
+
+    return getRangeQuery(parser, field, valueDefault, valueDefault, true, true);
+  }
+
+  @Override
+  public Query getRangeQuery(QParser parser, SchemaField field, String part1, String part2, final boolean minInclusive, final boolean maxInclusive) {
+      final CurrencyValue p1 = CurrencyValue.parse(part1, defaultCurrency);
+      final CurrencyValue p2 = CurrencyValue.parse(part2, defaultCurrency);
+
+      if (!p1.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.getCurrencyCode();
+    final CurrencyValueSource vs = new CurrencyValueSource(field, currencyCode, parser);
+
+    return new SolrConstantScoreQuery(new ValueSourceRangeFilter(vs,
+            p1.getAmount() + "", p2.getAmount() + "", minInclusive, maxInclusive));
+  }
+
+  @Override
+  public SortField getSortField(SchemaField field, boolean reverse) {
+    try {
+      // Convert all values to default currency for sorting.
+      return (new CurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse);
+    } catch (IOException e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+    }
+  }
+
+  public void write(XMLWriter xmlWriter, String name, IndexableField field) throws IOException {
+    xmlWriter.writeStr(name, field.stringValue(), false);
+  }
+
+  @Override
+  public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException {
+    writer.writeStr(name, field.stringValue(), false);
+  }
+
+  public ExchangeRateProvider getProvider() {
+    return provider;
+  }
+
+  class CurrencyValueSource extends ValueSource {
+    private static final long serialVersionUID = 1L;
+    private String targetCurrencyCode;
+    private ValueSource currencyValues;
+    private ValueSource amountValues;
+    private final SchemaField sf;
+
+    public CurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) {
+      this.sf = sfield;
+      this.targetCurrencyCode = targetCurrencyCode;
+
+      SchemaField amountField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_AMOUNT_RAW);
+      SchemaField currencyField = schema.getField(sf.getName() + POLY_FIELD_SEPARATOR + FIELD_SUFFIX_CURRENCY);
+
+      currencyValues = currencyField.getType().getValueSource(currencyField, parser);
+      amountValues = amountField.getType().getValueSource(amountField, parser);
+    }
+
+    public FunctionValues getValues(Map context, AtomicReaderContext reader) throws IOException {
+      final FunctionValues amounts = amountValues.getValues(context, reader);
+      final FunctionValues currencies = currencyValues.getValues(context, reader);
+
+      return new FunctionValues() {
+        private final int MAX_CURRENCIES_TO_CACHE = 256;
+        private final int[] fractionDigitCache = new int[MAX_CURRENCIES_TO_CACHE];
+        private final String[] currencyOrdToCurrencyCache = new String[MAX_CURRENCIES_TO_CACHE];
+        private final double[] exchangeRateCache = new double[MAX_CURRENCIES_TO_CACHE];
+        private int targetFractionDigits = -1;
+        private int targetCurrencyOrd = -1;
+        private boolean initializedCache;
+
+        private String getDocCurrencyCode(int doc, int currencyOrd) {
+          if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
+            String currency = currencyOrdToCurrencyCache[currencyOrd];
+
+            if (currency == null) {
+              currencyOrdToCurrencyCache[currencyOrd] = currency = currencies.strVal(doc);
+            }
+            
+            if (currency == null) {
+              currency = defaultCurrency;
+            }
+
+            if (targetCurrencyOrd == -1 && currency.equals(targetCurrencyCode)) {
+              targetCurrencyOrd = currencyOrd;
+            }
+
+            return currency;
+          } else {
+            return currencies.strVal(doc);
+          }
+        }
+
+        public long longVal(int doc) {
+          if (!initializedCache) {
+            for (int i = 0; i < fractionDigitCache.length; i++) {
+              fractionDigitCache[i] = -1;
+            }
+
+            initializedCache = true;
+          }
+
+          long amount = amounts.longVal(doc);
+          int currencyOrd = currencies.ordVal(doc);
+
+          if (currencyOrd == targetCurrencyOrd) {
+            return amount;
+          }
+
+          double exchangeRate;
+          int sourceFractionDigits;
+
+          if (targetFractionDigits == -1) {
+            targetFractionDigits = Currency.getInstance(targetCurrencyCode).getDefaultFractionDigits();
+          }
+
+          if (currencyOrd < MAX_CURRENCIES_TO_CACHE) {
+            exchangeRate = exchangeRateCache[currencyOrd];
+
+            if (exchangeRate <= 0.0) {
+              String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
+              exchangeRate = exchangeRateCache[currencyOrd] = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+            }
+
+            sourceFractionDigits = fractionDigitCache[currencyOrd];
+
+            if (sourceFractionDigits == -1) {
+              String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
+              sourceFractionDigits = fractionDigitCache[currencyOrd] = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+            }
+          } else {
+            String sourceCurrencyCode = getDocCurrencyCode(doc, currencyOrd);
+            exchangeRate = provider.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+            sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+          }
+
+          return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits);
+        }
+
+        public int intVal(int doc) {
+          return (int) longVal(doc);
+        }
+
+        public double doubleVal(int doc) {
+          return (double) longVal(doc);
+        }
+
+        public float floatVal(int doc) {
+          return (float) longVal(doc);
+        }
+
+        public String strVal(int doc) {
+          return Long.toString(longVal(doc));
+        }
+
+        public String toString(int doc) {
+          return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')';
+        }
+      };
+    }
+
+    public String name() {
+      return "currency";
+    }
+
+    @Override
+    public String description() {
+      return name() + "(" + sf.getName() + ")";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+
+      CurrencyValueSource that = (CurrencyValueSource) o;
+
+      return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) &&
+              !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) &&
+              !(targetCurrencyCode != null ? !targetCurrencyCode.equals(that.targetCurrencyCode) : that.targetCurrencyCode != null);
+
+    }
+
+    @Override
+    public int hashCode() {
+      int result = targetCurrencyCode != null ? targetCurrencyCode.hashCode() : 0;
+      result = 31 * result + (currencyValues != null ? currencyValues.hashCode() : 0);
+      result = 31 * result + (amountValues != null ? amountValues.hashCode() : 0);
+      return result;
+    }
+  }
+}
+
+/**
+ * Configuration for currency. Provides currency exchange rates.
+ */
+class FileExchangeRateProvider implements ExchangeRateProvider {
+  public static Logger log = LoggerFactory.getLogger(FileExchangeRateProvider.class);
+  protected static final String PARAM_CURRENCY_CONFIG       = "currencyConfig";
+
+  // Exchange rate map, maps Currency Code -> Currency Code -> Rate
+  private Map<String, Map<String, Double>> rates = new HashMap<String, Map<String, Double>>();
+
+  private String currencyConfigFile;
+  private ResourceLoader loader;
+
+  /**
+   * Returns the currently known exchange rate between two currencies. If a direct rate has been loaded,
+   * it is used. Otherwise, if a rate is known to convert the target currency to the source, the inverse
+   * exchange rate is computed.
+   *
+   * @param sourceCurrencyCode The source currency being converted from.
+   * @param targetCurrencyCode The target currency being converted to.
+   * @return The exchange rate.
+   * @throws an exception if the requested currency pair cannot be found 
+   */
+  public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) {
+    if (sourceCurrencyCode == null || targetCurrencyCode == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Cannot get exchange rate; currency was null.");
+    }
+    
+    if (sourceCurrencyCode.equals(targetCurrencyCode)) {
+      return 1.0;
+    }
+
+    Double directRate = lookupRate(sourceCurrencyCode, targetCurrencyCode);
+
+    if (directRate != null) {
+      return directRate;
+    }
+
+    Double symmetricRate = lookupRate(targetCurrencyCode, sourceCurrencyCode);
+
+    if (symmetricRate != null) {
+      return 1.0 / symmetricRate;
+    }
+
+    throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "No available conversion rate between " + sourceCurrencyCode + " to " + targetCurrencyCode);
+  }
+
+  /**
+   * Looks up the current known rate, if any, between the source and target currencies.
+   *
+   * @param sourceCurrencyCode The source currency being converted from.
+   * @param targetCurrencyCode The target currency being converted to.
+   * @return The exchange rate, or null if no rate has been registered.
+   */
+  private Double lookupRate(String sourceCurrencyCode, String targetCurrencyCode) {
+    Map<String, Double> rhs = rates.get(sourceCurrencyCode);
+
+    if (rhs != null) {
+      return rhs.get(targetCurrencyCode);
+    }
+
+    return null;
+  }
+
+  /**
+   * Registers the specified exchange rate.
+   *
+   * @param ratesMap           The map to add rate to
+   * @param sourceCurrencyCode The source currency.
+   * @param targetCurrencyCode The target currency.
+   * @param rate               The known exchange rate.
+   */
+  private void addRate(Map<String, Map<String, Double>> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) {
+    Map<String, Double> rhs = ratesMap.get(sourceCurrencyCode);
+
+    if (rhs == null) {
+      rhs = new HashMap<String, Double>();
+      ratesMap.put(sourceCurrencyCode, rhs);
+    }
+
+    rhs.put(targetCurrencyCode, rate);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+
+    FileExchangeRateProvider that = (FileExchangeRateProvider) o;
+
+    return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
+  }
+
+  @Override
+  public int hashCode() {
+    return rates != null ? rates.hashCode() : 0;
+  }
+
+  public String toString() {
+    return "["+this.getClass().getName()+" : " + rates.size() + " rates.]";
+  }
+
+  @Override
+  public String[] listAvailableCurrencies() {
+    List<String> pairs = new ArrayList<String>();
+    for(String from : rates.keySet()) {
+      for(String to : rates.get(from).keySet()) {
+        pairs.add(from+","+to);
+      }
+    }
+    return pairs.toArray(new String[1]);
+  }
+
+  @Override
+  public boolean reload() throws SolrException {
+    InputStream is = null;
+    Map<String, Map<String, Double>> tmpRates = new HashMap<String, Map<String, Double>>();
+    try {
+      log.info("Reloading exchange rates from file "+this.currencyConfigFile);
+
+      is = loader.openResource(currencyConfigFile);
+      javax.xml.parsers.DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+      try {
+        dbf.setXIncludeAware(true);
+        dbf.setNamespaceAware(true);
+      } catch (UnsupportedOperationException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "XML parser doesn't support XInclude option", e);
+      }
+      
+      try {
+        Document doc = dbf.newDocumentBuilder().parse(is);
+        XPathFactory xpathFactory = XPathFactory.newInstance();
+        XPath xpath = xpathFactory.newXPath();
+        
+        // Parse exchange rates.
+        NodeList nodes = (NodeList) xpath.evaluate("/currencyConfig/rates/rate", doc, XPathConstants.NODESET);
+        
+        for (int i = 0; i < nodes.getLength(); i++) {
+          Node rateNode = nodes.item(i);
+          NamedNodeMap attributes = rateNode.getAttributes();
+          Node from = attributes.getNamedItem("from");
+          Node to = attributes.getNamedItem("to");
+          Node rate = attributes.getNamedItem("rate");
+          
+          if (from == null || to == null || rate == null) {
+            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Exchange rate missing attributes (required: from, to, rate) " + rateNode);
+          }
+          
+          String fromCurrency = from.getNodeValue();
+          String toCurrency = to.getNodeValue();
+          Double exchangeRate;
+          
+          if (java.util.Currency.getInstance(fromCurrency) == null ||
+              java.util.Currency.getInstance(toCurrency) == null) {
+            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not find from currency specified in exchange rate: " + rateNode);
+          }
+          
+          try {
+            exchangeRate = Double.parseDouble(rate.getNodeValue());
+          } catch (NumberFormatException e) {
+            throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Could not parse exchange rate: " + rateNode, e);
+          }
+          
+          addRate(tmpRates, fromCurrency, toCurrency, exchangeRate);
+        }
+      } catch (SAXException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e);
+      } catch (IOException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e);
+      } catch (ParserConfigurationException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e);
+      } catch (XPathExpressionException e) {
+        throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing currency config.", e);
+      }
+    } catch (IOException e) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Error while opening Currency configuration file "+currencyConfigFile, e);
+    } finally {
+      try {
+        if (is != null) {
+          is.close();
+        }
+      } catch (IOException e) {
+        e.printStackTrace();
+      }
+    }
+    // Atomically swap in the new rates map, if it loaded successfully
+    this.rates = tmpRates;
+    return true;
+  }
+
+  @Override
+  public void init(Map<String,String> params) throws SolrException {
+    this.currencyConfigFile = params.get(PARAM_CURRENCY_CONFIG);
+    if(currencyConfigFile == null) {
+      throw new SolrException(ErrorCode.NOT_FOUND, "Missing required configuration "+PARAM_CURRENCY_CONFIG);
+    }
+    
+    // Removing config params custom to us
+    params.remove(PARAM_CURRENCY_CONFIG);
+  }
+
+  @Override
+  public void inform(ResourceLoader loader) throws SolrException {
+    if(loader == null) {
+      throw new SolrException(ErrorCode.BAD_REQUEST, "Needs ResourceLoader in order to load config file");
+    }
+    this.loader = loader;
+    reload();
+  }
+}
+
+/**
+ * Represents a Currency field value, which includes a long amount and ISO currency code.
+ */
+class CurrencyValue {
+  private long amount;
+  private String currencyCode;
+
+  /**
+   * Constructs a new currency value.
+   *
+   * @param amount       The amount.
+   * @param currencyCode The currency code.
+   */
+  public CurrencyValue(long amount, String currencyCode) {
+    this.amount = amount;
+    this.currencyCode = currencyCode;
+  }
+
+  /**
+   * Constructs a new currency value by parsing the specific input.
+   * <p/>
+   * Currency values are expected to be in the format &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) {
+    String amount = externalVal;
+    String code = defaultCurrency;
+
+    if (externalVal.contains(",")) {
+      String[] amountAndCode = externalVal.split(",");
+      amount = amountAndCode[0];
+      code = amountAndCode[1];
+    }
+
+    Currency currency = java.util.Currency.getInstance(code);
+
+    if (currency == null) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + code);
+    }
+
+    try {
+      double value = Double.parseDouble(amount);
+      long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
+
+      return new CurrencyValue(currencyValue, code);
+    } catch (NumberFormatException e) {
+      throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+    }
+  }
+
+  /**
+   * The amount of the CurrencyValue.
+   *
+   * @return The amount.
+   */
+  public long getAmount() {
+    return amount;
+  }
+
+  /**
+   * The ISO currency code of the CurrencyValue.
+   *
+   * @return The currency code.
+   */
+  public String getCurrencyCode() {
+    return currencyCode;
+  }
+
+  /**
+   * Performs a currency conversion & unit conversion.
+   *
+   * @param exchangeRates      Exchange rates to apply.
+   * @param sourceCurrencyCode The source currency code.
+   * @param sourceAmount       The source amount.
+   * @param targetCurrencyCode The target currency code.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+    double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+    return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
+  }
+
+  /**
+   * Performs a currency conversion & unit conversion.
+   *
+   * @param exchangeRate         Exchange rate to apply.
+   * @param sourceFractionDigits The fraction digits of the source.
+   * @param sourceAmount         The source amount.
+   * @param targetFractionDigits The fraction digits of the target.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
+    int digitDelta = targetFractionDigits - sourceFractionDigits;
+    double value = ((double) sourceAmount * exchangeRate);
+
+    if (digitDelta != 0) {
+      if (digitDelta < 0) {
+        for (int i = 0; i < -digitDelta; i++) {
+          value *= 0.1;
+        }
+      } else {
+        for (int i = 0; i < digitDelta; i++) {
+          value *= 10.0;
+        }
+      }
+    }
+
+    return (long) value;
+  }
+
+  /**
+   * Performs a currency conversion & unit conversion.
+   *
+   * @param exchangeRate       Exchange rate to apply.
+   * @param sourceCurrencyCode The source currency code.
+   * @param sourceAmount       The source amount.
+   * @param targetCurrencyCode The target currency code.
+   * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+   */
+  public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+    if (targetCurrencyCode.equals(sourceCurrencyCode)) {
+      return sourceAmount;
+    }
+
+    int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+    Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
+    int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
+    return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
+  }
+
+  /**
+   * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
+   *
+   * @param exchangeRates      The exchange rate provider.
+   * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
+   * @return The converted CurrencyValue.
+   */
+  public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
+    return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
+  }
+
+  public String toString() {
+    return String.valueOf(amount) + "," + currencyCode;
+  }
+}
\ No newline at end of file

Added: lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java (added)
+++ lucene/dev/trunk/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java Fri Mar  9 22:40:06 2012
@@ -0,0 +1,69 @@
+package org.apache.solr.schema;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.Map;
+
+import org.apache.solr.common.ResourceLoader;
+import org.apache.solr.common.SolrException;
+
+/**
+ * Interface for providing pluggable exchange rate providers to @CurrencyField
+ */
+public interface ExchangeRateProvider {
+  /**
+   * Get the exchange rate betwen the two given currencies
+   * @param sourceCurrencyCode
+   * @param targetCurrencyCode
+   * @return the exhange rate as a double
+   * @throws exception if the rate is not defined in the provider
+   */
+  public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) throws SolrException;
+  
+  /**
+   * List all configured currency code pairs
+   * @return a string array of <a href="http://en.wikipedia.org/wiki/ISO_4217">ISO 4217</a> currency codes on the format
+   * ["SRC,DST", "SRC,DST"...]
+   */
+  public String[] listAvailableCurrencies();
+
+  /**
+   * Ask the currency provider to explicitly reload/refresh its configuration.
+   * If this does not make sense for a particular provider, simply do nothing
+   * @throws SolrException if there is a problem reloading
+   * @return true if reload of rates succeeded, else false
+   */
+  public boolean reload() throws SolrException;
+
+  /**
+   * Initializes the provider by passing in a set of key/value configs as a map.
+   * Note that the map also contains other fieldType parameters, so make sure to
+   * avoid name clashes.
+   * <p>
+   * Important: Custom config params must be removed from the map before returning
+   * @param args a @Map of key/value config params to initialize the provider
+   */
+  public void init(Map<String,String> args);
+
+  /**
+   * Passes a ResourceLoader, used to read config files from e.g. ZooKeeper.
+   * Implementations not needing resource loader can implement this as NOOP.
+   * <p>Typically called after init
+   * @param loader a @ResourceLoader instance
+   */
+  public void inform(ResourceLoader loader) throws SolrException;
+}

Added: lucene/dev/trunk/solr/core/src/test-files/solr/conf/currency.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/conf/currency.xml?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/conf/currency.xml (added)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/conf/currency.xml Fri Mar  9 22:40:06 2012
@@ -0,0 +1,34 @@
+<?xml version="1.0" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Example exchange rates file.  -->
+
+<currencyConfig version="1.0">
+  <rates>
+    <!-- Example -->
+    <rate from="USD" to="JPY" rate="81.29"/>
+
+    <!-- Fake rates for testing -->
+    <rate from="USD" to="EUR" rate="2.5"/>
+    <rate from="USD" to="GBP" rate="0.5"/>
+    <rate from="EUR" to="GBP" rate="0.5"/>
+
+    <!-- Asymmetric rate -->
+    <rate from="EUR" to="USD" rate="0.5"/>
+  </rates>
+</currencyConfig>

Modified: lucene/dev/trunk/solr/core/src/test-files/solr/conf/schema.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test-files/solr/conf/schema.xml?rev=1299083&r1=1299082&r2=1299083&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test-files/solr/conf/schema.xml (original)
+++ lucene/dev/trunk/solr/core/src/test-files/solr/conf/schema.xml Fri Mar  9 22:40:06 2012
@@ -394,6 +394,10 @@
 
   <fieldType name="latLon" class="solr.LatLonType" subFieldType="double"/>
 
+  <!-- Currency type -->
+  <fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml"/>
+  <fieldType name="mock_currency" class="solr.CurrencyField" providerClass="org.apache.solr.schema.MockExchangeRateProvider" foo="bar" />
+
   <!--  some per-field similarity examples -->
   
   <!--  specify a Similarity classname directly -->
@@ -467,6 +471,9 @@
 
    <field name="point10" type="tenD" indexed="true" stored="true" multiValued="false"/>
 
+   <!-- Test currency -->
+   <field name="amount" type="currency" indexed="true" stored="true" multiValued="false"/>
+   <field name="mock_amount" type="mock_currency" indexed="true" stored="true"/>
 
    <!-- test different combinations of indexed and stored -->
    <field name="bind" type="boolean" indexed="true" stored="false"/>

Modified: lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java?rev=1299083&r1=1299082&r2=1299083&view=diff
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java (original)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java Fri Mar  9 22:40:06 2012
@@ -90,6 +90,7 @@ public abstract class AbstractZkTestCase
     putConfig(zkClient, "solrconfig.xml");
     putConfig(zkClient, "stopwords.txt");
     putConfig(zkClient, "protwords.txt");
+    putConfig(zkClient, "currency.xml");
     putConfig(zkClient, "mapping-ISOLatin1Accent.txt");
     putConfig(zkClient, "old_synonyms.txt");
     putConfig(zkClient, "synonyms.txt");

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java Fri Mar  9 22:40:06 2012
@@ -0,0 +1,211 @@
+package org.apache.solr.schema;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import org.apache.lucene.index.IndexableField;
+import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.core.SolrCore;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Tests currency field type.
+ */
+public class CurrencyFieldTest extends SolrTestCaseJ4 {
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig.xml", "schema.xml");
+  }
+
+  @Test
+  public void testCurrencySchema() throws Exception {
+    IndexSchema schema = h.getCore().getSchema();
+
+    SchemaField amount = schema.getField("amount");
+    assertNotNull(amount);
+    assertTrue(amount.isPolyField());
+
+    SchemaField[] dynFields = schema.getDynamicFieldPrototypes();
+    boolean seenCurrency = false;
+    boolean seenAmount = false;
+
+    for (SchemaField dynField : dynFields) {
+      if (dynField.getName().equals("*" + FieldType.POLY_FIELD_SEPARATOR + CurrencyField.FIELD_SUFFIX_CURRENCY)) {
+        seenCurrency = true;
+      }
+
+      if (dynField.getName().equals("*" + FieldType.POLY_FIELD_SEPARATOR + CurrencyField.FIELD_SUFFIX_AMOUNT_RAW)) {
+        seenAmount = true;
+      }
+    }
+
+    assertTrue("Didn't find the expected currency code dynamic field", seenCurrency);
+    assertTrue("Didn't find the expected value dynamic field", seenAmount);
+  }
+
+  @Test
+  public void testCurrencyFieldType() throws Exception {
+    SolrCore core = h.getCore();
+    IndexSchema schema = core.getSchema();
+    SchemaField amount = schema.getField("amount");
+    assertNotNull(amount);
+    assertTrue("amount is not a poly field", amount.isPolyField());
+    FieldType tmp = amount.getType();
+    assertTrue(tmp instanceof CurrencyField);
+    String currencyValue = "1.50,EUR";
+    IndexableField[] fields = amount.createFields(currencyValue, 2);
+    assertEquals(fields.length, 3);
+
+    // First field is currency code, second is value, third is stored.
+    for (int i = 0; i < 3; i++) {
+      boolean hasValue = fields[i].readerValue() != null
+              || fields[i].numericValue() != null
+              || fields[i].stringValue() != null;
+      assertTrue("Doesn't have a value: " + fields[i], hasValue);
+    }
+
+    assertEquals(schema.getFieldTypeByName("string").toExternal(fields[2]), "1.50,EUR");
+    
+    // A few tests on the provider directly
+    ExchangeRateProvider p = ((CurrencyField) tmp).getProvider();
+    String[] available = p.listAvailableCurrencies();
+    assert(available.length == 5);
+    assert(p.reload() == true);
+    assert(p.getExchangeRate("USD", "EUR") == 2.5);
+  }
+
+  @Test
+  public void testCurrencyRangeSearch() throws Exception {
+    for (int i = 1; i <= 10; i++) {
+      assertU(adoc("id", "" + i, "amount", i + ",USD"));
+    }
+
+    assertU(commit());
+
+    assertQ(req("fl", "*,score", "q",
+            "amount:[2.00,USD TO 5.00,USD]"),
+            "//*[@numFound='4']");
+
+    assertQ(req("fl", "*,score", "q",
+            "amount:[0.50,USD TO 1.00,USD]"),
+            "//*[@numFound='1']");
+
+    assertQ(req("fl", "*,score", "q",
+            "amount:[24.00,USD TO 25.00,USD]"),
+            "//*[@numFound='0']");
+
+    // "GBP" currency code is 1/2 of a USD dollar, for testing.
+    assertQ(req("fl", "*,score", "q",
+            "amount:[0.50,GBP TO 1.00,GBP]"),
+            "//*[@numFound='2']");
+
+    // "EUR" currency code is 2.5X of a USD dollar, for testing.
+    assertQ(req("fl", "*,score", "q",
+            "amount:[24.00,EUR TO 25.00,EUR]"),
+            "//*[@numFound='1']");
+
+    // Slight asymmetric rate should work.
+    assertQ(req("fl", "*,score", "q",
+            "amount:[24.99,EUR TO 25.01,EUR]"),
+            "//*[@numFound='1']");
+  }
+
+  @Test
+  public void testCurrencyPointQuery() throws Exception {
+    assertU(adoc("id", "" + 1, "amount", "10.00,USD"));
+    assertU(adoc("id", "" + 2, "amount", "15.00,EUR"));
+    assertU(commit());
+    assertQ(req("fl", "*,score", "q", "amount:10.00,USD"), "//int[@name='id']='1'");
+    assertQ(req("fl", "*,score", "q", "amount:9.99,USD"), "//*[@numFound='0']");
+    assertQ(req("fl", "*,score", "q", "amount:10.01,USD"), "//*[@numFound='0']");
+    assertQ(req("fl", "*,score", "q", "amount:15.00,EUR"), "//int[@name='id']='2'");
+    assertQ(req("fl", "*,score", "q", "amount:7.50,USD"), "//int[@name='id']='2'");
+    assertQ(req("fl", "*,score", "q", "amount:7.49,USD"), "//*[@numFound='0']");
+    assertQ(req("fl", "*,score", "q", "amount:7.51,USD"), "//*[@numFound='0']");
+  }
+
+  @Ignore
+  public void testPerformance() throws Exception {
+    Random r = new Random();
+    int initDocs = 200000;
+
+    for (int i = 1; i <= initDocs; i++) {
+      assertU(adoc("id", "" + i, "amount", (r.nextInt(10) + 1.00) + ",USD"));
+      if (i % 1000 == 0)
+        System.out.println(i);
+    }
+
+    assertU(commit());
+    for (int i = 0; i < 1000; i++) {
+      double lower = r.nextInt(10) + 1.00;
+      assertQ(req("fl", "*,score", "q", "amount:[" +  lower + ",USD TO " + (lower + 10.00) + ",USD]"), "//*");
+      assertQ(req("fl", "*,score", "q", "amount:[" +  lower + ",EUR TO " + (lower + 10.00) + ",EUR]"), "//*");
+    }
+
+    for (int j = 0; j < 3; j++) {
+      long t1 = System.currentTimeMillis();
+      for (int i = 0; i < 1000; i++) {
+        double lower = r.nextInt(10) + 1.00;
+        assertQ(req("fl", "*,score", "q", "amount:[" +  lower + ",USD TO " + (lower + (9.99 - (j * 0.01))) + ",USD]"), "//*");
+      }
+
+      System.out.println(System.currentTimeMillis() - t1);
+    }
+
+    System.out.println("---");
+
+    for (int j = 0; j < 3; j++) {
+      long t1 = System.currentTimeMillis();
+      for (int i = 0; i < 1000; i++) {
+        double lower = r.nextInt(10) + 1.00;
+        assertQ(req("fl", "*,score", "q", "amount:[" +  lower + ",EUR TO " + (lower + (9.99 - (j * 0.01))) + ",EUR]"), "//*");
+      }
+
+      System.out.println(System.currentTimeMillis() - t1);
+    }
+  }
+
+  @Test
+  public void testCurrencySort() throws Exception {
+    assertU(adoc("id", "" + 1, "amount", "10.00,USD"));
+    assertU(adoc("id", "" + 2, "amount", "15.00,EUR"));
+    assertU(adoc("id", "" + 3, "amount", "7.00,EUR"));
+    assertU(adoc("id", "" + 4, "amount", "6.00,GBP"));
+    assertU(adoc("id", "" + 5, "amount", "2.00,GBP"));
+    assertU(commit());
+
+    assertQ(req("fl", "*,score", "q", "*:*", "sort", "amount desc", "limit", "1"), "//int[@name='id']='4'");
+    assertQ(req("fl", "*,score", "q", "*:*", "sort", "amount asc", "limit", "1"), "//int[@name='id']='3'");
+  }
+
+  @Test
+  public void testMockExchangeRateProvider() throws Exception {
+    assertU(adoc("id", "1", "mock_amount", "1.00,USD"));
+    assertU(adoc("id", "2", "mock_amount", "1.00,EUR"));
+    assertU(adoc("id", "3", "mock_amount", "1.00,NOK"));
+    assertU(commit());
+
+    assertQ(req("fl", "*,score", "q", "mock_amount:5.0,NOK"),   "//*[@numFound='1']", "//int[@name='id']='1'");
+    assertQ(req("fl", "*,score", "q", "mock_amount:1.2,USD"), "//*[@numFound='1']",   "//int[@name='id']='2'");
+    assertQ(req("fl", "*,score", "q", "mock_amount:0.2,USD"), "//*[@numFound='1']",   "//int[@name='id']='3'");
+    assertQ(req("fl", "*,score", "q", "mock_amount:99,USD"),  "//*[@numFound='0']");
+  }
+}

Added: lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java (added)
+++ lucene/dev/trunk/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java Fri Mar  9 22:40:06 2012
@@ -0,0 +1,81 @@
+package org.apache.solr.schema;
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.solr.common.ResourceLoader;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.SolrException.ErrorCode;
+
+/**
+ * Simple mock provider with fixed rates and some assertions
+ */
+public class MockExchangeRateProvider implements ExchangeRateProvider {
+  private static Map<String,Double> map = new HashMap<String,Double>();
+  static {
+    map.put("USD,EUR", 0.8);
+    map.put("EUR,USD", 1.2);
+    map.put("USD,NOK", 5.0);
+    map.put("NOK,USD", 0.2);
+    map.put("EUR,NOK", 10.0);
+    map.put("NOK,EUR", 0.1);
+  }
+
+  private boolean gotArgs = false;
+  private boolean gotLoader = false;
+  
+  @Override
+  public double getExchangeRate(String sourceCurrencyCode, String targetCurrencyCode) {
+//    System.out.println("***** getExchangeRate("+sourceCurrencyCode+targetCurrencyCode+")");
+    if(sourceCurrencyCode.equals(targetCurrencyCode)) return 1.0;
+
+    Double result = map.get(sourceCurrencyCode+","+targetCurrencyCode);
+    if(result == null) {
+      throw new SolrException(ErrorCode.NOT_FOUND, "No exchange rate found for the pair "+sourceCurrencyCode+","+targetCurrencyCode);
+    }
+    return result;
+  }
+
+  @Override
+  public String[] listAvailableCurrencies() {
+    return map.keySet().toArray(new String[1]);
+  }
+
+  @Override
+  public boolean reload() throws SolrException {
+    assert(gotArgs == true);
+    assert(gotLoader == true);
+    return true;
+  }
+
+  @Override
+  public void init(Map<String,String> args) {
+    assert(args.get("foo").equals("bar"));
+    gotArgs = true;
+    args.remove("foo");
+  }
+
+  @Override
+  public void inform(ResourceLoader loader) throws SolrException {
+    assert(loader != null);
+    gotLoader = true;
+    assert(gotArgs == true);
+  }
+  
+}

Added: lucene/dev/trunk/solr/example/exampledocs/money.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/exampledocs/money.xml?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/example/exampledocs/money.xml (added)
+++ lucene/dev/trunk/solr/example/exampledocs/money.xml Fri Mar  9 22:40:06 2012
@@ -0,0 +1,65 @@
+<!--
+ 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.
+-->
+
+<!-- Example documents utilizing the CurrencyField type -->
+<add>
+<doc>
+  <field name="id">USD</field>
+  <field name="name">One Dollar</field>
+  <field name="manu">Bank of America</field>
+  <field name="manu_id_s">boa</field>
+  <field name="cat">currency</field>
+  <field name="features">Coins and notes</field>
+  <field name="price_c">1,USD</field>
+  <field name="inStock">true</field>
+</doc>
+
+<doc>
+  <field name="id">EUR</field>
+  <field name="name">One Euro</field>
+  <field name="manu">European Union</field>
+  <field name="manu_id_s">eu</field>
+  <field name="cat">currency</field>
+  <field name="features">Coins and notes</field>
+  <field name="price_c">1,EUR</field>
+  <field name="inStock">true</field>
+</doc>
+
+<doc>
+  <field name="id">GBP</field>
+  <field name="name">One British Pound</field>
+  <field name="manu">U.K.</field>
+  <field name="manu_id_s">uk</field>
+  <field name="cat">currency</field>
+  <field name="features">Coins and notes</field>
+  <field name="price_c">1,GBP</field>
+  <field name="inStock">true</field>
+</doc>
+
+<doc>
+  <field name="id">NOK</field>
+  <field name="name">One Krone</field>
+  <field name="manu">Bank of Norway</field>
+  <field name="manu_id_s">nor</field>
+  <field name="cat">currency</field>
+  <field name="features">Coins and notes</field>
+  <field name="price_c">1,NOK</field>
+  <field name="inStock">true</field>
+</doc>
+
+</add>
+

Added: lucene/dev/trunk/solr/example/solr/conf/currency.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/conf/currency.xml?rev=1299083&view=auto
==============================================================================
--- lucene/dev/trunk/solr/example/solr/conf/currency.xml (added)
+++ lucene/dev/trunk/solr/example/solr/conf/currency.xml Fri Mar  9 22:40:06 2012
@@ -0,0 +1,67 @@
+<?xml version="1.0" ?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License.  You may obtain a copy of the License at
+
+     http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Example exchange rates file for CurrencyField type named "currency" in example schema -->
+
+<currencyConfig version="1.0">
+  <rates>
+    <!-- Updated from http://www.exchangerate.com/ at 2011-09-27 -->
+    <rate from="USD" to="ARS" rate="4.333871" comment="ARGENTINA Peso" />
+    <rate from="USD" to="AUD" rate="1.025768" comment="AUSTRALIA Dollar" />
+    <rate from="USD" to="EUR" rate="0.743676" comment="European Euro" />
+    <rate from="USD" to="BRL" rate="1.881093" comment="BRAZIL Real" />
+    <rate from="USD" to="CAD" rate="1.030815" comment="CANADA Dollar" />
+    <rate from="USD" to="CLP" rate="519.0996" comment="CHILE Peso" />
+    <rate from="USD" to="CNY" rate="6.387310" comment="CHINA Yuan" />
+    <rate from="USD" to="CZK" rate="18.47134" comment="CZECH REP. Koruna" />
+    <rate from="USD" to="DKK" rate="5.515436" comment="DENMARK Krone" />
+    <rate from="USD" to="HKD" rate="7.801922" comment="HONG KONG Dollar" />
+    <rate from="USD" to="HUF" rate="215.6169" comment="HUNGARY Forint" />
+    <rate from="USD" to="ISK" rate="118.1280" comment="ICELAND Krona" />
+    <rate from="USD" to="INR" rate="49.49088" comment="INDIA Rupee" />
+    <rate from="USD" to="XDR" rate="0.641358" comment="INTNL MON. FUND SDR" />
+    <rate from="USD" to="ILS" rate="3.709739" comment="ISRAEL Sheqel" />
+    <rate from="USD" to="JPY" rate="76.32419" comment="JAPAN Yen" />
+    <rate from="USD" to="KRW" rate="1169.173" comment="KOREA (SOUTH) Won" />
+    <rate from="USD" to="KWD" rate="0.275142" comment="KUWAIT Dinar" />
+    <rate from="USD" to="MXN" rate="13.85895" comment="MEXICO Peso" />
+    <rate from="USD" to="NZD" rate="1.285159" comment="NEW ZEALAND Dollar" />
+    <rate from="USD" to="NOK" rate="5.859035" comment="NORWAY Krone" />
+    <rate from="USD" to="PKR" rate="87.57007" comment="PAKISTAN Rupee" />
+    <rate from="USD" to="PEN" rate="2.730683" comment="PERU Sol" />
+    <rate from="USD" to="PHP" rate="43.62039" comment="PHILIPPINES Peso" />
+    <rate from="USD" to="PLN" rate="3.310139" comment="POLAND Zloty" />
+    <rate from="USD" to="RON" rate="3.100932" comment="ROMANIA Leu" />
+    <rate from="USD" to="RUB" rate="32.14663" comment="RUSSIA Ruble" />
+    <rate from="USD" to="SAR" rate="3.750465" comment="SAUDI ARABIA Riyal" />
+    <rate from="USD" to="SGD" rate="1.299352" comment="SINGAPORE Dollar" />
+    <rate from="USD" to="ZAR" rate="8.329761" comment="SOUTH AFRICA Rand" />
+    <rate from="USD" to="SEK" rate="6.883442" comment="SWEDEN Krona" />
+    <rate from="USD" to="CHF" rate="0.906035" comment="SWITZERLAND Franc" />
+    <rate from="USD" to="TWD" rate="30.40283" comment="TAIWAN Dollar" />
+    <rate from="USD" to="THB" rate="30.89487" comment="THAILAND Baht" />
+    <rate from="USD" to="AED" rate="3.672955" comment="U.A.E. Dirham" />
+    <rate from="USD" to="UAH" rate="7.988582" comment="UKRAINE Hryvnia" />
+    <rate from="USD" to="GBP" rate="0.647910" comment="UNITED KINGDOM Pound" />
+    
+    <!-- Cross-rates for some common currencies -->
+    <rate from="EUR" to="GBP" rate="0.869914" />  
+    <rate from="EUR" to="NOK" rate="7.800095" />  
+    <rate from="GBP" to="NOK" rate="8.966508" />  
+  </rates>
+</currencyConfig>

Modified: lucene/dev/trunk/solr/example/solr/conf/schema.xml
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/conf/schema.xml?rev=1299083&r1=1299082&r2=1299083&view=diff
==============================================================================
--- lucene/dev/trunk/solr/example/solr/conf/schema.xml (original)
+++ lucene/dev/trunk/solr/example/solr/conf/schema.xml Fri Mar  9 22:40:06 2012
@@ -455,6 +455,15 @@
    -->
     <fieldtype name="geohash" class="solr.GeoHashField"/>
 
+   <!-- Money/currency field type. See http://wiki.apache.org/solr/MoneyFieldType
+        Parameters:
+          defaultCurrency: Specifies the default currency if none specified. Defaults to "USD"
+          providerClass:   Lets you plug in other exchange backend. Defaults to FileExchangeRateProvider
+                           The FileExchangeRateProvider takes one parameter:
+                             currencyConfig: name of an xml file holding exhange rates
+   -->
+    <fieldType name="currency" class="solr.CurrencyField" currencyConfig="currency.xml" defaultCurrency="USD"/>
+
    <!-- some examples for different languages (generally ordered by ISO code) -->
 
     <!-- Arabic -->
@@ -920,7 +929,7 @@
    <dynamicField name="*_d"  type="double" indexed="true"  stored="true"/>
 
    <!-- Type used to index the lat and lon components for the "location" FieldType -->
-   <dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false"/>
+   <dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false" />
 
    <dynamicField name="*_dt" type="date"    indexed="true"  stored="true"/>
    <dynamicField name="*_p"  type="location" indexed="true" stored="true"/>
@@ -933,6 +942,7 @@
    <dynamicField name="*_tdt" type="tdate"  indexed="true"  stored="true"/>
 
    <dynamicField name="*_pi"  type="pint"    indexed="true"  stored="true"/>
+   <dynamicField name="*_c"   type="currency" indexed="true"  stored="true"/>
 
    <dynamicField name="ignored_*" type="ignored" multiValued="true"/>
    <dynamicField name="attr_*" type="text_general" indexed="true" stored="true" multiValued="true"/>
@@ -968,6 +978,9 @@
    <copyField source="features" dest="text"/>
    <copyField source="includes" dest="text"/>
    <copyField source="manu" dest="manu_exact"/>
+
+   <!-- Copy the price into a currency enabled field (default USD) -->
+   <copyField source="price" dest="price_c"/>
 	
    <!-- Above, multiple source fields are copied to the [text] field. 
 	  Another way to map multiple source fields to the same 

Modified: lucene/dev/trunk/solr/example/solr/conf/velocity/doc.vm
URL: http://svn.apache.org/viewvc/lucene/dev/trunk/solr/example/solr/conf/velocity/doc.vm?rev=1299083&r1=1299082&r2=1299083&view=diff
==============================================================================
--- lucene/dev/trunk/solr/example/solr/conf/velocity/doc.vm (original)
+++ lucene/dev/trunk/solr/example/solr/conf/velocity/doc.vm Fri Mar  9 22:40:06 2012
@@ -2,7 +2,7 @@
 ##do we have a physical store for this product
 #set($store = $doc.getFieldValue('store'))
 #if($store)<div class="map"><img src="http://maps.google.com/maps/api/staticmap?&zoom=12&size=150x80&maptype=roadmap&markers=$doc.getFieldValue('store')&sensor=false" /><div><small><a target="_map" href="http://maps.google.com/?q=$store&amp;source=embed">Larger Map</a></small></div></div>#end
-<div>Price: $!number.currency($doc.getFieldValue('price'))</div>
+<div>Price: #field('price_c')</div>
 <div>Features: #field('features')</div>
 <div>In Stock: #field('inStock')</div>
 <div class="mlt">