diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 6477ada483e..85535c3a668 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -343,6 +343,8 @@ Upgrade Notes * SOLR-10379: ManagedSynonymFilterFactory has been deprecated in favor of ManagedSynonymGraphFilterFactory. +* SOLR-10503: CurrencyField has been deprecated in favor of new CurrencyFieldType. + New Features ---------------------- @@ -490,6 +492,10 @@ Other Changes rendered visibly in the PDF. Also add .adoc file checks to the top-level validate target, including for the invisible substitutions PDF problem. (Steve Rowe) +* SOLR-10503,SOLR-10502: Deprecate CurrencyField in favor of new CurrencyFieldType, which works + with point fields and provides control over dynamic fields used for the raw amount and currency + code sub-fields. (hossman, Steve Rowe) + ================== 6.6.1 ================== Bug Fixes diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyField.java b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java index 286d2c12c8b..e2676fe45d2 100644 --- a/solr/core/src/java/org/apache/solr/schema/CurrencyField.java +++ b/solr/core/src/java/org/apache/solr/schema/CurrencyField.java @@ -14,189 +14,74 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.solr.schema; -import java.io.IOException; -import java.io.InputStream; -import java.lang.invoke.MethodHandles; + import java.util.ArrayList; -import java.util.Currency; +import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; -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 org.apache.lucene.analysis.util.ResourceLoader; import org.apache.lucene.analysis.util.ResourceLoaderAware; -import org.apache.lucene.document.StoredField; -import org.apache.lucene.index.LeafReaderContext; -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.BooleanClause.Occur; -import org.apache.lucene.search.BooleanQuery; -import org.apache.lucene.search.FieldValueQuery; -import org.apache.lucene.search.Query; -import org.apache.lucene.search.SortField; -import org.apache.solr.uninverting.UninvertingReader.Type; import org.apache.solr.common.SolrException; import org.apache.solr.common.SolrException.ErrorCode; -import org.apache.solr.response.TextResponseWriter; -import org.apache.solr.search.Filter; -import org.apache.solr.search.QParser; -import org.apache.solr.search.QueryWrapperFilter; -import org.apache.solr.search.SolrConstantScoreQuery; -import org.apache.solr.search.function.ValueSourceRangeFilter; -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; + /** * Field type for support of monetary values. *

* See http://wiki.apache.org/solr/CurrencyField + * @deprecated Use {@link CurrencyFieldType} */ -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 Object PARAM_PRECISION_STEP = "precisionStep"; - protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider"; - protected static final String DEFAULT_DEFAULT_CURRENCY = "USD"; - protected static final String DEFAULT_PRECISION_STEP = "0"; - protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw"; - protected static final String FIELD_SUFFIX_CURRENCY = "_currency"; - - private IndexSchema schema; - protected FieldType fieldTypeCurrency; - protected FieldType fieldTypeAmountRaw; - private String exchangeRateProviderClass; - private String defaultCurrency; - private ExchangeRateProvider provider; - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - - /** - * A wrapper arround Currency.getInstance that returns null - * instead of throwing IllegalArgumentException - * if the specified Currency does not exist in this JVM. - * - * @see Currency#getInstance(String) - */ - public static Currency getCurrency(final String code) { - try { - return Currency.getInstance(code); - } catch (IllegalArgumentException e) { - /* :NOOP: */ - } - return null; - } +@Deprecated +public class CurrencyField extends CurrencyFieldType implements SchemaAware, ResourceLoaderAware { + protected static final String FIELD_SUFFIX_AMOUNT_RAW = "_amount_raw"; + protected static final String FIELD_SUFFIX_CURRENCY = "_currency"; + protected static final String FIELD_TYPE_AMOUNT_RAW = "amount_raw_type_long"; + protected static final String FIELD_TYPE_CURRENCY = "currency_type_string"; + protected static final String PARAM_PRECISION_STEP = "precisionStep"; + protected static final String DEFAULT_PRECISION_STEP = "0"; @Override protected void init(IndexSchema schema, Map args) { - super.init(schema, args); - if (this.isMultiValued()) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, - "CurrencyField types can not be multiValued: " + - this.typeName); + + // Fail if amountLongSuffix or codeStrSuffix are specified + List unknownParams = new ArrayList<>(); + fieldSuffixAmountRaw = args.get(PARAM_FIELD_SUFFIX_AMOUNT_RAW); + if (fieldSuffixAmountRaw != null) { + unknownParams.add(PARAM_FIELD_SUFFIX_AMOUNT_RAW); } - 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; + fieldSuffixCurrency = args.get(PARAM_FIELD_SUFFIX_CURRENCY); + if (fieldSuffixCurrency != null) { + unknownParams.add(PARAM_FIELD_SUFFIX_CURRENCY); + } + if ( ! unknownParams.isEmpty()) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Unknown parameter(s): " + unknownParams); } - if (this.exchangeRateProviderClass == null) { - this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS; - } - - if (null == getCurrency(this.defaultCurrency)) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Default currency code is not supported by this JVM: " + this.defaultCurrency); - } - String precisionStepString = args.get(PARAM_PRECISION_STEP); if (precisionStepString == null) { precisionStepString = DEFAULT_PRECISION_STEP; + } else { + args.remove(PARAM_PRECISION_STEP); } // Initialize field type for amount fieldTypeAmountRaw = new TrieLongField(); - fieldTypeAmountRaw.setTypeName("amount_raw_type_tlong"); + fieldTypeAmountRaw.setTypeName(FIELD_TYPE_AMOUNT_RAW); Map map = new HashMap<>(1); map.put("precisionStep", precisionStepString); fieldTypeAmountRaw.init(schema, map); - + fieldSuffixAmountRaw = FIELD_SUFFIX_AMOUNT_RAW; + // Initialize field type for currency string fieldTypeCurrency = new StrField(); - fieldTypeCurrency.setTypeName("currency_type_string"); - fieldTypeCurrency.init(schema, new HashMap()); - - args.remove(PARAM_RATE_PROVIDER_CLASS); - args.remove(PARAM_DEFAULT_CURRENCY); - args.remove(PARAM_PRECISION_STEP); + fieldTypeCurrency.setTypeName(FIELD_TYPE_CURRENCY); + fieldTypeCurrency.init(schema, Collections.emptyMap()); + fieldSuffixCurrency = FIELD_SUFFIX_CURRENCY; - try { - Class c = schema.getResourceLoader().findClass(exchangeRateProviderClass, ExchangeRateProvider.class); - provider = c.newInstance(); - provider.init(args); - } catch (Exception e) { - throw new SolrException(ErrorCode.BAD_REQUEST, "Error instantiating exchange rate provider "+exchangeRateProviderClass+": " + e.getMessage(), e); - } - } - - @Override - public boolean isPolyField() { - return true; - } - - @Override - public void checkSchemaField(final SchemaField field) throws SolrException { - super.checkSchemaField(field); - if (field.multiValued()) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, - "CurrencyFields can not be multiValued: " + - field.getName()); - } - } - - @Override - public List createFields(SchemaField field, Object externalVal) { - CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency); - - List f = new ArrayList<>(); - SchemaField amountField = getAmountField(field); - f.add(amountField.createField(String.valueOf(value.getAmount()))); - SchemaField currencyField = getCurrencyField(field); - f.add(currencyField.createField(value.getCurrencyCode())); - - if (field.stored()) { - String storedValue = externalVal.toString().trim(); - if (storedValue.indexOf(",") < 0) { - storedValue += "," + defaultCurrency; - } - f.add(createField(field.getName(), storedValue, StoredField.TYPE)); - } - - 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); + super.init(schema, args); // Must be called last so that field types are not doubly created } private void createDynamicCurrencyField(String suffix, FieldType type) { @@ -212,825 +97,15 @@ public class CurrencyField extends FieldType implements SchemaAware, ResourceLoa /** * When index schema is informed, add dynamic fields "*____currency" and "*____amount_raw". - * + * * {@inheritDoc} - * + * * @param schema {@inheritDoc} */ @Override public void inform(IndexSchema schema) { - this.schema = schema; createDynamicCurrencyField(FIELD_SUFFIX_CURRENCY, fieldTypeCurrency); createDynamicCurrencyField(FIELD_SUFFIX_AMOUNT_RAW, fieldTypeAmountRaw); - } - - /** - * Load the currency config when resource loader initialized. - * - * @param resourceLoader The resource loader. - */ - @Override - 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); - } - - /** - *

- * Returns a ValueSource over this field in which the numeric value for - * each document represents the indexed value as converted to the default - * currency for the field, normalized to its most granular form based - * on the default fractional digits. - *

- *

- * For example: If the default Currency specified for a field is - * USD, then the values returned by this value source would - * represent the equivilent number of "cents" (ie: value in dollars * 100) - * after converting each document's native currency to USD -- because the - * default fractional digits for USD is "2". - * So for a document whose indexed value was currently equivilent to - * "5.43,USD" using the the exchange provider for this field, - * this ValueSource would return a value of "543" - *

- * - * @see #PARAM_DEFAULT_CURRENCY - * @see #DEFAULT_DEFAULT_CURRENCY - * @see Currency#getDefaultFractionDigits - * @see #getConvertedValueSource - */ - public RawCurrencyValueSource getValueSource(SchemaField field, - QParser parser) { - getAmountField(field).checkFieldCacheSource(); - getCurrencyField(field).checkFieldCacheSource(); - return new RawCurrencyValueSource(field, defaultCurrency, parser); - } - - /** - *

- * Returns a ValueSource over this field in which the numeric value for - * each document represents the value from the underlying - * RawCurrencyValueSource as converted to the specified target - * Currency. - *

- *

- * For example: If the targetCurrencyCode param is set to - * USD, then the values returned by this value source would - * represent the equivilent number of dollars after converting each - * document's raw value to USD. So for a document whose - * indexed value was currently equivilent to "5.43,USD" - * using the the exchange provider for this field, this ValueSource would - * return a value of "5.43" - *

- * - * @param targetCurrencyCode The target currency for the resulting value source, if null the defaultCurrency for this field type will be used - * @param source the raw ValueSource to wrap - * @see #PARAM_DEFAULT_CURRENCY - * @see #DEFAULT_DEFAULT_CURRENCY - * @see #getValueSource - */ - public ValueSource getConvertedValueSource(String targetCurrencyCode, - RawCurrencyValueSource source) { - if (null == targetCurrencyCode) { - targetCurrencyCode = defaultCurrency; - } - return new ConvertedCurrencyValueSource(targetCurrencyCode, - source); - } - - @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 != null && p2 != null && !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 != null) ? p1.getCurrencyCode() : - (p2 != null) ? p2.getCurrencyCode() : defaultCurrency; - - // ValueSourceRangeFilter doesn't check exists(), so we have to - final Filter docsWithValues = new QueryWrapperFilter(new FieldValueQuery(getAmountField(field).getName())); - final Filter vsRangeFilter = new ValueSourceRangeFilter - (new RawCurrencyValueSource(field, currencyCode, parser), - p1 == null ? null : p1.getAmount() + "", - p2 == null ? null : p2.getAmount() + "", - minInclusive, maxInclusive); - final BooleanQuery.Builder docsInRange = new BooleanQuery.Builder(); - docsInRange.add(docsWithValues, Occur.FILTER); - docsInRange.add(vsRangeFilter, Occur.FILTER); - - return new SolrConstantScoreQuery(new QueryWrapperFilter(docsInRange.build())); - } - - @Override - public SortField getSortField(SchemaField field, boolean reverse) { - // Convert all values to default currency for sorting. - return (new RawCurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse); - } - - @Override - public Type getUninversionType(SchemaField sf) { - return null; - } - - @Override - public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException { - writer.writeStr(name, field.stringValue(), true); - } - - public ExchangeRateProvider getProvider() { - return provider; - } - - /** - *

- * A value source whose values represent the "normal" values - * in the specified target currency. - *

- * @see RawCurrencyValueSource - */ - class ConvertedCurrencyValueSource extends ValueSource { - private final Currency targetCurrency; - private final RawCurrencyValueSource source; - private final double rate; - public ConvertedCurrencyValueSource(String targetCurrencyCode, - RawCurrencyValueSource source) { - this.source = source; - this.targetCurrency = getCurrency(targetCurrencyCode); - if (null == targetCurrency) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode); - } - // the target digits & currency of our source, - // become the source digits & currency of ourselves - this.rate = provider.getExchangeRate - (source.getTargetCurrency().getCurrencyCode(), - targetCurrency.getCurrencyCode()); - } - - @Override - public FunctionValues getValues(Map context, LeafReaderContext reader) - throws IOException { - final FunctionValues amounts = source.getValues(context, reader); - // the target digits & currency of our source, - // become the source digits & currency of ourselves - final String sourceCurrencyCode = source.getTargetCurrency().getCurrencyCode(); - final int sourceFractionDigits = source.getTargetCurrency().getDefaultFractionDigits(); - final double divisor = Math.pow(10D, targetCurrency.getDefaultFractionDigits()); - return new FunctionValues() { - @Override - public boolean exists(int doc) throws IOException { - return amounts.exists(doc); - } - @Override - public long longVal(int doc) throws IOException { - return (long) doubleVal(doc); - } - @Override - public int intVal(int doc) throws IOException { - return (int) doubleVal(doc); - } - - @Override - public double doubleVal(int doc) throws IOException { - return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor; - } - - @Override - public float floatVal(int doc) throws IOException { - return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor); - } - - @Override - public String strVal(int doc) throws IOException { - return Double.toString(doubleVal(doc)); - } - - @Override - public String toString(int doc) throws IOException { - return name() + '(' + strVal(doc) + ')'; - } - }; - } - public String name() { - return "currency"; - } - - @Override - public String description() { - return name() + "(" + source.getField().getName() + "," + targetCurrency.getCurrencyCode()+")"; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o; - - return !(source != null ? !source.equals(that.source) : that.source != null) && - (rate == that.rate) && - !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null); - - } - - @Override - public int hashCode() { - int result = targetCurrency != null ? targetCurrency.hashCode() : 0; - result = 31 * result + (source != null ? source.hashCode() : 0); - result = 31 * (int) Double.doubleToLongBits(rate); - return result; - } - } - - /** - *

- * A value source whose values represent the "raw" (ie: normalized using - * the number of default fractional digits) values in the specified - * target currency). - *

- *

- * For example: if the specified target currency is "USD" - * then the numeric values are the number of pennies in the value - * (ie: $n * 100) since the number of defalt fractional - * digits for USD is "2") - *

- * @see ConvertedCurrencyValueSource - */ - class RawCurrencyValueSource extends ValueSource { - private static final long serialVersionUID = 1L; - private final Currency targetCurrency; - private ValueSource currencyValues; - private ValueSource amountValues; - private final SchemaField sf; - - public RawCurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) { - this.sf = sfield; - this.targetCurrency = getCurrency(targetCurrencyCode); - if (null == targetCurrency) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode); - } - - SchemaField amountField = getAmountField(sf); - SchemaField currencyField = getCurrencyField(sf); - - currencyValues = currencyField.getType().getValueSource(currencyField, parser); - amountValues = amountField.getType().getValueSource(amountField, parser); - } - - public SchemaField getField() { return sf; } - public Currency getTargetCurrency() { return targetCurrency; } - - @Override - public FunctionValues getValues(Map context, LeafReaderContext reader) throws IOException { - final FunctionValues amounts = amountValues.getValues(context, reader); - final FunctionValues currencies = currencyValues.getValues(context, reader); - - return new FunctionValues() { - private static 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) throws IOException { - 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(targetCurrency.getCurrencyCode() )) { - targetCurrencyOrd = currencyOrd; - } - - return currency; - } else { - return currencies.strVal(doc); - } - } - /** throws a (Server Error) SolrException if the code is not valid */ - private Currency getDocCurrency(int doc, int currencyOrd) throws IOException { - String code = getDocCurrencyCode(doc, currencyOrd); - Currency c = getCurrency(code); - if (null == c) { - throw new SolrException - (SolrException.ErrorCode.SERVER_ERROR, - "Currency code of document is not supported by this JVM: "+code); - } - return c; - } - - @Override - public boolean exists(int doc) throws IOException { - return amounts.exists(doc); - } - - @Override - public long longVal(int doc) throws IOException { - long amount = amounts.longVal(doc); - // bail fast using whatever amounts defaults to if no value - // (if we don't do this early, currencyOrd may be < 0, - // causing index bounds exception - if ( ! exists(doc) ) { - return amount; - } - - if (!initializedCache) { - for (int i = 0; i < fractionDigitCache.length; i++) { - fractionDigitCache[i] = -1; - } - - initializedCache = true; - } - - int currencyOrd = currencies.ordVal(doc); - - if (currencyOrd == targetCurrencyOrd) { - return amount; - } - - double exchangeRate; - int sourceFractionDigits; - - if (targetFractionDigits == -1) { - targetFractionDigits = targetCurrency.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, targetCurrency.getCurrencyCode()); - } - - sourceFractionDigits = fractionDigitCache[currencyOrd]; - - if (sourceFractionDigits == -1) { - sourceFractionDigits = fractionDigitCache[currencyOrd] = getDocCurrency(doc, currencyOrd).getDefaultFractionDigits(); - } - } else { - Currency source = getDocCurrency(doc, currencyOrd); - exchangeRate = provider.getExchangeRate(source.getCurrencyCode(), targetCurrency.getCurrencyCode()); - sourceFractionDigits = source.getDefaultFractionDigits(); - } - - return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits); - } - - @Override - public int intVal(int doc) throws IOException { - return (int) longVal(doc); - } - - @Override - public double doubleVal(int doc) throws IOException { - return (double) longVal(doc); - } - - @Override - public float floatVal(int doc) throws IOException { - return (float) longVal(doc); - } - - @Override - public String strVal(int doc) throws IOException { - return Long.toString(longVal(doc)); - } - - @Override - public String toString(int doc) throws IOException { - return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')'; - } - }; - } - - public String name() { - return "rawcurrency"; - } - - @Override - public String description() { - return name() + "(" + sf.getName() + - ",target="+targetCurrency.getCurrencyCode()+")"; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - RawCurrencyValueSource that = (RawCurrencyValueSource) o; - - return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) && - !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) && - !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null); - - } - - @Override - public int hashCode() { - int result = targetCurrency != null ? targetCurrency.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 { - private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); - 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 SolrException if the requested currency pair cannot be found - */ - @Override - 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; - } - - @Override - public String toString() { - return "["+this.getClass().getName()+" : " + rates.size() + " rates.]"; - } - - @Override - public Set listAvailableCurrencies() { - Set currencies = new HashSet<>(); - for(String from : rates.keySet()) { - currencies.add(from); - for(String to : rates.get(from).keySet()) { - currencies.add(to); - } - } - return currencies; - } - - @Override - public boolean reload() throws SolrException { - InputStream is = null; - Map> tmpRates = new HashMap<>(); - try { - log.debug("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.SERVER_ERROR, "Exchange rate missing attributes (required: from, to, rate) " + rateNode); - } - - String fromCurrency = from.getNodeValue(); - String toCurrency = to.getNodeValue(); - Double exchangeRate; - - if (null == CurrencyField.getCurrency(fromCurrency)) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'from' currency not supported in this JVM: " + fromCurrency); - } - if (null == CurrencyField.getCurrency(toCurrency)) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'to' currency not supported in this JVM: " + toCurrency); - } - - try { - exchangeRate = Double.parseDouble(rate.getNodeValue()); - } catch (NumberFormatException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Could not parse exchange rate: " + rateNode, e); - } - - addRate(tmpRates, fromCurrency, toCurrency, exchangeRate); - } - } catch (SAXException | XPathExpressionException | ParserConfigurationException | IOException e) { - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error parsing currency config.", e); - } - } catch (IOException e) { - throw new SolrException(ErrorCode.SERVER_ERROR, "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.SERVER_ERROR, "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) { - if (externalVal == null) { - return null; - } - String amount = externalVal; - String code = defaultCurrency; - - if (externalVal.contains(",")) { - String[] amountAndCode = externalVal.split(","); - amount = amountAndCode[0]; - code = amountAndCode[1]; - } - - if (amount.equals("*")) { - return null; - } - - Currency currency = CurrencyField.getCurrency(code); - - if (currency == null) { - throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + 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); - } - - @Override - public String toString() { - return String.valueOf(amount) + "," + currencyCode; + super.inform(schema); } } diff --git a/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java new file mode 100644 index 00000000000..0ace37e2772 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/CurrencyFieldType.java @@ -0,0 +1,829 @@ +/* + * 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. + */ + +package org.apache.solr.schema; + +import java.io.IOException; +import java.lang.invoke.MethodHandles; +import java.util.ArrayList; +import java.util.Currency; +import java.util.List; +import java.util.Map; + +import org.apache.lucene.analysis.util.ResourceLoader; +import org.apache.lucene.analysis.util.ResourceLoaderAware; +import org.apache.lucene.document.StoredField; +import org.apache.lucene.index.LeafReaderContext; +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.BooleanClause.Occur; +import org.apache.lucene.search.BooleanQuery; +import org.apache.lucene.search.FieldValueQuery; +import org.apache.lucene.search.Query; +import org.apache.lucene.search.SortField; +import org.apache.solr.uninverting.UninvertingReader.Type; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; +import org.apache.solr.response.TextResponseWriter; +import org.apache.solr.search.Filter; +import org.apache.solr.search.QParser; +import org.apache.solr.search.QueryWrapperFilter; +import org.apache.solr.search.SolrConstantScoreQuery; +import org.apache.solr.search.function.ValueSourceRangeFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Field type for support of monetary values. + *

+ * See http://wiki.apache.org/solr/CurrencyField + */ +public class CurrencyFieldType extends FieldType implements SchemaAware, ResourceLoaderAware { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + + protected static final String PARAM_DEFAULT_CURRENCY = "defaultCurrency"; + protected static final String DEFAULT_DEFAULT_CURRENCY = "USD"; + protected static final String PARAM_RATE_PROVIDER_CLASS = "providerClass"; + protected static final String DEFAULT_RATE_PROVIDER_CLASS = "solr.FileExchangeRateProvider"; + protected static final String PARAM_FIELD_SUFFIX_AMOUNT_RAW = "amountLongSuffix"; + protected static final String PARAM_FIELD_SUFFIX_CURRENCY = "codeStrSuffix"; + + protected IndexSchema schema; + protected FieldType fieldTypeCurrency; + protected FieldType fieldTypeAmountRaw; + protected String fieldSuffixAmountRaw; + protected String fieldSuffixCurrency; + + private String exchangeRateProviderClass; + private String defaultCurrency; + private ExchangeRateProvider provider; + + /** + * A wrapper around Currency.getInstance that returns null + * instead of throwing IllegalArgumentException + * if the specified Currency does not exist in this JVM. + * + * @see Currency#getInstance(String) + */ + public static Currency getCurrency(final String code) { + try { + return Currency.getInstance(code); + } catch (IllegalArgumentException e) { + /* :NOOP: */ + } + return null; + } + + @Override + protected void init(IndexSchema schema, Map args) { + super.init(schema, args); + if (this.isMultiValued()) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + getClass().getSimpleName() + " types can not be multiValued: " + this.typeName); + } + this.schema = schema; + + this.defaultCurrency = args.get(PARAM_DEFAULT_CURRENCY); + if (this.defaultCurrency == null) { + this.defaultCurrency = DEFAULT_DEFAULT_CURRENCY; + } else { + args.remove(PARAM_DEFAULT_CURRENCY); + } + if (null == getCurrency(this.defaultCurrency)) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, + "Default currency code is not supported by this JVM: " + this.defaultCurrency); + } + + this.exchangeRateProviderClass = args.get(PARAM_RATE_PROVIDER_CLASS); + if (this.exchangeRateProviderClass == null) { + this.exchangeRateProviderClass = DEFAULT_RATE_PROVIDER_CLASS; + } else { + args.remove(PARAM_RATE_PROVIDER_CLASS); + } + try { + Class c + = schema.getResourceLoader().findClass(exchangeRateProviderClass, ExchangeRateProvider.class); + provider = c.newInstance(); + provider.init(args); + } catch (Exception e) { + throw new SolrException(ErrorCode.SERVER_ERROR, + "Error instantiating exchange rate provider " + exchangeRateProviderClass + ": " + e.getMessage(), e); + } + + if (fieldTypeAmountRaw == null) { // Don't initialize if subclass already has done so + fieldSuffixAmountRaw = args.get(PARAM_FIELD_SUFFIX_AMOUNT_RAW); + if (fieldSuffixAmountRaw == null) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required param " + PARAM_FIELD_SUFFIX_AMOUNT_RAW); + } else { + args.remove(PARAM_FIELD_SUFFIX_AMOUNT_RAW); + } + } + + if (fieldTypeCurrency == null) { // Don't initialize if subclass already has done so + fieldSuffixCurrency = args.get(PARAM_FIELD_SUFFIX_CURRENCY); + if (fieldSuffixCurrency == null) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required param " + PARAM_FIELD_SUFFIX_CURRENCY); + } else { + args.remove(PARAM_FIELD_SUFFIX_CURRENCY); + } + } + } + + @Override + public boolean isPolyField() { + return true; + } + + @Override + public void checkSchemaField(final SchemaField field) throws SolrException { + super.checkSchemaField(field); + if (field.multiValued()) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + getClass().getSimpleName() + " fields can not be multiValued: " + field.getName()); + } + } + + @Override + public List createFields(SchemaField field, Object externalVal) { + CurrencyValue value = CurrencyValue.parse(externalVal.toString(), defaultCurrency); + + List f = new ArrayList<>(); + SchemaField amountField = getAmountField(field); + f.add(amountField.createField(String.valueOf(value.getAmount()))); + SchemaField currencyField = getCurrencyField(field); + f.add(currencyField.createField(value.getCurrencyCode())); + + if (field.stored()) { + String storedValue = externalVal.toString().trim(); + if (storedValue.indexOf(",") < 0) { + storedValue += "," + defaultCurrency; + } + f.add(createField(field.getName(), storedValue, StoredField.TYPE)); + } + + return f; + } + + private SchemaField getAmountField(SchemaField field) { + return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + fieldSuffixAmountRaw); + } + + private SchemaField getCurrencyField(SchemaField field) { + return schema.getField(field.getName() + POLY_FIELD_SEPARATOR + fieldSuffixCurrency); + } + + /** + * When index schema is informed, get field types for the configured dynamic sub-fields + * + * {@inheritDoc} + * + * @param schema {@inheritDoc} + */ + @Override + public void inform(IndexSchema schema) { + this.schema = schema; + if (null == fieldTypeAmountRaw) { + assert null != fieldSuffixAmountRaw : "How did we get here?"; + SchemaField field = schema.getFieldOrNull(POLY_FIELD_SEPARATOR + fieldSuffixAmountRaw); + if (field == null) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName() + + "\": Undefined dynamic field for " + PARAM_FIELD_SUFFIX_AMOUNT_RAW + "=\"" + fieldSuffixAmountRaw + "\""); + } + fieldTypeAmountRaw = field.getType(); + if (!(fieldTypeAmountRaw instanceof LongValueFieldType)) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName() + + "\": Dynamic field for " + PARAM_FIELD_SUFFIX_AMOUNT_RAW + "=\"" + fieldSuffixAmountRaw + + "\" must have type class extending LongValueFieldType"); + } + } + if (null == fieldTypeCurrency) { + assert null != fieldSuffixCurrency : "How did we get here?"; + SchemaField field = schema.getFieldOrNull(POLY_FIELD_SEPARATOR + fieldSuffixCurrency); + if (field == null) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName() + + "\": Undefined dynamic field for " + PARAM_FIELD_SUFFIX_CURRENCY + "=\"" + fieldSuffixCurrency + "\""); + } + fieldTypeCurrency = field.getType(); + if (!(fieldTypeCurrency instanceof StrField)) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Field type \"" + this.getTypeName() + + "\": Dynamic field for " + PARAM_FIELD_SUFFIX_CURRENCY + "=\"" + fieldSuffixCurrency + + "\" must have type class of (or extending) StrField"); + } + } + } + + /** + * Load the currency config when resource loader initialized. + * + * @param resourceLoader The resource loader. + */ + @Override + 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); + } + + /** + *

+ * Returns a ValueSource over this field in which the numeric value for + * each document represents the indexed value as converted to the default + * currency for the field, normalized to its most granular form based + * on the default fractional digits. + *

+ *

+ * For example: If the default Currency specified for a field is + * USD, then the values returned by this value source would + * represent the equivilent number of "cents" (ie: value in dollars * 100) + * after converting each document's native currency to USD -- because the + * default fractional digits for USD is "2". + * So for a document whose indexed value was currently equivilent to + * "5.43,USD" using the the exchange provider for this field, + * this ValueSource would return a value of "543" + *

+ * + * @see #PARAM_DEFAULT_CURRENCY + * @see #DEFAULT_DEFAULT_CURRENCY + * @see Currency#getDefaultFractionDigits + * @see #getConvertedValueSource + */ + public RawCurrencyValueSource getValueSource(SchemaField field, + QParser parser) { + getAmountField(field).checkFieldCacheSource(); + getCurrencyField(field).checkFieldCacheSource(); + return new RawCurrencyValueSource(field, defaultCurrency, parser); + } + + /** + *

+ * Returns a ValueSource over this field in which the numeric value for + * each document represents the value from the underlying + * RawCurrencyValueSource as converted to the specified target + * Currency. + *

+ *

+ * For example: If the targetCurrencyCode param is set to + * USD, then the values returned by this value source would + * represent the equivilent number of dollars after converting each + * document's raw value to USD. So for a document whose + * indexed value was currently equivilent to "5.43,USD" + * using the the exchange provider for this field, this ValueSource would + * return a value of "5.43" + *

+ * + * @param targetCurrencyCode The target currency for the resulting value source, if null the defaultCurrency for this field type will be used + * @param source the raw ValueSource to wrap + * @see #PARAM_DEFAULT_CURRENCY + * @see #DEFAULT_DEFAULT_CURRENCY + * @see #getValueSource + */ + public ValueSource getConvertedValueSource(String targetCurrencyCode, + RawCurrencyValueSource source) { + if (null == targetCurrencyCode) { + targetCurrencyCode = defaultCurrency; + } + return new ConvertedCurrencyValueSource(targetCurrencyCode, + source); + } + + @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 != null && p2 != null && !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 != null) ? p1.getCurrencyCode() : + (p2 != null) ? p2.getCurrencyCode() : defaultCurrency; + + // ValueSourceRangeFilter doesn't check exists(), so we have to + final Filter docsWithValues = new QueryWrapperFilter(new FieldValueQuery(getAmountField(field).getName())); + final Filter vsRangeFilter = new ValueSourceRangeFilter + (new RawCurrencyValueSource(field, currencyCode, parser), + p1 == null ? null : p1.getAmount() + "", + p2 == null ? null : p2.getAmount() + "", + minInclusive, maxInclusive); + final BooleanQuery.Builder docsInRange = new BooleanQuery.Builder(); + docsInRange.add(docsWithValues, Occur.FILTER); + docsInRange.add(vsRangeFilter, Occur.FILTER); + + return new SolrConstantScoreQuery(new QueryWrapperFilter(docsInRange.build())); + } + + @Override + public SortField getSortField(SchemaField field, boolean reverse) { + // Convert all values to default currency for sorting. + return (new RawCurrencyValueSource(field, defaultCurrency, null)).getSortField(reverse); + } + + @Override + public Type getUninversionType(SchemaField sf) { + return null; + } + + @Override + public void write(TextResponseWriter writer, String name, IndexableField field) throws IOException { + writer.writeStr(name, field.stringValue(), true); + } + + public ExchangeRateProvider getProvider() { + return provider; + } + + /** + *

+ * A value source whose values represent the "normal" values + * in the specified target currency. + *

+ * @see RawCurrencyValueSource + */ + class ConvertedCurrencyValueSource extends ValueSource { + private final Currency targetCurrency; + private final RawCurrencyValueSource source; + private final double rate; + public ConvertedCurrencyValueSource(String targetCurrencyCode, + RawCurrencyValueSource source) { + this.source = source; + this.targetCurrency = getCurrency(targetCurrencyCode); + if (null == targetCurrency) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode); + } + // the target digits & currency of our source, + // become the source digits & currency of ourselves + this.rate = provider.getExchangeRate + (source.getTargetCurrency().getCurrencyCode(), + targetCurrency.getCurrencyCode()); + } + + @Override + public FunctionValues getValues(Map context, LeafReaderContext reader) + throws IOException { + final FunctionValues amounts = source.getValues(context, reader); + // the target digits & currency of our source, + // become the source digits & currency of ourselves + final String sourceCurrencyCode = source.getTargetCurrency().getCurrencyCode(); + final double divisor = Math.pow(10D, targetCurrency.getDefaultFractionDigits()); + return new FunctionValues() { + @Override + public boolean exists(int doc) throws IOException { + return amounts.exists(doc); + } + @Override + public long longVal(int doc) throws IOException { + return (long) doubleVal(doc); + } + @Override + public int intVal(int doc) throws IOException { + return (int) doubleVal(doc); + } + + @Override + public double doubleVal(int doc) throws IOException { + return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / divisor; + } + + @Override + public float floatVal(int doc) throws IOException { + return CurrencyValue.convertAmount(rate, sourceCurrencyCode, amounts.longVal(doc), targetCurrency.getCurrencyCode()) / ((float)divisor); + } + + @Override + public String strVal(int doc) throws IOException { + return Double.toString(doubleVal(doc)); + } + + @Override + public String toString(int doc) throws IOException { + return name() + '(' + strVal(doc) + ')'; + } + }; + } + public String name() { + return "currency"; + } + + @Override + public String description() { + return name() + "(" + source.getField().getName() + "," + targetCurrency.getCurrencyCode()+")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o; + + return !(source != null ? !source.equals(that.source) : that.source != null) && + (rate == that.rate) && + !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null); + + } + + @Override + public int hashCode() { + int result = targetCurrency != null ? targetCurrency.hashCode() : 0; + result = 31 * result + (source != null ? source.hashCode() : 0); + result = 31 * (int) Double.doubleToLongBits(rate); + return result; + } + } + + /** + *

+ * A value source whose values represent the "raw" (ie: normalized using + * the number of default fractional digits) values in the specified + * target currency). + *

+ *

+ * For example: if the specified target currency is "USD" + * then the numeric values are the number of pennies in the value + * (ie: $n * 100) since the number of default fractional + * digits for USD is "2") + *

+ * @see ConvertedCurrencyValueSource + */ + class RawCurrencyValueSource extends ValueSource { + private static final long serialVersionUID = 1L; + private final Currency targetCurrency; + private ValueSource currencyValues; + private ValueSource amountValues; + private final SchemaField sf; + + public RawCurrencyValueSource(SchemaField sfield, String targetCurrencyCode, QParser parser) { + this.sf = sfield; + this.targetCurrency = getCurrency(targetCurrencyCode); + if (null == targetCurrency) { + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + targetCurrencyCode); + } + + SchemaField amountField = getAmountField(sf); + SchemaField currencyField = getCurrencyField(sf); + + currencyValues = currencyField.getType().getValueSource(currencyField, parser); + amountValues = amountField.getType().getValueSource(amountField, parser); + } + + public SchemaField getField() { return sf; } + public Currency getTargetCurrency() { return targetCurrency; } + + @Override + public FunctionValues getValues(Map context, LeafReaderContext reader) throws IOException { + final FunctionValues amounts = amountValues.getValues(context, reader); + final FunctionValues currencies = currencyValues.getValues(context, reader); + + return new FunctionValues() { + private static 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) throws IOException { + 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(targetCurrency.getCurrencyCode() )) { + targetCurrencyOrd = currencyOrd; + } + + return currency; + } else { + return currencies.strVal(doc); + } + } + /** throws a (Server Error) SolrException if the code is not valid */ + private Currency getDocCurrency(int doc, int currencyOrd) throws IOException { + String code = getDocCurrencyCode(doc, currencyOrd); + Currency c = getCurrency(code); + if (null == c) { + throw new SolrException + (SolrException.ErrorCode.SERVER_ERROR, + "Currency code of document is not supported by this JVM: "+code); + } + return c; + } + + @Override + public boolean exists(int doc) throws IOException { + return amounts.exists(doc); + } + + @Override + public long longVal(int doc) throws IOException { + long amount = amounts.longVal(doc); + // bail fast using whatever amounts defaults to if no value + // (if we don't do this early, currencyOrd may be < 0, + // causing index bounds exception + if ( ! exists(doc) ) { + return amount; + } + + if (!initializedCache) { + for (int i = 0; i < fractionDigitCache.length; i++) { + fractionDigitCache[i] = -1; + } + + initializedCache = true; + } + + int currencyOrd = currencies.ordVal(doc); + + if (currencyOrd == targetCurrencyOrd) { + return amount; + } + + double exchangeRate; + int sourceFractionDigits; + + if (targetFractionDigits == -1) { + targetFractionDigits = targetCurrency.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, targetCurrency.getCurrencyCode()); + } + + sourceFractionDigits = fractionDigitCache[currencyOrd]; + + if (sourceFractionDigits == -1) { + sourceFractionDigits = fractionDigitCache[currencyOrd] = getDocCurrency(doc, currencyOrd).getDefaultFractionDigits(); + } + } else { + Currency source = getDocCurrency(doc, currencyOrd); + exchangeRate = provider.getExchangeRate(source.getCurrencyCode(), targetCurrency.getCurrencyCode()); + sourceFractionDigits = source.getDefaultFractionDigits(); + } + + return CurrencyValue.convertAmount(exchangeRate, sourceFractionDigits, amount, targetFractionDigits); + } + + @Override + public int intVal(int doc) throws IOException { + return (int) longVal(doc); + } + + @Override + public double doubleVal(int doc) throws IOException { + return (double) longVal(doc); + } + + @Override + public float floatVal(int doc) throws IOException { + return (float) longVal(doc); + } + + @Override + public String strVal(int doc) throws IOException { + return Long.toString(longVal(doc)); + } + + @Override + public String toString(int doc) throws IOException { + return name() + '(' + amounts.toString(doc) + ',' + currencies.toString(doc) + ')'; + } + }; + } + + public String name() { + return "rawcurrency"; + } + + @Override + public String description() { + return name() + "(" + sf.getName() + + ",target="+targetCurrency.getCurrencyCode()+")"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + RawCurrencyValueSource that = (RawCurrencyValueSource) o; + + return !(amountValues != null ? !amountValues.equals(that.amountValues) : that.amountValues != null) && + !(currencyValues != null ? !currencyValues.equals(that.currencyValues) : that.currencyValues != null) && + !(targetCurrency != null ? !targetCurrency.equals(that.targetCurrency) : that.targetCurrency != null); + + } + + @Override + public int hashCode() { + int result = targetCurrency != null ? targetCurrency.hashCode() : 0; + result = 31 * result + (currencyValues != null ? currencyValues.hashCode() : 0); + result = 31 * result + (amountValues != null ? amountValues.hashCode() : 0); + return result; + } + } + + /** + * Represents a Currency field value, which includes a long amount and ISO currency code. + */ + static 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) { + if (externalVal == null) { + return null; + } + String amount = externalVal; + String code = defaultCurrency; + + if (externalVal.contains(",")) { + String[] amountAndCode = externalVal.split(","); + amount = amountAndCode[0]; + code = amountAndCode[1]; + } + + if (amount.equals("*")) { + return null; + } + + Currency currency = getCurrency(code); + + if (currency == null) { + throw new SolrException(ErrorCode.BAD_REQUEST, "Currency code not supported by this JVM: " + 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(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); + } + + @Override + public String toString() { + return String.valueOf(amount) + "," + currencyCode; + } + } +} + diff --git a/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java b/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java new file mode 100644 index 00000000000..014861793d7 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/schema/FileExchangeRateProvider.java @@ -0,0 +1,252 @@ +/* + * 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. + */ + +package org.apache.solr.schema; + +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.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.lucene.analysis.util.ResourceLoader; +import org.apache.solr.common.SolrException; +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; + +/** + * Configuration for currency. Provides currency exchange rates. + */ +class FileExchangeRateProvider implements ExchangeRateProvider { + private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); + 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 SolrException if the requested currency pair cannot be found + */ + @Override + 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; + } + + @Override + public String toString() { + return "["+this.getClass().getName()+" : " + rates.size() + " rates.]"; + } + + @Override + public Set listAvailableCurrencies() { + Set currencies = new HashSet<>(); + for(String from : rates.keySet()) { + currencies.add(from); + for(String to : rates.get(from).keySet()) { + currencies.add(to); + } + } + return currencies; + } + + @Override + public boolean reload() throws SolrException { + InputStream is = null; + Map> tmpRates = new HashMap<>(); + try { + log.debug("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.SERVER_ERROR, "Exchange rate missing attributes (required: from, to, rate) " + rateNode); + } + + String fromCurrency = from.getNodeValue(); + String toCurrency = to.getNodeValue(); + Double exchangeRate; + + if (null == CurrencyFieldType.getCurrency(fromCurrency)) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'from' currency not supported in this JVM: " + fromCurrency); + } + if (null == CurrencyFieldType.getCurrency(toCurrency)) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Specified 'to' currency not supported in this JVM: " + toCurrency); + } + + try { + exchangeRate = Double.parseDouble(rate.getNodeValue()); + } catch (NumberFormatException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Could not parse exchange rate: " + rateNode, e); + } + + addRate(tmpRates, fromCurrency, toCurrency, exchangeRate); + } + } catch (SAXException | XPathExpressionException | ParserConfigurationException | IOException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error parsing currency config.", e); + } + } catch (IOException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "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(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(SolrException.ErrorCode.SERVER_ERROR, "Needs ResourceLoader in order to load config file"); + } + this.loader = loader; + reload(); + } +} diff --git a/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java b/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java index 2d16108d200..2b6cbf6b417 100644 --- a/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java +++ b/solr/core/src/java/org/apache/solr/schema/OpenExchangeRatesOrgProvider.java @@ -35,7 +35,7 @@ import org.slf4j.LoggerFactory; /** *

- * Exchange Rates Provider for {@link CurrencyField} capable of fetching & + * Exchange Rates Provider for {@link CurrencyField} and {@link CurrencyFieldType} capable of fetching & * parsing the freely available exchange rates from openexchangerates.org *

*

diff --git a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java index b59927232a6..7d6d162ce1a 100644 --- a/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java +++ b/solr/core/src/java/org/apache/solr/search/ValueSourceParser.java @@ -47,7 +47,7 @@ import org.apache.lucene.util.BytesRefBuilder; import org.apache.solr.common.SolrException; import org.apache.solr.common.util.NamedList; import org.apache.solr.request.SolrRequestInfo; -import org.apache.solr.schema.CurrencyField; +import org.apache.solr.schema.CurrencyFieldType; import org.apache.solr.schema.FieldType; import org.apache.solr.schema.SchemaField; import org.apache.solr.schema.StrField; @@ -444,11 +444,11 @@ public abstract class ValueSourceParser implements NamedListInitializedPlugin { String fieldName = fp.parseArg(); SchemaField f = fp.getReq().getSchema().getField(fieldName); - if (! (f.getType() instanceof CurrencyField)) { + if (! (f.getType() instanceof CurrencyFieldType)) { throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, - "Currency function input must be the name of a CurrencyField: " + fieldName); + "Currency function input must be the name of a CurrencyFieldType: " + fieldName); } - CurrencyField ft = (CurrencyField) f.getType(); + CurrencyFieldType ft = (CurrencyFieldType) f.getType(); String code = fp.hasMoreArguments() ? fp.parseArg() : null; return ft.getConvertedValueSource(code, ft.getValueSource(f, fp)); } diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml new file mode 100644 index 00000000000..bafdb37d722 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-amount-suffix.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml new file mode 100644 index 00000000000..06973f46fa8 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-code-suffix.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml index 539f503eedc..a1d664cd027 100644 --- a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currency-ft-oer-norates.xml @@ -18,7 +18,7 @@ - + + + + + + + + + + + + + + + + + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml new file mode 100644 index 00000000000..717245533fd --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-bogus-code-suffix.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml new file mode 100644 index 00000000000..2fba82a6aec --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-dynamic-multivalued.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml new file mode 100644 index 00000000000..3b5c69535ac --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml new file mode 100644 index 00000000000..27483729127 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-bogus-default-code.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml new file mode 100644 index 00000000000..6afcea452e6 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-multivalued.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml new file mode 100644 index 00000000000..8d8533d75ed --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-ft-oer-norates.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml new file mode 100644 index 00000000000..77a531d5b32 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-amount-suffix.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml new file mode 100644 index 00000000000..89c70801055 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-missing-code-suffix.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml new file mode 100644 index 00000000000..9e95458331d --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-multivalued.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml new file mode 100644 index 00000000000..51881901279 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-amount-ft.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml new file mode 100644 index 00000000000..c3eadc1e019 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-schema-currencyfieldtype-wrong-code-ft.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + id + + diff --git a/solr/core/src/test-files/solr/collection1/conf/schema.xml b/solr/core/src/test-files/solr/collection1/conf/schema.xml index 23c104547f8..4aaef4842dd 100644 --- a/solr/core/src/test-files/solr/collection1/conf/schema.xml +++ b/solr/core/src/test-files/solr/collection1/conf/schema.xml @@ -451,6 +451,17 @@ multiValued="false" providerClass="solr.OpenExchangeRatesOrgProvider" ratesFileLocation="open-exchange-rates.json"/> + + + @@ -539,6 +550,9 @@ + + + @@ -652,8 +666,10 @@ + + diff --git a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java index b7d00a9c371..b9dc1aae0a1 100644 --- a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java +++ b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java @@ -68,14 +68,22 @@ public class BadIndexSchemaTest extends AbstractBadConfigTestBase { doTest("bad-schema-currency-ft-multivalued.xml", "types can not be multiValued: currency"); doTest("bad-schema-currency-multivalued.xml", - "Fields can not be multiValued: money"); + "fields can not be multiValued: money"); doTest("bad-schema-currency-dynamic-multivalued.xml", - "Fields can not be multiValued: *_c"); + "fields can not be multiValued: *_c"); + doTest("bad-schema-currencyfieldtype-ft-multivalued.xml", + "types can not be multiValued: currency"); + doTest("bad-schema-currencyfieldtype-multivalued.xml", + "fields can not be multiValued: money"); + doTest("bad-schema-currencyfieldtype-dynamic-multivalued.xml", + "fields can not be multiValued: *_c"); } public void testCurrencyOERNoRates() throws Exception { doTest("bad-schema-currency-ft-oer-norates.xml", "ratesFileLocation"); + doTest("bad-schema-currencyfieldtype-ft-oer-norates.xml", + "ratesFileLocation"); } public void testCurrencyBogusCode() throws Exception { @@ -83,6 +91,35 @@ public class BadIndexSchemaTest extends AbstractBadConfigTestBase { "HOSS"); doTest("bad-schema-currency-ft-bogus-code-in-xml.xml", "HOSS"); + doTest("bad-schema-currencyfieldtype-ft-bogus-default-code.xml", + "HOSS"); + doTest("bad-schema-currencyfieldtype-ft-bogus-code-in-xml.xml", + "HOSS"); + } + + public void testCurrencyDisallowedSuffixParams() throws Exception { + doTest("bad-schema-currency-ft-code-suffix.xml", + "Unknown parameter(s)"); + doTest("bad-schema-currency-ft-amount-suffix.xml", + "Unknown parameter(s)"); + } + + public void testCurrencyBogusSuffixes() throws Exception { + doTest("bad-schema-currencyfieldtype-bogus-code-suffix.xml", + "Undefined dynamic field for codeStrSuffix"); + doTest("bad-schema-currencyfieldtype-bogus-amount-suffix.xml", + "Undefined dynamic field for amountLongSuffix"); + doTest("bad-schema-currencyfieldtype-wrong-code-ft.xml", + "Dynamic field for codeStrSuffix=\"_l\" must have type class of (or extending) StrField"); + doTest("bad-schema-currencyfieldtype-wrong-amount-ft.xml", + "Dynamic field for amountLongSuffix=\"_s\" must have type class extending LongValueFieldType"); + } + + public void testCurrencyMissingSuffixes() throws Exception { + doTest("bad-schema-currencyfieldtype-missing-code-suffix.xml", + "Missing required param codeStrSuffix"); + doTest("bad-schema-currencyfieldtype-missing-amount-suffix.xml", + "Missing required param amountLongSuffix"); } public void testPerFieldtypeSimButNoSchemaSimFactory() throws Exception { diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java deleted file mode 100644 index fed51eb9aac..00000000000 --- a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldOpenExchangeTest.java +++ /dev/null @@ -1,27 +0,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. - */ -package org.apache.solr.schema; - -/** - * Tests currency field type using OpenExchangeRatesOrgProvider. - */ -public class CurrencyFieldOpenExchangeTest extends AbstractCurrencyFieldTest { - - public String field() { - return "oer_amount"; - } -} diff --git a/solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java similarity index 53% rename from solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java rename to solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java index be719dbd8e3..c2f8f2d97e5 100644 --- a/solr/core/src/test/org/apache/solr/schema/AbstractCurrencyFieldTest.java +++ b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldTypeTest.java @@ -14,12 +14,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.apache.solr.schema; + +import java.util.Arrays; import java.util.Currency; import java.util.List; import java.util.Random; import java.util.Set; +import com.carrotsearch.randomizedtesting.annotations.ParametersFactory; import org.apache.lucene.index.IndexableField; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.core.SolrCore; @@ -29,13 +33,28 @@ import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; -/** - * Tests currency field type. - * @see #field - */ -@Ignore("Abstract base class with test methods") -public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { +/** Tests CurrencyField and CurrencyFieldType. */ +public class CurrencyFieldTypeTest extends SolrTestCaseJ4 { + private final String fieldName; + private final Class expectedProviderClass; + + public CurrencyFieldTypeTest(String fieldName, Class expectedProviderClass) { + this.fieldName = fieldName; + this.expectedProviderClass = expectedProviderClass; + } + @ParametersFactory + public static Iterable parameters() { + return Arrays.asList(new Object[][] { + {"amount", FileExchangeRateProvider.class}, // CurrencyField + {"mock_amount", MockExchangeRateProvider.class}, // CurrencyField + {"oer_amount", OpenExchangeRatesOrgProvider.class}, // CurrencyField + {"amount_CFT", FileExchangeRateProvider.class}, // CurrencyFieldType + {"mock_amount_CFT", MockExchangeRateProvider.class}, // CurrencyFieldType + {"oer_amount_CFT", OpenExchangeRatesOrgProvider.class} // CurrencyFieldType + }); + } + /** * "Assumes" that the specified list of currency codes are * supported in this JVM @@ -59,44 +78,50 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { initCore("solrconfig.xml", "schema.xml"); } - /** The field name to use in all tests */ - public abstract String field(); - @Test public void testCurrencySchema() throws Exception { IndexSchema schema = h.getCore().getLatestSchema(); - SchemaField amount = schema.getField(field()); + SchemaField amount = schema.getField(fieldName); assertNotNull(amount); assertTrue(amount.isPolyField()); + CurrencyFieldType type = (CurrencyFieldType)amount.getType(); + String currencyDynamicField + = "*" + (type instanceof CurrencyField ? FieldType.POLY_FIELD_SEPARATOR : "") + type.fieldSuffixCurrency; + String amountDynamicField + = "*" + (type instanceof CurrencyField ? FieldType.POLY_FIELD_SEPARATOR : "") + type.fieldSuffixAmountRaw; + 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(amountDynamicField)) { + seenAmount = true; } - if (dynField.getName().equals("*" + FieldType.POLY_FIELD_SEPARATOR + CurrencyField.FIELD_SUFFIX_AMOUNT_RAW)) { - seenAmount = true; + if (dynField.getName().equals(currencyDynamicField)) { + seenCurrency = true; } } - assertTrue("Didn't find the expected currency code dynamic field", seenCurrency); - assertTrue("Didn't find the expected value dynamic field", seenAmount); + assertTrue("Didn't find the expected currency code dynamic field " + currencyDynamicField, seenCurrency); + assertTrue("Didn't find the expected value dynamic field " + amountDynamicField, seenAmount); } @Test public void testCurrencyFieldType() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + SolrCore core = h.getCore(); IndexSchema schema = core.getLatestSchema(); - SchemaField amount = schema.getField(field()); + SchemaField amount = schema.getField(fieldName); assertNotNull(amount); - assertTrue(field() + " is not a poly field", amount.isPolyField()); + assertTrue(fieldName + " is not a poly field", amount.isPolyField()); FieldType tmp = amount.getType(); - assertTrue(tmp instanceof CurrencyField); + assertTrue(fieldName + " is not an instance of CurrencyFieldType", tmp instanceof CurrencyFieldType); String currencyValue = "1.50,EUR"; List fields = amount.createFields(currencyValue); assertEquals(fields.size(), 3); @@ -112,41 +137,47 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { assertEquals(schema.getFieldTypeByName("string").toExternal(fields.get(2)), "1.50,EUR"); // A few tests on the provider directly - ExchangeRateProvider p = ((CurrencyField) tmp).getProvider(); + ExchangeRateProvider p = ((CurrencyFieldType)tmp).getProvider(); Set availableCurrencies = p.listAvailableCurrencies(); assertEquals(5, availableCurrencies.size()); - assert(p.reload() == true); - assert(p.getExchangeRate("USD", "EUR") == 2.5); + assertTrue(p.reload()); + assertEquals(2.5, p.getExchangeRate("USD", "EUR"), 0.00000000001); } @Test public void testMockExchangeRateProvider() throws Exception { + assumeTrue("This test is only applicable to the mock exchange rate provider", + expectedProviderClass.equals(MockExchangeRateProvider.class)); + SolrCore core = h.getCore(); IndexSchema schema = core.getLatestSchema(); - SchemaField amount = schema.getField("mock_amount"); + SchemaField field = schema.getField(fieldName); + FieldType fieldType = field.getType(); + ExchangeRateProvider provider = ((CurrencyFieldType)fieldType).getProvider(); // A few tests on the provider directly - ExchangeRateProvider p = ((CurrencyField)amount.getType()).getProvider(); - Set availableCurrencies = p.listAvailableCurrencies(); - assert(availableCurrencies.size() == 3); - assert(p.reload() == true); - assert(p.getExchangeRate("USD", "EUR") == 0.8); + assertEquals(3, provider.listAvailableCurrencies().size()); + assertTrue(provider.reload()); + assertEquals(0.8, provider.getExchangeRate("USD", "EUR"), 0.00000000001); } @Test public void testCurrencyRangeSearch() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + clearIndex(); final int emptyDocs = atLeast(50); // times 2 final int negDocs = atLeast(5); - assertU(adoc("id", "0", field(), "0,USD")); // 0 + assertU(adoc("id", "0", fieldName, "0,USD")); // 0 // lots of docs w/o values for (int i = 100; i <= 100 + emptyDocs; i++) { assertU(adoc("id", "" + i)); } // docs with values in ranges we'll query for (int i = 1; i <= 10; i++) { - assertU(adoc("id", "" + i, field(), i + ",USD")); + assertU(adoc("id", "" + i, fieldName, i + ",USD")); } // more docs w/o values for (int i = 500; i <= 500 + emptyDocs; i++) { @@ -154,62 +185,62 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { } // some negative values for (int i = -100; i > -100 - negDocs; i--) { - assertU(adoc("id", "" + i, field(), i + ",USD")); + assertU(adoc("id", "" + i, fieldName, i + ",USD")); } - assertU(adoc("id", "40", field(), "0,USD")); // 0 + assertU(adoc("id", "40", fieldName, "0,USD")); // 0 assertU(commit()); assertQ(req("fl", "*,score", "q", - field()+":[2.00,USD TO 5.00,USD]"), + fieldName+":[2.00,USD TO 5.00,USD]"), "//*[@numFound='4']"); assertQ(req("fl", "*,score", "q", - field()+":[0.50,USD TO 1.00,USD]"), + fieldName+":[0.50,USD TO 1.00,USD]"), "//*[@numFound='1']"); assertQ(req("fl", "*,score", "q", - field()+":[24.00,USD TO 25.00,USD]"), + fieldName+":[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", - field()+":[0.50,GBP TO 1.00,GBP]"), + fieldName+":[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", - field()+":[24.00,EUR TO 25.00,EUR]"), + fieldName+":[24.00,EUR TO 25.00,EUR]"), "//*[@numFound='1']"); // Slight asymmetric rate should work. assertQ(req("fl", "*,score", "q", - field()+":[24.99,EUR TO 25.01,EUR]"), + fieldName+":[24.99,EUR TO 25.01,EUR]"), "//*[@numFound='1']"); // Open ended ranges without currency assertQ(req("fl", "*,score", "q", - field()+":[* TO *]"), + fieldName+":[* TO *]"), "//*[@numFound='" + (2 + 10 + negDocs) + "']"); // Open ended ranges with currency assertQ(req("fl", "*,score", "q", - field()+":[*,EUR TO *,EUR]"), + fieldName+":[*,EUR TO *,EUR]"), "//*[@numFound='" + (2 + 10 + negDocs) + "']"); // Open ended start range without currency assertQ(req("fl", "*,score", "q", - field()+":[* TO 5,USD]"), + fieldName+":[* TO 5,USD]"), "//*[@numFound='" + (2 + 5 + negDocs) + "']"); // Open ended start range with currency (currency for the * won't matter) assertQ(req("fl", "*,score", "q", - field()+":[*,USD TO 5,USD]"), + fieldName+":[*,USD TO 5,USD]"), "//*[@numFound='" + (2 + 5 + negDocs) + "']"); // Open ended end range assertQ(req("fl", "*,score", "q", - field()+":[3 TO *]"), + fieldName+":[3 TO *]"), "//*[@numFound='8']"); } @@ -220,23 +251,26 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { // bogus currency assertQEx("Expected exception for invalid currency", req("fl", "*,score", "q", - field()+":[3,HOSS TO *]"), + fieldName+":[3,HOSS TO *]"), 400); } @Test public void testCurrencyPointQuery() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + clearIndex(); - assertU(adoc("id", "" + 1, field(), "10.00,USD")); - assertU(adoc("id", "" + 2, field(), "15.00,MXN")); + assertU(adoc("id", "" + 1, fieldName, "10.00,USD")); + assertU(adoc("id", "" + 2, fieldName, "15.00,MXN")); assertU(commit()); - assertQ(req("fl", "*,score", "q", field()+":10.00,USD"), "//str[@name='id']='1'"); - assertQ(req("fl", "*,score", "q", field()+":9.99,USD"), "//*[@numFound='0']"); - assertQ(req("fl", "*,score", "q", field()+":10.01,USD"), "//*[@numFound='0']"); - assertQ(req("fl", "*,score", "q", field()+":15.00,MXN"), "//str[@name='id']='2'"); - assertQ(req("fl", "*,score", "q", field()+":7.50,USD"), "//str[@name='id']='2'"); - assertQ(req("fl", "*,score", "q", field()+":7.49,USD"), "//*[@numFound='0']"); - assertQ(req("fl", "*,score", "q", field()+":7.51,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":10.00,USD"), "//str[@name='id']='1'"); + assertQ(req("fl", "*,score", "q", fieldName+":9.99,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":10.01,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":15.00,MXN"), "//str[@name='id']='2'"); + assertQ(req("fl", "*,score", "q", fieldName+":7.50,USD"), "//str[@name='id']='2'"); + assertQ(req("fl", "*,score", "q", fieldName+":7.49,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":7.51,USD"), "//*[@numFound='0']"); } @Ignore @@ -247,7 +281,7 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { int initDocs = 200000; for (int i = 1; i <= initDocs; i++) { - assertU(adoc("id", "" + i, field(), (r.nextInt(10) + 1.00) + ",USD")); + assertU(adoc("id", "" + i, fieldName, (r.nextInt(10) + 1.00) + ",USD")); if (i % 1000 == 0) System.out.println(i); } @@ -255,15 +289,15 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { assertU(commit()); for (int i = 0; i < 1000; i++) { double lower = r.nextInt(10) + 1.00; - assertQ(req("fl", "*,score", "q", field()+":[" + lower + ",USD TO " + (lower + 10.00) + ",USD]"), "//*"); - assertQ(req("fl", "*,score", "q", field()+":[" + lower + ",EUR TO " + (lower + 10.00) + ",EUR]"), "//*"); + assertQ(req("fl", "*,score", "q", fieldName+":[" + lower + ",USD TO " + (lower + 10.00) + ",USD]"), "//*"); + assertQ(req("fl", "*,score", "q", fieldName+":[" + lower + ",EUR TO " + (lower + 10.00) + ",EUR]"), "//*"); } for (int j = 0; j < 3; j++) { final RTimer timer = new RTimer(); for (int i = 0; i < 1000; i++) { double lower = r.nextInt(10) + 1.00; - assertQ(req("fl", "*,score", "q", field()+":[" + lower + ",USD TO " + (lower + (9.99 - (j * 0.01))) + ",USD]"), "//*"); + assertQ(req("fl", "*,score", "q", fieldName+":[" + lower + ",USD TO " + (lower + (9.99 - (j * 0.01))) + ",USD]"), "//*"); } System.out.println(timer.getTime()); @@ -275,7 +309,7 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { final RTimer timer = new RTimer(); for (int i = 0; i < 1000; i++) { double lower = r.nextInt(10) + 1.00; - assertQ(req("fl", "*,score", "q", field()+":[" + lower + ",EUR TO " + (lower + (9.99 - (j * 0.01))) + ",EUR]"), "//*"); + assertQ(req("fl", "*,score", "q", fieldName+":[" + lower + ",EUR TO " + (lower + (9.99 - (j * 0.01))) + ",EUR]"), "//*"); } System.out.println(timer.getTime()); @@ -284,27 +318,42 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { @Test public void testCurrencySort() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + clearIndex(); - assertU(adoc("id", "" + 1, field(), "10.00,USD")); - assertU(adoc("id", "" + 2, field(), "15.00,EUR")); - assertU(adoc("id", "" + 3, field(), "7.00,EUR")); - assertU(adoc("id", "" + 4, field(), "6.00,GBP")); - assertU(adoc("id", "" + 5, field(), "2.00,GBP")); + assertU(adoc("id", "" + 1, fieldName, "10.00,USD")); + assertU(adoc("id", "" + 2, fieldName, "15.00,EUR")); + assertU(adoc("id", "" + 3, fieldName, "7.00,EUR")); + assertU(adoc("id", "" + 4, fieldName, "6.00,GBP")); + assertU(adoc("id", "" + 5, fieldName, "2.00,GBP")); assertU(commit()); - assertQ(req("fl", "*,score", "q", "*:*", "sort", field()+" desc", "limit", "1"), "//str[@name='id']='4'"); - assertQ(req("fl", "*,score", "q", "*:*", "sort", field()+" asc", "limit", "1"), "//str[@name='id']='3'"); + assertQ(req("fl", "*,score", "q", "*:*", "sort", fieldName+" desc", "limit", "1"), "//str[@name='id']='4'"); + assertQ(req("fl", "*,score", "q", "*:*", "sort", fieldName+" asc", "limit", "1"), "//str[@name='id']='3'"); } + public void testExpectedProvider() { + SolrCore core = h.getCore(); + IndexSchema schema = core.getLatestSchema(); + SchemaField field = schema.getField(fieldName); + FieldType fieldType = field.getType(); + ExchangeRateProvider provider = ((CurrencyFieldType)fieldType).getProvider(); + assertEquals(expectedProviderClass, provider.getClass()); + } + public void testFunctionUsage() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + clearIndex(); for (int i = 1; i <= 8; i++) { // "GBP" currency code is 1/2 of a USD dollar, for testing. - assertU(adoc("id", "" + i, field(), (((float)i)/2) + ",GBP")); + assertU(adoc("id", "" + i, fieldName, (((float)i)/2) + ",GBP")); } for (int i = 9; i <= 11; i++) { - assertU(adoc("id", "" + i, field(), i + ",USD")); + assertU(adoc("id", "" + i, fieldName, i + ",USD")); } assertU(commit()); @@ -312,17 +361,17 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { // direct value source usage, gets "raw" form od default currency // default==USD, so raw==penies assertQ(req("fl", "id,func:field($f)", - "f", field(), + "f", fieldName, "q", "id:5"), "//*[@numFound='1']", "//doc/float[@name='func' and .=500]"); assertQ(req("fl", "id,func:field($f)", - "f", field(), + "f", fieldName, "q", "id:10"), "//*[@numFound='1']", "//doc/float[@name='func' and .=1000]"); - assertQ(req("fl", "id,score,"+field(), - "q", "{!frange u=500}"+field()) + assertQ(req("fl", "id,score,"+fieldName, + "q", "{!frange u=500}"+fieldName) ,"//*[@numFound='5']" ,"//str[@name='id']='1'" ,"//str[@name='id']='2'" @@ -330,8 +379,8 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { ,"//str[@name='id']='4'" ,"//str[@name='id']='5'" ); - assertQ(req("fl", "id,score,"+field(), - "q", "{!frange l=500 u=1000}"+field()) + assertQ(req("fl", "id,score,"+fieldName, + "q", "{!frange l=500 u=1000}"+fieldName) ,"//*[@numFound='6']" ,"//str[@name='id']='5'" ,"//str[@name='id']='6'" @@ -343,17 +392,17 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { // use the currency function to convert to default (USD) assertQ(req("fl", "id,func:currency($f)", - "f", field(), + "f", fieldName, "q", "id:10"), "//*[@numFound='1']", "//doc/float[@name='func' and .=10]"); assertQ(req("fl", "id,func:currency($f)", - "f", field(), + "f", fieldName, "q", "id:5"), "//*[@numFound='1']", "//doc/float[@name='func' and .=5]"); - assertQ(req("fl", "id,score"+field(), - "f", field(), + assertQ(req("fl", "id,score"+fieldName, + "f", fieldName, "q", "{!frange u=5}currency($f)") ,"//*[@numFound='5']" ,"//str[@name='id']='1'" @@ -362,8 +411,8 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { ,"//str[@name='id']='4'" ,"//str[@name='id']='5'" ); - assertQ(req("fl", "id,score"+field(), - "f", field(), + assertQ(req("fl", "id,score"+fieldName, + "f", fieldName, "q", "{!frange l=5 u=10}currency($f)") ,"//*[@numFound='6']" ,"//str[@name='id']='5'" @@ -376,17 +425,17 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { // use the currency function to convert to MXN assertQ(req("fl", "id,func:currency($f,MXN)", - "f", field(), + "f", fieldName, "q", "id:5"), "//*[@numFound='1']", "//doc/float[@name='func' and .=10]"); assertQ(req("fl", "id,func:currency($f,MXN)", - "f", field(), + "f", fieldName, "q", "id:10"), "//*[@numFound='1']", "//doc/float[@name='func' and .=20]"); - assertQ(req("fl", "*,score,"+field(), - "f", field(), + assertQ(req("fl", "*,score,"+fieldName, + "f", fieldName, "q", "{!frange u=10}currency($f,MXN)") ,"//*[@numFound='5']" ,"//str[@name='id']='1'" @@ -395,8 +444,8 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { ,"//str[@name='id']='4'" ,"//str[@name='id']='5'" ); - assertQ(req("fl", "*,score,"+field(), - "f", field(), + assertQ(req("fl", "*,score,"+fieldName, + "f", fieldName, "q", "{!frange l=10 u=20}currency($f,MXN)") ,"//*[@numFound='6']" ,"//str[@name='id']='5'" @@ -411,16 +460,35 @@ public abstract class AbstractCurrencyFieldTest extends SolrTestCaseJ4 { @Test public void testMockFieldType() throws Exception { + assumeTrue("This test is only applicable to the mock exchange rate provider", + expectedProviderClass.equals(MockExchangeRateProvider.class)); + clearIndex(); - 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(adoc("id", "1", fieldName, "1.00,USD")); + assertU(adoc("id", "2", fieldName, "1.00,EUR")); + assertU(adoc("id", "3", fieldName, "1.00,NOK")); assertU(commit()); - assertQ(req("fl", "*,score", "q", "mock_amount:5.0,NOK"), "//*[@numFound='1']", "//str[@name='id']='1'"); - assertQ(req("fl", "*,score", "q", "mock_amount:1.2,USD"), "//*[@numFound='1']", "//str[@name='id']='2'"); - assertQ(req("fl", "*,score", "q", "mock_amount:0.2,USD"), "//*[@numFound='1']", "//str[@name='id']='3'"); - assertQ(req("fl", "*,score", "q", "mock_amount:99,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":5.0,NOK"), "//*[@numFound='1']", "//str[@name='id']='1'"); + assertQ(req("fl", "*,score", "q", fieldName+":1.2,USD"), "//*[@numFound='1']", "//str[@name='id']='2'"); + assertQ(req("fl", "*,score", "q", fieldName+":0.2,USD"), "//*[@numFound='1']", "//str[@name='id']='3'"); + assertQ(req("fl", "*,score", "q", fieldName+":99,USD"), "//*[@numFound='0']"); + } + + @Test + public void testAsymmetricPointQuery() throws Exception { + assumeTrue("This test is only applicable to the XML file based exchange rate provider", + expectedProviderClass.equals(FileExchangeRateProvider.class)); + + clearIndex(); + assertU(adoc("id", "" + 1, fieldName, "10.00,USD")); + assertU(adoc("id", "" + 2, fieldName, "15.00,EUR")); + assertU(commit()); + + assertQ(req("fl", "*,score", "q", fieldName+":15.00,EUR"), "//str[@name='id']='2'"); + assertQ(req("fl", "*,score", "q", fieldName+":7.50,USD"), "//str[@name='id']='2'"); + assertQ(req("fl", "*,score", "q", fieldName+":7.49,USD"), "//*[@numFound='0']"); + assertQ(req("fl", "*,score", "q", fieldName+":7.51,USD"), "//*[@numFound='0']"); } } diff --git a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldXmlFileTest.java b/solr/core/src/test/org/apache/solr/schema/CurrencyFieldXmlFileTest.java deleted file mode 100644 index e8bffe7023f..00000000000 --- a/solr/core/src/test/org/apache/solr/schema/CurrencyFieldXmlFileTest.java +++ /dev/null @@ -1,42 +0,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. - */ -package org.apache.solr.schema; -import org.junit.Test; - -/** - * Tests currency field type using FileExchangeRateProvider - */ -public class CurrencyFieldXmlFileTest extends AbstractCurrencyFieldTest { - - public String field() { - return "amount"; - } - - @Test - public void testAsymetricPointQuery() throws Exception { - clearIndex(); - assertU(adoc("id", "" + 1, field(), "10.00,USD")); - assertU(adoc("id", "" + 2, field(), "15.00,EUR")); - assertU(commit()); - - assertQ(req("fl", "*,score", "q", field()+":15.00,EUR"), "//str[@name='id']='2'"); - assertQ(req("fl", "*,score", "q", field()+":7.50,USD"), "//str[@name='id']='2'"); - assertQ(req("fl", "*,score", "q", field()+":7.49,USD"), "//*[@numFound='0']"); - assertQ(req("fl", "*,score", "q", field()+":7.51,USD"), "//*[@numFound='0']"); - } - -} diff --git a/solr/core/src/test/org/apache/solr/schema/OpenExchangeRatesOrgProviderTest.java b/solr/core/src/test/org/apache/solr/schema/OpenExchangeRatesOrgProviderTest.java index ce44c38d823..972ebe6cd40 100644 --- a/solr/core/src/test/org/apache/solr/schema/OpenExchangeRatesOrgProviderTest.java +++ b/solr/core/src/test/org/apache/solr/schema/OpenExchangeRatesOrgProviderTest.java @@ -40,7 +40,7 @@ public class OpenExchangeRatesOrgProviderTest extends SolrTestCaseJ4 { @Override @Before public void setUp() throws Exception { - AbstractCurrencyFieldTest.assumeCurrencySupport + CurrencyFieldTypeTest.assumeCurrencySupport ("USD", "EUR", "MXN", "GBP", "JPY"); super.setUp(); diff --git a/solr/example/example-DIH/solr/db/conf/managed-schema b/solr/example/example-DIH/solr/db/conf/managed-schema index de138ba1345..b96ff4c0cd6 100644 --- a/solr/example/example-DIH/solr/db/conf/managed-schema +++ b/solr/example/example-DIH/solr/db/conf/managed-schema @@ -202,8 +202,10 @@ + + @@ -703,17 +705,22 @@ - - + diff --git a/solr/example/example-DIH/solr/mail/conf/managed-schema b/solr/example/example-DIH/solr/mail/conf/managed-schema index 7b058ebd108..e027717cfca 100644 --- a/solr/example/example-DIH/solr/mail/conf/managed-schema +++ b/solr/example/example-DIH/solr/mail/conf/managed-schema @@ -138,8 +138,10 @@ + + @@ -623,17 +625,22 @@ - - + diff --git a/solr/example/example-DIH/solr/solr/conf/currency.xml b/solr/example/example-DIH/solr/solr/conf/currency.xml index 3a9c58afee8..532221a90bc 100644 --- a/solr/example/example-DIH/solr/solr/conf/currency.xml +++ b/solr/example/example-DIH/solr/solr/conf/currency.xml @@ -16,7 +16,7 @@ limitations under the License. --> - + diff --git a/solr/example/example-DIH/solr/solr/conf/managed-schema b/solr/example/example-DIH/solr/solr/conf/managed-schema index 679f567710c..338aa358ba4 100644 --- a/solr/example/example-DIH/solr/solr/conf/managed-schema +++ b/solr/example/example-DIH/solr/solr/conf/managed-schema @@ -202,8 +202,10 @@ + + @@ -703,17 +705,22 @@ - - + diff --git a/solr/example/files/conf/managed-schema b/solr/example/files/conf/managed-schema index bf154f47c0b..fe91a5d8a07 100644 --- a/solr/example/files/conf/managed-schema +++ b/solr/example/files/conf/managed-schema @@ -13,7 +13,7 @@ - + @@ -504,6 +504,8 @@ + + diff --git a/solr/server/solr/configsets/basic_configs/conf/managed-schema b/solr/server/solr/configsets/basic_configs/conf/managed-schema index 599b0cca4d3..c2db621d5df 100644 --- a/solr/server/solr/configsets/basic_configs/conf/managed-schema +++ b/solr/server/solr/configsets/basic_configs/conf/managed-schema @@ -136,8 +136,10 @@ + + @@ -601,19 +603,24 @@ - - - + amountLongSuffix: Required. Refers to a dynamic field for the raw amount sub-field. + The dynamic field must have a field type that extends LongValueFieldType. + Note: If you expect to use Atomic Updates, this dynamic field may not be stored. + codeStrSuffix: Required. Refers to a dynamic field for the currency code sub-field. + The dynamic field must have a field type that extends StrField. + Note: If you expect to use Atomic Updates, this dynamic field may not be stored. + defaultCurrency: Specifies the default currency if none specified. Defaults to "USD" + providerClass: Lets you plug in other exchange provider backend: + solr.FileExchangeRateProvider is the default and takes one parameter: + currencyConfig: name of an xml file holding exchange rates + solr.OpenExchangeRatesOrgProvider uses rates from openexchangerates.org: + ratesFileLocation: URL or path to rates JSON file (default latest.json on the web) + refreshInterval: Number of minutes between each rates fetch (default: 1440, min: 60) + --> + diff --git a/solr/server/solr/configsets/data_driven_schema_configs/conf/currency.xml b/solr/server/solr/configsets/data_driven_schema_configs/conf/currency.xml index 3a9c58afee8..532221a90bc 100644 --- a/solr/server/solr/configsets/data_driven_schema_configs/conf/currency.xml +++ b/solr/server/solr/configsets/data_driven_schema_configs/conf/currency.xml @@ -16,7 +16,7 @@ limitations under the License. --> - + diff --git a/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema b/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema index 80a58fbe312..5604a93b12d 100644 --- a/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema +++ b/solr/server/solr/configsets/data_driven_schema_configs/conf/managed-schema @@ -136,8 +136,10 @@ + + @@ -625,19 +627,24 @@ - - - + amountLongSuffix: Required. Refers to a dynamic field for the raw amount sub-field. + The dynamic field must have a field type that extends LongValueFieldType. + Note: If you expect to use Atomic Updates, this dynamic field may not be stored. + codeStrSuffix: Required. Refers to a dynamic field for the currency code sub-field. + The dynamic field must have a field type that extends StrField. + Note: If you expect to use Atomic Updates, this dynamic field may not be stored. + defaultCurrency: Specifies the default currency if none specified. Defaults to "USD" + providerClass: Lets you plug in other exchange provider backend: + solr.FileExchangeRateProvider is the default and takes one parameter: + currencyConfig: name of an xml file holding exchange rates + solr.OpenExchangeRatesOrgProvider uses rates from openexchangerates.org: + ratesFileLocation: URL or path to rates JSON file (default latest.json on the web) + refreshInterval: Number of minutes between each rates fetch (default: 1440, min: 60) + --> + diff --git a/solr/server/solr/configsets/sample_techproducts_configs/conf/managed-schema b/solr/server/solr/configsets/sample_techproducts_configs/conf/managed-schema index 1c4f1feab59..cd33c4f1068 100644 --- a/solr/server/solr/configsets/sample_techproducts_configs/conf/managed-schema +++ b/solr/server/solr/configsets/sample_techproducts_configs/conf/managed-schema @@ -199,8 +199,10 @@ + + @@ -760,17 +762,22 @@ - - + diff --git a/solr/solr-ref-guide/src/field-types-included-with-solr.adoc b/solr/solr-ref-guide/src/field-types-included-with-solr.adoc index 88af33b6cef..5c82970b777 100644 --- a/solr/solr-ref-guide/src/field-types-included-with-solr.adoc +++ b/solr/solr-ref-guide/src/field-types-included-with-solr.adoc @@ -28,7 +28,8 @@ The following table lists the field types that are available in Solr. The `org.a |BinaryField |Binary data. |BoolField |Contains either true or false. Values of "1", "t", or "T" in the first character are interpreted as true. Any other values in the first character are interpreted as false. |CollationField |Supports Unicode collation for sorting and range queries. ICUCollationField is a better choice if you can use ICU4J. See the section <>. -|CurrencyField |Supports currencies and exchange rates. See the section <>. +|CurrencyField |Deprecated in favor of CurrencyFieldType. +|CurrencyFieldType |Supports currencies and exchange rates. See the section <>. |DateRangeField |Supports indexing date ranges, to include point in time date instances as well (single-millisecond durations). See the section <> for more detail on using this field type. Consider using this field type even if it's just for date instances, particularly when the queries typically fall on UTC year/month/day/hour, etc., boundaries. |ExternalFileField |Pulls values from a file on disk. See the section <>. |EnumField |Allows defining an enumerated set of values which may not be easily sorted by either alphabetic or numeric order (such as a list of severities, for example). This field type takes a configuration file, which lists the proper order of the field values. See the section <> for more information. diff --git a/solr/solr-ref-guide/src/updating-parts-of-documents.adoc b/solr/solr-ref-guide/src/updating-parts-of-documents.adoc index fac3cac57b1..5ff8a2827ee 100644 --- a/solr/solr-ref-guide/src/updating-parts-of-documents.adoc +++ b/solr/solr-ref-guide/src/updating-parts-of-documents.adoc @@ -59,7 +59,7 @@ The core functionality of atomically updating a document requires that all field If `` destinations are configured as stored, then Solr will attempt to index both the current value of the field as well as an additional copy from any source fields. If such fields contain some information that comes from the indexing program and some information that comes from copyField, then the information which originally came from the indexing program will be lost when an atomic update is made. -There are other kinds of derived fields that must also be set so they aren't stored. Some spatial field types use derived fields. Examples of this are solr.BBoxField and solr.LatLonType. These types create additional fields which are normally specified by a dynamic field definition. That dynamic field definition must be not stored, or indexing will fail. +There are other kinds of derived fields that must also be set so they aren't stored. Some spatial field types use derived fields. Examples of this are solr.BBoxField and solr.LatLonType. CurrencyFieldType also uses derived fields. These types create additional fields which are normally specified by a dynamic field definition. That dynamic field definition must be not stored, or indexing will fail. [[UpdatingPartsofDocuments-Example]] === Example diff --git a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc index c651263e0d4..5ed4a56a7c9 100644 --- a/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc +++ b/solr/solr-ref-guide/src/working-with-currencies-and-exchange-rates.adoc @@ -30,11 +30,18 @@ The `currency` FieldType provides support for monetary values to Solr/Lucene wit [[WorkingwithCurrenciesandExchangeRates-ConfiguringCurrencies]] == Configuring Currencies -The `currency` field type is defined in `schema.xml`. This is the default configuration of this type: +.CurrencyField has been Deprecated +[WARNING] +==== +CurrencyField has been deprecated in favor of CurrencyFieldType; all configuration examples below use CurrencyFieldType. +==== + +The `currency` field type is defined in `schema.xml`. This is the default configuration of this type. [source,xml] ---- - ---- @@ -53,6 +60,26 @@ At indexing time, money fields can be indexed in a native currency. For example, During query processing, range and point queries are both supported. +[[WorkingwithCurrenciesandExchangeRates-Sub-fieldSuffixes]] +=== Sub-field Suffixes + +You must specify parameters `amountLongSuffix` and `codeStrSuffix`, corresponding to dynamic fields to be used for the raw amount and the currency dynamic sub-fields, e.g.: + +[source,xml] +---- + +---- + +In the above example, the raw amount field will use the `"*_l_ns"` dynamic field, which must exist in the schema and use a long field type, i.e. one that extends `LongValueFieldType`. The currency code field will use the `"*_s_ns"` dynamic field, which must exist in the schema and use a string field type, i.e. one that is or extends `StrField`. + +.Atomic Updates won't work if dynamic sub-fields are stored +[NOTE] +==== +As noted on <>, stored dynamic sub-fields will cause indexing to fail when you use Atomic Updates. To avoid this problem, specify `stored="false"` on those dynamic fields. +==== + [[WorkingwithCurrenciesandExchangeRates-ExchangeRates]] == Exchange Rates @@ -95,7 +122,8 @@ In this case, you need to specify the `providerClass` in the definitions for the [source,xml] ---- -