blob: 4bd3071ca0c94dab4341e1d310289c2ec10a1d9e [file] [log] [blame]
/*
* 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.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.EnumFieldSource;
import org.apache.lucene.search.SortField;
import org.apache.lucene.util.BytesRefBuilder;
import org.apache.solr.common.EnumFieldValue;
import org.apache.solr.common.SolrException;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.util.SafeXMLParsing;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/***
* Abstract Field type for support of string values with custom sort order.
*/
public abstract class AbstractEnumField extends PrimitiveFieldType {
protected EnumMapping enumMapping;
@Override
protected void init(IndexSchema schema, Map<String, String> args) {
super.init(schema, args);
enumMapping = new EnumMapping(schema, this, args);
}
public EnumMapping getEnumMapping() {
return enumMapping;
}
/**
* Models all the info contained in an enums config XML file
* @lucene.internal
*/
public static final class EnumMapping {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
public static final String PARAM_ENUMS_CONFIG = "enumsConfig";
public static final String PARAM_ENUM_NAME = "enumName";
public static final Integer DEFAULT_VALUE = -1;
public final Map<String, Integer> enumStringToIntMap;
public final Map<Integer, String> enumIntToStringMap;
protected final String enumsConfigFile;
protected final String enumName;
/**
* Takes in a FieldType and the initArgs Map used for that type, removing the keys
* that specify the enum.
*
* @param schema for opening resources
* @param fieldType Used for logging or error messages
* @param args the init args to comsume the enum name + config file from
*/
public EnumMapping(IndexSchema schema, FieldType fieldType, Map<String, String> args) {
final String ftName = fieldType.getTypeName();
// NOTE: ghosting member variables for most of constructor
final Map<String, Integer> enumStringToIntMap = new HashMap<>();
final Map<Integer, String> enumIntToStringMap = new HashMap<>();
enumsConfigFile = args.get(PARAM_ENUMS_CONFIG);
if (enumsConfigFile == null) {
throw new SolrException(SolrException.ErrorCode.NOT_FOUND,
ftName + ": No enums config file was configured.");
}
enumName = args.get(PARAM_ENUM_NAME);
if (enumName == null) {
throw new SolrException(SolrException.ErrorCode.NOT_FOUND,
ftName + ": No enum name was configured.");
}
final SolrResourceLoader loader = schema.getResourceLoader();
try {
log.debug("Reloading enums config file from {}", enumsConfigFile);
Document doc = SafeXMLParsing.parseConfigXML(log, loader, enumsConfigFile);
final XPathFactory xpathFactory = XPathFactory.newInstance();
final XPath xpath = xpathFactory.newXPath();
final String xpathStr = String.format(Locale.ROOT, "/enumsConfig/enum[@name='%s']", enumName);
final NodeList nodes = (NodeList) xpath.evaluate(xpathStr, doc, XPathConstants.NODESET);
final int nodesLength = nodes.getLength();
if (nodesLength == 0) {
String exceptionMessage = String.format
(Locale.ENGLISH, "%s: No enum configuration found for enum '%s' in %s.",
ftName, enumName, enumsConfigFile);
throw new SolrException(SolrException.ErrorCode.NOT_FOUND, exceptionMessage);
}
if (nodesLength > 1) {
log.warn("{}: More than one enum configuration found for enum '{}' in {}. The last one was taken."
, ftName, enumName, enumsConfigFile);
}
final Node enumNode = nodes.item(nodesLength - 1);
final NodeList valueNodes = (NodeList) xpath.evaluate("value", enumNode, XPathConstants.NODESET);
for (int i = 0; i < valueNodes.getLength(); i++) {
final Node valueNode = valueNodes.item(i);
final String valueStr = valueNode.getTextContent();
if ((valueStr == null) || (valueStr.length() == 0)) {
final String exceptionMessage = String.format
(Locale.ENGLISH, "%s: A value was defined with an no value in enum '%s' in %s.",
ftName, enumName, enumsConfigFile);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, exceptionMessage);
}
if (enumStringToIntMap.containsKey(valueStr)) {
final String exceptionMessage = String.format
(Locale.ENGLISH, "%s: A duplicated definition was found for value '%s' in enum '%s' in %s.",
ftName, valueStr, enumName, enumsConfigFile);
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, exceptionMessage);
}
enumIntToStringMap.put(i, valueStr);
enumStringToIntMap.put(valueStr, i);
}
} catch (IOException | SAXException | XPathExpressionException e) {
throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
ftName + ": Error while parsing enums config.", e);
}
if ((enumStringToIntMap.size() == 0) || (enumIntToStringMap.size() == 0)) {
String exceptionMessage = String.format
(Locale.ENGLISH, "%s: Invalid configuration was defined for enum '%s' in %s.",
ftName, enumName, enumsConfigFile);
throw new SolrException(SolrException.ErrorCode.NOT_FOUND, exceptionMessage);
}
this.enumStringToIntMap = Collections.unmodifiableMap(enumStringToIntMap);
this.enumIntToStringMap = Collections.unmodifiableMap(enumIntToStringMap);
args.remove(PARAM_ENUMS_CONFIG);
args.remove(PARAM_ENUM_NAME);
}
/**
* Converting the (internal) integer value (indicating the sort order) to string (displayed) value
* @param intVal integer value
* @return string value
*/
public String intValueToStringValue(Integer intVal) {
if (intVal == null)
return null;
final String enumString = enumIntToStringMap.get(intVal);
if (enumString != null)
return enumString;
// can't find matching enum name - return DEFAULT_VALUE.toString()
return DEFAULT_VALUE.toString();
}
/**
* Converting the string (displayed) value (internal) to integer value (indicating the sort order)
* @param stringVal string value
* @return integer value
*/
public Integer stringValueToIntValue(String stringVal) {
if (stringVal == null)
return null;
Integer intValue;
final Integer enumInt = enumStringToIntMap.get(stringVal);
if (enumInt != null) //enum int found for string
return enumInt;
//enum int not found for string
intValue = tryParseInt(stringVal);
if (intValue == null) //not Integer
intValue = DEFAULT_VALUE;
final String enumString = enumIntToStringMap.get(intValue);
if (enumString != null) //has matching string
return intValue;
return DEFAULT_VALUE;
}
private static Integer tryParseInt(String valueStr) {
Integer intValue = null;
try {
intValue = Integer.parseInt(valueStr);
}
catch (NumberFormatException e) {
}
return intValue;
}
}
@Override
public EnumFieldValue toObject(IndexableField f) {
Integer intValue = null;
String stringValue = null;
final Number val = f.numericValue();
if (val != null) {
intValue = val.intValue();
stringValue = enumMapping.intValueToStringValue(intValue);
}
return new EnumFieldValue(intValue, stringValue);
}
@Override
public SortField getSortField(SchemaField field, boolean top) {
if (field.multiValued()) {
MultiValueSelector selector = field.type.getDefaultMultiValueSelectorForSort(field, top);
if (null != selector) {
final SortField result = getSortedSetSortField(field, selector.getSortedSetSelectorType(),
// yes: Strings, it's how SortedSetSortField works
top, SortField.STRING_FIRST, SortField.STRING_LAST);
if (null == result.getMissingValue()) {
// special case 'enum' default behavior: assume missing values are "below" all enum values
result.setMissingValue(SortField.STRING_FIRST);
}
return result;
}
}
// else...
// either single valued, or don't support implicit multi selector
// (in which case let getSortField() give the error)
final SortField result = getSortField(field, SortField.Type.INT, top, Integer.MIN_VALUE, Integer.MAX_VALUE);
if (null == result.getMissingValue()) {
// special case 'enum' default behavior: assume missing values are "below" all enum values
result.setMissingValue(Integer.MIN_VALUE);
}
return result;
}
@Override
public ValueSource getValueSource(SchemaField field, QParser qparser) {
field.checkFieldCacheSource();
return new EnumFieldSource(field.getName(), enumMapping.enumIntToStringMap, enumMapping.enumStringToIntMap);
}
@Override
public void write(TextResponseWriter writer, String name, IndexableField f) throws IOException {
final Number val = f.numericValue();
if (val == null) {
writer.writeNull(name);
return;
}
final String readableValue = enumMapping.intValueToStringValue(val.intValue());
writer.writeStr(name, readableValue, true);
}
@Override
public boolean isTokenized() {
return false;
}
@Override
public NumberType getNumberType() {
return NumberType.INTEGER;
}
@Override
public String readableToIndexed(String val) {
if (val == null)
return null;
final BytesRefBuilder bytes = new BytesRefBuilder();
readableToIndexed(val, bytes);
return bytes.get().utf8ToString();
}
@Override
public String toInternal(String val) {
return readableToIndexed(val);
}
@Override
public String toExternal(IndexableField f) {
final Number val = f.numericValue();
if (val == null)
return null;
return enumMapping.intValueToStringValue(val.intValue());
}
@Override
public Object toNativeType(Object val) {
if (val instanceof CharSequence) {
final String str = val.toString();
final Integer entry = enumMapping.enumStringToIntMap.get(str);
if (entry != null) {
return new EnumFieldValue(entry, str);
} else if (NumberUtils.isCreatable(str)) {
final int num = Integer.parseInt(str);
return new EnumFieldValue(num, enumMapping.enumIntToStringMap.get(num));
}
} else if (val instanceof Number) {
final int num = ((Number) val).intValue();
return new EnumFieldValue(num, enumMapping.enumIntToStringMap.get(num));
}
return super.toNativeType(val);
}
}