org.apache.solr.schema.CurrencyFieldType Maven / Gradle / Ivy
Show all versions of solr-core Show documentation
/*
* 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