mirror of https://github.com/apache/lucene.git
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:
parent
a461a63992
commit
bf2be9b35a
|
@ -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
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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']");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
||||
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
Loading…
Reference in New Issue