diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 39a0f9249ef..7f7d3be8c10 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -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
----------------------
diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyField.java b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
new file mode 100644
index 00000000000..5bdbe6ad35b
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java
@@ -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.
+ *
+ * See http://wiki.apache.org/solr/CurrencyField
+ */
+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 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 props = new HashMap();
+ 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> rates = new HashMap>();
+
+ private String currencyConfigFile;
+ private ResourceLoader loader;
+
+ /**
+ * Returns the currently known exchange rate between two currencies. If a direct rate has been loaded,
+ * it is used. Otherwise, if a rate is known to convert the target currency to the source, the inverse
+ * exchange rate is computed.
+ *
+ * @param sourceCurrencyCode The source currency being converted from.
+ * @param targetCurrencyCode The target currency being converted to.
+ * @return The exchange rate.
+ * @throws 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 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> ratesMap, String sourceCurrencyCode, String targetCurrencyCode, double rate) {
+ Map rhs = ratesMap.get(sourceCurrencyCode);
+
+ if (rhs == null) {
+ rhs = new HashMap();
+ ratesMap.put(sourceCurrencyCode, rhs);
+ }
+
+ rhs.put(targetCurrencyCode, rate);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ FileExchangeRateProvider that = (FileExchangeRateProvider) o;
+
+ return !(rates != null ? !rates.equals(that.rates) : that.rates != null);
+ }
+
+ @Override
+ public int hashCode() {
+ return rates != null ? rates.hashCode() : 0;
+ }
+
+ public String toString() {
+ return "["+this.getClass().getName()+" : " + rates.size() + " rates.]";
+ }
+
+ @Override
+ public String[] listAvailableCurrencies() {
+ List pairs = new ArrayList();
+ 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> tmpRates = new HashMap>();
+ 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 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.
+ *
+ * Currency values are expected to be in the format <amount>,<currency code>,
+ * for example, "500,USD" would represent 5 U.S. Dollars.
+ *
+ * If no currency code is specified, the default is assumed.
+ *
+ * @param externalVal The value to parse.
+ * @param defaultCurrency The default currency.
+ * @return The parsed CurrencyValue.
+ */
+ public static CurrencyValue parse(String externalVal, String defaultCurrency) {
+ String amount = externalVal;
+ String code = defaultCurrency;
+
+ if (externalVal.contains(",")) {
+ String[] amountAndCode = externalVal.split(",");
+ amount = amountAndCode[0];
+ code = amountAndCode[1];
+ }
+
+ Currency currency = java.util.Currency.getInstance(code);
+
+ if (currency == null) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Invalid currency code " + code);
+ }
+
+ try {
+ double value = Double.parseDouble(amount);
+ long currencyValue = Math.round(value * Math.pow(10.0, currency.getDefaultFractionDigits()));
+
+ return new CurrencyValue(currencyValue, code);
+ } catch (NumberFormatException e) {
+ throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, e);
+ }
+ }
+
+ /**
+ * The amount of the CurrencyValue.
+ *
+ * @return The amount.
+ */
+ public long getAmount() {
+ return amount;
+ }
+
+ /**
+ * The ISO currency code of the CurrencyValue.
+ *
+ * @return The currency code.
+ */
+ public String getCurrencyCode() {
+ return currencyCode;
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRates Exchange rates to apply.
+ * @param sourceCurrencyCode The source currency code.
+ * @param sourceAmount The source amount.
+ * @param targetCurrencyCode The target currency code.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(ExchangeRateProvider exchangeRates, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+ double exchangeRate = exchangeRates.getExchangeRate(sourceCurrencyCode, targetCurrencyCode);
+ return convertAmount(exchangeRate, sourceCurrencyCode, sourceAmount, targetCurrencyCode);
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRate Exchange rate to apply.
+ * @param sourceFractionDigits The fraction digits of the source.
+ * @param sourceAmount The source amount.
+ * @param targetFractionDigits The fraction digits of the target.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(final double exchangeRate, final int sourceFractionDigits, final long sourceAmount, final int targetFractionDigits) {
+ int digitDelta = targetFractionDigits - sourceFractionDigits;
+ double value = ((double) sourceAmount * exchangeRate);
+
+ if (digitDelta != 0) {
+ if (digitDelta < 0) {
+ for (int i = 0; i < -digitDelta; i++) {
+ value *= 0.1;
+ }
+ } else {
+ for (int i = 0; i < digitDelta; i++) {
+ value *= 10.0;
+ }
+ }
+ }
+
+ return (long) value;
+ }
+
+ /**
+ * Performs a currency conversion & unit conversion.
+ *
+ * @param exchangeRate Exchange rate to apply.
+ * @param sourceCurrencyCode The source currency code.
+ * @param sourceAmount The source amount.
+ * @param targetCurrencyCode The target currency code.
+ * @return The converted indexable units after the exchange rate and currency fraction digits are applied.
+ */
+ public static long convertAmount(double exchangeRate, String sourceCurrencyCode, long sourceAmount, String targetCurrencyCode) {
+ if (targetCurrencyCode.equals(sourceCurrencyCode)) {
+ return sourceAmount;
+ }
+
+ int sourceFractionDigits = Currency.getInstance(sourceCurrencyCode).getDefaultFractionDigits();
+ Currency targetCurrency = Currency.getInstance(targetCurrencyCode);
+ int targetFractionDigits = targetCurrency.getDefaultFractionDigits();
+ return convertAmount(exchangeRate, sourceFractionDigits, sourceAmount, targetFractionDigits);
+ }
+
+ /**
+ * Returns a new CurrencyValue that is the conversion of this CurrencyValue to the specified currency.
+ *
+ * @param exchangeRates The exchange rate provider.
+ * @param targetCurrencyCode The target currency code to convert this CurrencyValue to.
+ * @return The converted CurrencyValue.
+ */
+ public CurrencyValue convertTo(ExchangeRateProvider exchangeRates, String targetCurrencyCode) {
+ return new CurrencyValue(convertAmount(exchangeRates, this.getCurrencyCode(), this.getAmount(), targetCurrencyCode), targetCurrencyCode);
+ }
+
+ public String toString() {
+ return String.valueOf(amount) + "," + currencyCode;
+ }
+}
\ No newline at end of file
diff --git a/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java b/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java
new file mode 100644
index 00000000000..eb2fc6c3009
--- /dev/null
+++ b/solr/core/src/java/org/apache/solr/schema/ExchangeRateProvider.java
@@ -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 ISO 4217 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.
+ *
+ * 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 args);
+
+ /**
+ * Passes a ResourceLoader, used to read config files from e.g. ZooKeeper.
+ * Implementations not needing resource loader can implement this as NOOP.
+ * Typically called after init
+ * @param loader a @ResourceLoader instance
+ */
+ public void inform(ResourceLoader loader) throws SolrException;
+}
diff --git a/solr/core/src/test-files/solr/conf/currency.xml b/solr/core/src/test-files/solr/conf/currency.xml
new file mode 100644
index 00000000000..f74f6e96399
--- /dev/null
+++ b/solr/core/src/test-files/solr/conf/currency.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr/core/src/test-files/solr/conf/schema.xml b/solr/core/src/test-files/solr/conf/schema.xml
index 7cb97e59d9e..c6b286768c5 100644
--- a/solr/core/src/test-files/solr/conf/schema.xml
+++ b/solr/core/src/test-files/solr/conf/schema.xml
@@ -394,6 +394,10 @@
+
+
+
+
@@ -467,6 +471,9 @@
+
+
+
diff --git a/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java b/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java
index 3db5d1cfe90..9c99f96f35d 100644
--- a/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java
+++ b/solr/core/src/test/org/apache/solr/cloud/AbstractZkTestCase.java
@@ -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");
diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java
new file mode 100644
index 00000000000..bfd190e59dc
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTest.java
@@ -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']");
+ }
+}
diff --git a/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java b/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java
new file mode 100644
index 00000000000..8fa50815c94
--- /dev/null
+++ b/solr/core/src/test/org/apache/solr/schema/MockExchangeRateProvider.java
@@ -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 map = new HashMap();
+ 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 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);
+ }
+
+}
diff --git a/solr/example/exampledocs/money.xml b/solr/example/exampledocs/money.xml
new file mode 100644
index 00000000000..b1b8036c369
--- /dev/null
+++ b/solr/example/exampledocs/money.xml
@@ -0,0 +1,65 @@
+
+
+
+
+
+ USD
+ One Dollar
+ Bank of America
+ boa
+ currency
+ Coins and notes
+ 1,USD
+ true
+
+
+
+ EUR
+ One Euro
+ European Union
+ eu
+ currency
+ Coins and notes
+ 1,EUR
+ true
+
+
+
+ GBP
+ One British Pound
+ U.K.
+ uk
+ currency
+ Coins and notes
+ 1,GBP
+ true
+
+
+
+ NOK
+ One Krone
+ Bank of Norway
+ nor
+ currency
+ Coins and notes
+ 1,NOK
+ true
+
+
+
+
diff --git a/solr/example/solr/conf/currency.xml b/solr/example/solr/conf/currency.xml
new file mode 100644
index 00000000000..3a9c58afee8
--- /dev/null
+++ b/solr/example/solr/conf/currency.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/solr/example/solr/conf/schema.xml b/solr/example/solr/conf/schema.xml
index 26019eab33f..ff7daebc299 100755
--- a/solr/example/solr/conf/schema.xml
+++ b/solr/example/solr/conf/schema.xml
@@ -455,6 +455,15 @@
-->
+
+
+
@@ -920,7 +929,7 @@
-
+
@@ -933,6 +942,7 @@
+
@@ -968,6 +978,9 @@
+
+
+