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 <amount>,<currency code>,
+ * for example, "500,USD" would represent 5 U.S. Dollars.
+ * <p/>
+ * If no currency code is specified, the default is assumed.
+ *
+ * @param externalVal The value to parse.
+ * @param defaultCurrency The default currency.
+ * @return The parsed CurrencyValue.
+ */
+ public static CurrencyValue parse(String externalVal, String defaultCurrency) {
+ 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&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">