| package org.apache.solr.schema; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| import org.apache.lucene.document.FieldType; |
| import org.apache.lucene.index.StorableField; |
| import org.apache.lucene.queries.function.ValueSource; |
| import org.apache.lucene.queries.function.valuesource.EnumFieldSource; |
| import org.apache.lucene.search.*; |
| import org.apache.lucene.util.BytesRef; |
| import org.apache.lucene.util.CharsRef; |
| import org.apache.lucene.util.NumericUtils; |
| import org.apache.solr.common.EnumFieldValue; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.response.TextResponseWriter; |
| import org.apache.solr.search.QParser; |
| 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; |
| |
| import javax.xml.parsers.DocumentBuilderFactory; |
| import javax.xml.parsers.ParserConfigurationException; |
| import javax.xml.xpath.XPath; |
| import javax.xml.xpath.XPathConstants; |
| import javax.xml.xpath.XPathExpressionException; |
| import javax.xml.xpath.XPathFactory; |
| import java.io.IOException; |
| import java.io.InputStream; |
| |
| import java.util.HashMap; |
| import java.util.Locale; |
| import java.util.Map; |
| |
| /*** |
| * Field type for support of string values with custom sort order. |
| */ |
| public class EnumField extends PrimitiveFieldType { |
| |
| public static final Logger log = LoggerFactory.getLogger(EnumField.class); |
| protected static final Locale LOCALE = Locale.getDefault(); |
| protected static final String PARAM_ENUMS_CONFIG = "enumsConfig"; |
| protected static final String PARAM_ENUM_NAME = "enumName"; |
| protected static final Integer DEFAULT_VALUE = -1; |
| protected static final int DEFAULT_PRECISION_STEP = Integer.MAX_VALUE; |
| |
| protected Map<String, Integer> enumStringToIntMap = new HashMap<>(); |
| protected Map<Integer, String> enumIntToStringMap = new HashMap<>(); |
| |
| protected String enumsConfigFile; |
| protected String enumName; |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| protected void init(IndexSchema schema, Map<String, String> args) { |
| super.init(schema, args); |
| enumsConfigFile = args.get(PARAM_ENUMS_CONFIG); |
| if (enumsConfigFile == null) { |
| throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "No enums config file was configured."); |
| } |
| enumName = args.get(PARAM_ENUM_NAME); |
| if (enumName == null) { |
| throw new SolrException(SolrException.ErrorCode.NOT_FOUND, "No enum name was configured."); |
| } |
| |
| InputStream is = null; |
| |
| try { |
| is = schema.getResourceLoader().openResource(enumsConfigFile); |
| final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
| try { |
| final Document doc = dbf.newDocumentBuilder().parse(is); |
| final XPathFactory xpathFactory = XPathFactory.newInstance(); |
| final XPath xpath = xpathFactory.newXPath(); |
| final String xpathStr = String.format(LOCALE, "/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, "No enum configuration found for enum '%s' in %s.", |
| enumName, enumsConfigFile); |
| throw new SolrException(SolrException.ErrorCode.NOT_FOUND, exceptionMessage); |
| } |
| if (nodesLength > 1) { |
| if (log.isWarnEnabled()) |
| log.warn("More than one enum configuration found for enum '{}' in {}. The last one was taken.", 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, "A value was defined with an no value in enum '%s' in %s.", |
| enumName, enumsConfigFile); |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, exceptionMessage); |
| } |
| if (enumStringToIntMap.containsKey(valueStr)) { |
| final String exceptionMessage = String.format(LOCALE, "A duplicated definition was found for value '%s' in enum '%s' in %s.", |
| valueStr, enumName, enumsConfigFile); |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, exceptionMessage); |
| } |
| enumIntToStringMap.put(i, valueStr); |
| enumStringToIntMap.put(valueStr, i); |
| } |
| } |
| catch (ParserConfigurationException e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing enums config.", e); |
| } |
| catch (SAXException e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing enums config.", e); |
| } |
| catch (XPathExpressionException e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error parsing enums config.", e); |
| } |
| } |
| catch (IOException e) { |
| throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "Error while opening enums config.", e); |
| } finally { |
| try { |
| if (is != null) { |
| is.close(); |
| } |
| } |
| catch (IOException e) { |
| e.printStackTrace(); |
| } |
| } |
| |
| if ((enumStringToIntMap.size() == 0) || (enumIntToStringMap.size() == 0)) { |
| String exceptionMessage = String.format(LOCALE, "Invalid configuration was defined for enum '%s' in %s.", |
| enumName, enumsConfigFile); |
| throw new SolrException(SolrException.ErrorCode.NOT_FOUND, exceptionMessage); |
| } |
| |
| args.remove(PARAM_ENUMS_CONFIG); |
| args.remove(PARAM_ENUM_NAME); |
| } |
| |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public EnumFieldValue toObject(StorableField f) { |
| Integer intValue = null; |
| String stringValue = null; |
| final Number val = f.numericValue(); |
| if (val != null) { |
| intValue = val.intValue(); |
| stringValue = intValueToStringValue(intValue); |
| } |
| return new EnumFieldValue(intValue, stringValue); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public SortField getSortField(SchemaField field, boolean top) { |
| field.checkSortability(); |
| final Object missingValue = Integer.MIN_VALUE; |
| SortField sf = new SortField(field.getName(), FieldCache.NUMERIC_UTILS_INT_PARSER, top); |
| sf.setMissingValue(missingValue); |
| return sf; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public ValueSource getValueSource(SchemaField field, QParser qparser) { |
| field.checkFieldCacheSource(qparser); |
| return new EnumFieldSource(field.getName(), FieldCache.NUMERIC_UTILS_INT_PARSER, enumIntToStringMap, enumStringToIntMap); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void write(TextResponseWriter writer, String name, StorableField f) throws IOException { |
| final Number val = f.numericValue(); |
| if (val == null) { |
| writer.writeNull(name); |
| return; |
| } |
| |
| final String readableValue = intValueToStringValue(val.intValue()); |
| writer.writeStr(name, readableValue, true); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public boolean isTokenized() { |
| return false; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public Query getRangeQuery(QParser parser, SchemaField field, String min, String max, boolean minInclusive, boolean maxInclusive) { |
| Integer minValue = stringValueToIntValue(min); |
| Integer maxValue = stringValueToIntValue(max); |
| |
| if (field.multiValued() && field.hasDocValues() && !field.indexed()) { |
| // for the multi-valued dv-case, the default rangeimpl over toInternal is correct |
| return super.getRangeQuery(parser, field, minValue.toString(), maxValue.toString(), minInclusive, maxInclusive); |
| } |
| Query query = null; |
| final boolean matchOnly = field.hasDocValues() && !field.indexed(); |
| if (matchOnly) { |
| query = new ConstantScoreQuery(FieldCacheRangeFilter.newIntRange(field.getName(), |
| min == null ? null : minValue, |
| max == null ? null : maxValue, |
| minInclusive, maxInclusive)); |
| } else { |
| query = NumericRangeQuery.newIntRange(field.getName(), DEFAULT_PRECISION_STEP, |
| min == null ? null : minValue, |
| max == null ? null : maxValue, |
| minInclusive, maxInclusive); |
| } |
| |
| return query; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void checkSchemaField(final SchemaField field) { |
| if (field.hasDocValues() && !field.multiValued() && !(field.isRequired() || field.getDefaultValue() != null)) { |
| throw new IllegalStateException("Field " + this + " has single-valued doc values enabled, but has no default value and is not required"); |
| } |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String readableToIndexed(String val) { |
| if (val == null) |
| return null; |
| |
| final BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_LONG); |
| readableToIndexed(val, bytes); |
| return bytes.utf8ToString(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public void readableToIndexed(CharSequence val, BytesRef result) { |
| final String s = val.toString(); |
| if (s == null) |
| return; |
| |
| final Integer intValue = stringValueToIntValue(s); |
| NumericUtils.intToPrefixCoded(intValue, 0, result); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String toInternal(String val) { |
| return readableToIndexed(val); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String toExternal(StorableField f) { |
| final Number val = f.numericValue(); |
| if (val == null) |
| return null; |
| |
| return intValueToStringValue(val.intValue()); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String indexedToReadable(String indexedForm) { |
| if (indexedForm == null) |
| return null; |
| final BytesRef bytesRef = new BytesRef(indexedForm); |
| final Integer intValue = NumericUtils.prefixCodedToInt(bytesRef); |
| return intValueToStringValue(intValue); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public CharsRef indexedToReadable(BytesRef input, CharsRef output) { |
| final Integer intValue = NumericUtils.prefixCodedToInt(input); |
| final String stringValue = intValueToStringValue(intValue); |
| output.grow(stringValue.length()); |
| output.length = stringValue.length(); |
| stringValue.getChars(0, output.length, output.chars, 0); |
| return output; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public EnumFieldValue toObject(SchemaField sf, BytesRef term) { |
| final Integer intValue = NumericUtils.prefixCodedToInt(term); |
| final String stringValue = intValueToStringValue(intValue); |
| return new EnumFieldValue(intValue, stringValue); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public String storedToIndexed(StorableField f) { |
| final Number val = f.numericValue(); |
| if (val == null) |
| return null; |
| final BytesRef bytes = new BytesRef(NumericUtils.BUF_SIZE_LONG); |
| NumericUtils.intToPrefixCoded(val.intValue(), 0, bytes); |
| return bytes.utf8ToString(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| @Override |
| public StorableField createField(SchemaField field, Object value, float boost) { |
| final boolean indexed = field.indexed(); |
| final boolean stored = field.stored(); |
| final boolean docValues = field.hasDocValues(); |
| |
| if (!indexed && !stored && !docValues) { |
| if (log.isTraceEnabled()) |
| log.trace("Ignoring unindexed/unstored field: " + field); |
| return null; |
| } |
| final Integer intValue = stringValueToIntValue(value.toString()); |
| if (intValue == null || intValue.equals(DEFAULT_VALUE)) |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unknown value for enum field: " + value.toString()); |
| |
| String intAsString = intValue.toString(); |
| final FieldType newType = new FieldType(); |
| |
| newType.setIndexed(field.indexed()); |
| newType.setTokenized(field.isTokenized()); |
| newType.setStored(field.stored()); |
| newType.setOmitNorms(field.omitNorms()); |
| newType.setIndexOptions(getIndexOptions(field, intAsString)); |
| newType.setStoreTermVectors(field.storeTermVector()); |
| newType.setStoreTermVectorOffsets(field.storeTermOffsets()); |
| newType.setStoreTermVectorPositions(field.storeTermPositions()); |
| newType.setNumericType(FieldType.NumericType.INT); |
| newType.setNumericPrecisionStep(DEFAULT_PRECISION_STEP); |
| |
| final org.apache.lucene.document.Field f; |
| f = new org.apache.lucene.document.IntField(field.getName(), intValue.intValue(), newType); |
| |
| f.setBoost(boost); |
| return f; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| } |
| |