All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.solr.schema.CurrencyFieldType Maven / Gradle / Ivy

There is a newer version: 9.6.1
Show newest version
/*
 * 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 java.util.Objects;
import org.apache.lucene.document.StoredField;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.index.LeafReaderContext;
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.ConstantScoreQuery;
import org.apache.lucene.search.DocValuesFieldExistsQuery;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.ResourceLoader;
import org.apache.lucene.util.ResourceLoaderAware;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.search.function.ValueSourceRangeFilter;
import org.apache.solr.uninverting.UninvertingReader.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Field type for support of monetary values.
 *
 * 

See https://solr.apache.org/guide/solr/latest/indexing-guide/currencies-exchange-rates.html */ 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; } /** The identifier code for the default currency of this field type */ public String getDefaultCurrency() { return defaultCurrency; } @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.getConstructor().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.contains(",")) { 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 getRangeQueryInternal(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 equivalent 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 equivalent 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 */ @Override 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 equivalent number of dollars after * converting each document's raw value to USD. So for a document whose indexed value * was currently equivalent 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 the default existenceQuery implementation to run an existence query on the underlying * amountField instead. */ @Override public Query getExistenceQuery(QParser parser, SchemaField field) { // Use an existence query of the underlying amount field SchemaField amountField = getAmountField(field); return amountField.getType().getExistenceQuery(parser, amountField); } @Override protected Query getSpecializedRangeQuery( 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 getRangeQueryInternal(parser, field, p1, p2, minInclusive, maxInclusive); } private Query getRangeQueryInternal( 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 Query docsWithValues = new DocValuesFieldExistsQuery(getAmountField(field).getName()); final Query vsRangeFilter = new ValueSourceRangeFilter( new RawCurrencyValueSource(field, currencyCode, parser), p1 == null ? null : p1.getAmount() + "", p2 == null ? null : p2.getAmount() + "", minInclusive, maxInclusive); return new ConstantScoreQuery( new BooleanQuery.Builder() .add(docsWithValues, Occur.FILTER) .add(vsRangeFilter, Occur.FILTER) .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 instanceof ConvertedCurrencyValueSource)) return false; ConvertedCurrencyValueSource that = (ConvertedCurrencyValueSource) o; return Objects.equals(source, that.source) && (rate == that.rate) && Objects.equals(targetCurrency, that.targetCurrency); } @Override public int hashCode() { int result = targetCurrency != null ? targetCurrency.hashCode() : 0; result = 31 * result + (source != null ? source.hashCode() : 0); result = 31 * result + (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 instanceof RawCurrencyValueSource)) return false; RawCurrencyValueSource that = (RawCurrencyValueSource) o; return Objects.equals(amountValues, that.amountValues) && Objects.equals(currencyValues, that.currencyValues) && Objects.equals(targetCurrency, that.targetCurrency); } @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; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy