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

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

/*
 * 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 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 enumStringToIntMap;
    public final Map 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 args) {
      final String ftName = fieldType.getTypeName();
      
      // NOTE: ghosting member variables for most of constructor
      final Map enumStringToIntMap = new HashMap<>();
      final Map 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);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy