SOLR-2202: Money/Currency FieldType

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1299083 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Jan Høydahl 2012-03-09 22:40:06 +00:00
parent a461a63992
commit bf2be9b35a
12 changed files with 1304 additions and 2 deletions

View File

@ -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
----------------------

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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"/>

View File

@ -90,6 +90,7 @@ public abstract class AbstractZkTestCase extends SolrTestCaseJ4 {
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");

View File

@ -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']");
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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"/>
@ -969,6 +979,9 @@
<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
destination field is to use the dynamic field syntax.

View File

@ -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">