/*
 * 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.exoplatform.services.jcr.impl.core.query.lucene;

import org.apache.lucene.analysis.Analyzer;
import org.exoplatform.commons.utils.ClassLoading;
import org.exoplatform.services.jcr.core.NamespaceAccessor;
import org.exoplatform.services.jcr.core.nodetype.NodeTypeDataManager;
import org.exoplatform.services.jcr.dataflow.ItemDataConsumer;
import org.exoplatform.services.jcr.datamodel.IllegalNameException;
import org.exoplatform.services.jcr.datamodel.InternalQName;
import org.exoplatform.services.jcr.datamodel.NodeData;
import org.exoplatform.services.jcr.datamodel.PropertyData;
import org.exoplatform.services.jcr.datamodel.QPath;
import org.exoplatform.services.jcr.datamodel.QPathEntry;
import org.exoplatform.services.jcr.datamodel.ValueData;
import org.exoplatform.services.jcr.impl.Constants;
import org.exoplatform.services.jcr.impl.core.LocationFactory;
import org.exoplatform.services.jcr.impl.core.query.AdditionalNamespaceResolver;
import org.exoplatform.services.jcr.impl.core.query.QueryHandlerContext;
import org.exoplatform.services.jcr.impl.core.query.misc.Pattern;
import org.exoplatform.services.jcr.impl.dataflow.ValueDataUtil;
import org.exoplatform.services.jcr.impl.util.ISO9075;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.CharacterData;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;

/**
 * <code>IndexingConfigurationImpl</code> implements a concrete indexing
 * configuration.
 */
public class IndexingConfigurationImpl implements IndexingConfiguration
{

   /**
    * The logger instance for this class
    */
   private static final Logger LOG = LoggerFactory.getLogger("exo.jcr.component.core.IndexingConfigurationImpl");

   /**
    * A namespace resolver for parsing QNames in the configuration.
    */
   private LocationFactory resolver;

   /**
    * The item state manager to retrieve additional item states.
    */
   private ItemDataConsumer ism;

   /**
    * The {@link IndexingRule}s inside this configuration.
    */
   private Map<InternalQName, List<IndexingRule>> configElements = new HashMap<InternalQName, List<IndexingRule>>();

   /**
    * The indexing aggregates inside this configuration.
    */
   private AggregateRule[] aggregateRules;

   /**
    * The configured analyzers for indexing properties.
    */
   private Map<String, Analyzer> analyzers = new HashMap<String, Analyzer>();

   private Set<ExcludingRule> excludingRules = new HashSet<ExcludingRule>();

   /**
    * {@inheritDoc}
    */
   public void init(Element config, QueryHandlerContext context, NamespaceMappings nsMappings) throws Exception
   {
      ism = context.getItemStateManager();

      NamespaceAccessor nsResolver = new AdditionalNamespaceResolver(getNamespaces(config));
      resolver = new LocationFactory(nsResolver);// new
      // ParsingNameResolver(NameFactoryImpl.getInstance(),
      // nsResolver);

      NodeTypeDataManager ntReg = context.getNodeTypeDataManager();
      //List<NodeTypeData> ntNames = ntReg.getAllNodeTypes();
      List<AggregateRuleImpl> idxAggregates = new ArrayList<AggregateRuleImpl>();
      NodeList indexingConfigs = config.getChildNodes();
      for (int i = 0; i < indexingConfigs.getLength(); i++)
      {
         Node configNode = indexingConfigs.item(i);
         if (configNode.getNodeName().equals("index-rule"))
         {
            IndexingRule element = new IndexingRule(configNode);
            // register under node type and all its sub types
            LOG.debug("Found rule '{}' for NodeType '{}'", element, element.getNodeTypeName());
            Set<InternalQName> subs = ntReg.getSubtypes(element.getNodeTypeName());
            subs.add(element.getNodeTypeName());
            for (InternalQName subTypeName : subs)
            {
               List<IndexingRule> perNtConfig = configElements.get(subTypeName);
               if (perNtConfig == null)
               {
                  perNtConfig = new ArrayList<IndexingRule>();
                  configElements.put(subTypeName, perNtConfig);
               }
               LOG.debug("Registering it for name '{}'", subTypeName);
               perNtConfig.add(new IndexingRule(element, subTypeName));

            }

         }
         else if (configNode.getNodeName().equals("aggregate"))
         {
            idxAggregates.add(new AggregateRuleImpl(configNode, resolver, ism));
         }
         else if (configNode.getNodeName().equals("analyzers"))
         {
            NodeList childNodes = configNode.getChildNodes();
            for (int j = 0; j < childNodes.getLength(); j++)
            {
               Node analyzerNode = childNodes.item(j);
               if (analyzerNode.getNodeName().equals("analyzer"))
               {
                  String analyzerClassName = analyzerNode.getAttributes().getNamedItem("class").getNodeValue();
                  try
                  {
                     Class<?> clazz = ClassLoading.forName(analyzerClassName, this);
                     if (clazz == JcrStandartAnalyzer.class)
                     {
                        LOG.warn("Not allowed to configure " + JcrStandartAnalyzer.class.getName()
                           + " for a property. " + "Using default analyzer for that property.");
                     }
                     else if (Analyzer.class.isAssignableFrom(clazz))
                     {
                        Analyzer analyzer = (Analyzer)clazz.newInstance();
                        NodeList propertyChildNodes = analyzerNode.getChildNodes();
                        for (int k = 0; k < propertyChildNodes.getLength(); k++)
                        {
                           Node propertyNode = propertyChildNodes.item(k);
                           if (propertyNode.getNodeName().equals("property"))
                           {
                              // get property name
                              InternalQName propName =
                                 resolver.parseJCRName(getTextContent(propertyNode)).getInternalName();
                              String fieldName = nsMappings.translateName(propName);
                              // set analyzer for the fulltext
                              // property fieldname
                              int idx = fieldName.indexOf(':');
                              fieldName =
                                 fieldName.substring(0, idx + 1) + FieldNames.FULLTEXT_PREFIX
                                    + fieldName.substring(idx + 1);
                              Object prevAnalyzer = analyzers.put(fieldName, analyzer);
                              if (prevAnalyzer != null)
                              {
                                 LOG.warn("Property " + propName.getName()
                                    + " has been configured for multiple analyzers. "
                                    + " Last configured analyzer is used");
                              }
                           }
                        }
                     }
                     else
                     {
                        LOG.warn("org.apache.lucene.analysis.Analyzer is not a superclass of " + analyzerClassName
                           + ". Ignoring this configure analyzer");
                     }
                  }
                  catch (ClassNotFoundException e)
                  {
                     LOG.warn("Analyzer class not found: " + analyzerClassName, e);
                  }
               }
            }
         }
         else if (configNode.getNodeName().equals("exclude"))
         {
            excludingRules.add(new ExcludingRuleImpl(configNode, ntReg, resolver));
         }

      }
      aggregateRules = idxAggregates.toArray(new AggregateRule[idxAggregates.size()]);
   }

   /**
    * Returns the configured indexing aggregate rules or <code>null</code> if
    * none exist.
    * 
    * @return the configured rules or <code>null</code> if none exist.
    */
   public AggregateRule[] getAggregateRules()
   {
      return aggregateRules;
   }

   /**
    * Returns <code>true</code> if the property with the given name is fulltext
    * indexed according to this configuration.
    * 
    * @param state
    *            the node state.
    * @param propertyName
    *            the name of a property.
    * @return <code>true</code> if the property is fulltext indexed;
    *         <code>false</code> otherwise.
    */
   public boolean isIndexed(NodeData state, InternalQName propertyName)
   {
      IndexingRule rule = getApplicableIndexingRule(state);
      if (rule != null)
      {
         return rule.isIndexed(propertyName);
      }
      // none of the configs matches -> index property
      return true;
   }

   /**
    * {@inheritDoc}
    */
   public boolean isExcluded(NodeData state)
   {
      for (ExcludingRule rule : excludingRules)
      {
         if (rule.suiteFor(state))
         {
            return true;
         }
      }

      return false;
   }

   /**
    * Returns the boost value for the given property name. If there is no
    * configuration entry for the property name the {@link #DEFAULT_BOOST} is
    * returned.
    * 
    * @param state
    *            the node state.
    * @param propertyName
    *            the name of a property.
    * @return the boost value for the property.
    */
   public float getPropertyBoost(NodeData state, InternalQName propertyName)
   {
      IndexingRule rule = getApplicableIndexingRule(state);
      if (rule != null)
      {
         return rule.getBoost(propertyName);
      }
      return DEFAULT_BOOST;
   }

   /**
    * Returns the boost for the node scope fulltext index field.
    * 
    * @param state
    *            the node state.
    * @return the boost for the node scope fulltext index field.
    */
   public float getNodeBoost(NodeData state)
   {
      IndexingRule rule = getApplicableIndexingRule(state);
      if (rule != null)
      {
         return rule.getNodeBoost();
      }
      return DEFAULT_BOOST;
   }

   /**
    * Returns <code>true</code> if the property with the given name should be
    * included in the node scope fulltext index. If there is not configuration
    * entry for that propery <code>false</code> is returned.
    * 
    * @param state
    *            the node state.
    * @param propertyName
    *            the name of a property.
    * @return <code>true</code> if the property should be included in the node
    *         scope fulltext index.
    */
   public boolean isIncludedInNodeScopeIndex(NodeData state, InternalQName propertyName)
   {
      IndexingRule rule = getApplicableIndexingRule(state);
      if (rule != null)
      {
         return rule.isIncludedInNodeScopeIndex(propertyName);
      }
      // none of the config elements matched -> default is to include
      return true;
   }

   /**
    * Returns <code>true</code> if the content of the property with the given
    * name should show up in an excerpt. If there is no configuration entry for
    * that property <code>true</code> is returned.
    * 
    * @param state
    *            the node state.
    * @param propertyName
    *            the name of a property.
    * @return <code>true</code> if the content of the property should be
    *         included in an excerpt; <code>false</code> otherwise.
    */
   public boolean useInExcerpt(NodeData state, InternalQName propertyName)
   {
      IndexingRule rule = getApplicableIndexingRule(state);
      if (rule != null)
      {
         return rule.useInExcerpt(propertyName);
      }
      // none of the config elements matched -> default is to include
      return true;
   }

   /**
    * Returns the analyzer configured for the property with this fieldName (the
    * string representation ,JCR-style name, of the given
    * <code>InternalQName</code> prefixed with
    * <code>FieldNames.FULLTEXT_PREFIX</code>)), and <code>null</code> if none
    * is configured, or the configured analyzer cannot be found. If
    * <code>null</code> is returned, the default Analyzer is used.
    * 
    * @param fieldName
    *            the string representation ,JCR-style name, of the given
    *            <code>InternalQName</code> prefixed with
    *            <code>FieldNames.FULLTEXT_PREFIX</code>))
    * @return the <code>analyzer</code> to use for indexing this property
    */
   public Analyzer getPropertyAnalyzer(String fieldName)
   {
      if (analyzers.containsKey(fieldName))
      {
         return analyzers.get(fieldName);
      }
      return null;
   }

   // ---------------------------------< internal
   // >-----------------------------

   /**
    * Returns the first indexing rule that applies to the given node
    * <code>state</code>.
    * 
    * @param state
    *            a node state.
    * @return the indexing rule or <code>null</code> if none applies.
    */
   private IndexingRule getApplicableIndexingRule(NodeData state)
   {
      List<IndexingRule> rules = null;
      List<IndexingRule> r = configElements.get(state.getPrimaryTypeName());
      if (r != null)
      {
         rules = new ArrayList<IndexingRule>();
         rules.addAll(r);
      }

      InternalQName[] mixTypes = state.getMixinTypeNames();
      for (InternalQName mixType : mixTypes)
      {
         r = configElements.get(mixType);
         if (r != null)
         {
            if (rules == null)
            {
               rules = new ArrayList<IndexingRule>();
            }
            rules.addAll(r);
         }
      }

      if (rules != null)
      {
         for (IndexingRule ir : rules)
         {
            if (ir.appliesTo(state))
            {
               return ir;
            }
         }
      }

      // no applicable rule
      return null;
   }

   /**
    * Returns the namespaces declared on the <code>node</code>.
    * 
    * @param node
    *            a DOM node.
    * @return the namespaces
    */
   private Properties getNamespaces(Node node)
   {
      Properties namespaces = new Properties();
      NamedNodeMap attributes = node.getAttributes();
      for (int i = 0; i < attributes.getLength(); i++)
      {
         Attr attribute = (Attr)attributes.item(i);
         if (attribute.getName().startsWith("xmlns:"))
         {
            namespaces.setProperty(attribute.getName().substring(6), attribute.getValue());
         }
      }
      return namespaces;
   }

   /**
    * Creates property configurations defined in the <code>config</code>.
    * 
    * @param config
    *            the fulltext indexing configuration.
    * @param propConfigs
    *            will be filled with exact <code>InternalQName</code> to
    *            <code>PropertyConfig</code> mappings.
    * @param namePatterns
    *            will be filled with <code>NamePattern</code>s.
    * @throws IllegalNameException
    *             if the node type name contains illegal characters.
    * @throws RepositoryException
    */
   private void createPropertyConfigs(Node config, Map<InternalQName, PropertyConfig> propConfigs,
      List<NamePattern> namePatterns) throws IllegalNameException, RepositoryException
   {
      NodeList childNodes = config.getChildNodes();
      for (int i = 0; i < childNodes.getLength(); i++)
      {
         Node n = childNodes.item(i);
         if (n.getNodeName().equals("property"))
         {
            NamedNodeMap attributes = n.getAttributes();
            // get boost value
            float boost = 1.0f;
            Node boostAttr = attributes.getNamedItem("boost");
            if (boostAttr != null)
            {
               try
               {
                  boost = Float.parseFloat(boostAttr.getNodeValue());
               }
               catch (NumberFormatException e)
               {
                  if (LOG.isTraceEnabled())
                  {
                     LOG.trace("An exception occurred: " + e.getMessage());
                  }
               }
            }

            // get nodeScopeIndex flag
            boolean nodeScopeIndex = true;
            Node nsIndex = attributes.getNamedItem("nodeScopeIndex");
            if (nsIndex != null)
            {
               nodeScopeIndex = Boolean.valueOf(nsIndex.getNodeValue()).booleanValue();
            }

            // get isRegexp flag
            boolean isRegexp = false;
            Node regexp = attributes.getNamedItem("isRegexp");
            if (regexp != null)
            {
               isRegexp = Boolean.valueOf(regexp.getNodeValue()).booleanValue();
            }

            // get useInExcerpt flag
            boolean useInExcerpt = true;
            Node excerpt = attributes.getNamedItem("useInExcerpt");
            if (excerpt != null)
            {
               useInExcerpt = Boolean.valueOf(excerpt.getNodeValue()).booleanValue();
            }

            PropertyConfig pc = new PropertyConfig(boost, nodeScopeIndex, useInExcerpt);

            if (isRegexp)
            {
               namePatterns.add(new NamePattern(getTextContent(n), pc, resolver));
            }
            else
            {
               InternalQName propName = resolver.parseJCRName(getTextContent(n)).getInternalName();
               propConfigs.put(propName, pc);
            }
         }
      }
   }

   /**
    * Gets the condition expression from the configuration.
    * 
    * @param config
    *            the config node.
    * @return the condition expression or <code>null</code> if there is no
    *         condition set on the <code>config</code>.
    * @throws MalformedPathException
    *             if the condition string is malformed.
    * @throws IllegalNameException
    *             if a name contains illegal characters.
    * @throws RepositoryException
    */
   private PathExpression getCondition(Node config) throws IllegalNameException, RepositoryException
   {
      Node conditionAttr = config.getAttributes().getNamedItem("condition");
      if (conditionAttr == null)
      {
         return null;
      }
      String conditionString = conditionAttr.getNodeValue();
      int idx;
      int axis;
      InternalQName elementTest = null;
      InternalQName nameTest = null;
      InternalQName propertyName;
      String propertyValue;

      // parse axis
      if (conditionString.startsWith("ancestor::"))
      {
         axis = PathExpression.ANCESTOR;
         idx = "ancestor::".length();
      }
      else if (conditionString.startsWith("parent::"))
      {
         axis = PathExpression.PARENT;
         idx = "parent::".length();
      }
      else if (conditionString.startsWith("@"))
      {
         axis = PathExpression.SELF;
         idx = "@".length();
      }
      else
      {
         axis = PathExpression.CHILD;
         idx = 0;
      }

      try
      {
         if (conditionString.startsWith("element(", idx))
         {
            int colon = conditionString.indexOf(',', idx + "element(".length());
            String name = conditionString.substring(idx + "element(".length(), colon).trim();
            if (!name.equals("*"))
            {
               nameTest = resolver.parseJCRName(ISO9075.decode(name)).getInternalName();
            }
            idx = conditionString.indexOf(")/@", colon);
            String type = conditionString.substring(colon + 1, idx).trim();
            elementTest = resolver.parseJCRName(ISO9075.decode(type)).getInternalName();
            idx += ")/@".length();
         }
         else
         {
            if (axis == PathExpression.ANCESTOR || axis == PathExpression.CHILD || axis == PathExpression.PARENT)
            {
               // simple name test
               String name = conditionString.substring(idx, conditionString.indexOf('/', idx));
               if (!name.equals("*"))
               {
                  nameTest = resolver.parseJCRName(ISO9075.decode(name)).getInternalName();
               }
               idx += name.length() + "/@".length();
            }
         }

         // parse property name
         int eq = conditionString.indexOf('=', idx);
         String name = conditionString.substring(idx, eq).trim();
         propertyName = resolver.parseJCRName(ISO9075.decode(name)).getInternalName();

         // parse string value
         int quote = conditionString.indexOf('\'', eq) + 1;
         propertyValue = conditionString.substring(quote, conditionString.indexOf('\'', quote));
      }
      catch (IndexOutOfBoundsException e)
      {
         throw new RepositoryException(conditionString);
      }

      return new PathExpression(axis, elementTest, nameTest, propertyName, propertyValue);
   }

   /**
    * @param node
    *            a node.
    * @return the text content of the <code>node</code>.
    */
   private static String getTextContent(Node node)
   {
      StringBuilder content = new StringBuilder();
      NodeList nodes = node.getChildNodes();
      for (int i = 0; i < nodes.getLength(); i++)
      {
         Node n = nodes.item(i);
         if (n.getNodeType() == Node.TEXT_NODE)
         {
            content.append(((CharacterData)n).getData());
         }
      }
      return content.toString();
   }

   /**
    * A property name pattern.
    */
   private static final class NamePattern
   {

      /**
       * The pattern to match.
       */
      private final Pattern pattern;

      /**
       * The associated configuration.
       */
      private final PropertyConfig config;

      /**
       * Creates a new name pattern.
       * 
       * @param pattern
       *            the pattern as read from the configuration file.
       * @param config
       *            the associated configuration.
       * @param resolver
       *            a namespace resolver for parsing name from the
       *            configuration.
       * @throws IllegalNameException
       *             if the prefix of the name pattern is illegal.
       * @throws RepositoryException
       */
      private NamePattern(String pattern, PropertyConfig config, LocationFactory resolver) throws IllegalNameException,
         RepositoryException
      {
         String uri = Constants.NS_DEFAULT_URI;
         String localPattern = pattern;
         int idx = pattern.indexOf(':');
         if (idx != -1)
         {
            String prefix = pattern.substring(0, idx);
            if (prefix.equals(".*"))
            {
               // match all namespaces
               uri = prefix;
            }
            else
            {
               // use a dummy local name to get namespace uri
               uri = resolver.parseJCRName(pattern.substring(0, idx) + ":a").getNamespace();
            }
            localPattern = pattern.substring(idx + 1);
         }
         else if (pattern.equals(".*"))
         {
            uri = ".*";
         }
         this.pattern = Pattern.name(uri, localPattern);
         this.config = config;
      }

      /**
       * @param path
       *            the path to match.
       * @return <code>true</code> if <code>path</code> matches this name
       *         pattern; <code>false</code> otherwise.
       */
      boolean matches(QPath path)
      {
         return pattern.match(path).isFullMatch();
      }

      /**
       * @return the property configuration for this name pattern.
       */
      PropertyConfig getConfig()
      {
         return config;
      }
   }

   private class IndexingRule
   {

      /**
       * The node type of this fulltext indexing rule.
       */
      private final InternalQName nodeTypeName;

      /**
       * Map of {@link PropertyConfig}. Key=InternalQName of property.
       */
      private final Map<InternalQName, PropertyConfig> propConfigs;

      /**
       * List of {@link NamePattern}s.
       */
      private final List<NamePattern> namePatterns;

      /**
       * An expression based on a relative path.
       */
      private final PathExpression condition;

      /**
       * The boost value for this config element.
       */
      private final float boost;

      /**
       * Creates a new indexing rule base on an existing one, but for a
       * different node type name.
       * 
       * @param original
       *            the existing rule.
       * @param nodeTypeName
       *            the node type name for the rule.
       */
      IndexingRule(IndexingRule original, InternalQName nodeTypeName)
      {
         this.nodeTypeName = nodeTypeName;
         this.propConfigs = original.propConfigs;
         this.namePatterns = original.namePatterns;
         this.condition = original.condition;
         this.boost = original.boost;
      }

      /**
       * 
       * @param config
       *            the configuration for this rule.
       * @throws MalformedPathException
       *             if the condition expression is malformed.
       * @throws IllegalNameException
       *             if a name contains illegal characters.
       * @throws RepositoryException
       */
      IndexingRule(Node config) throws IllegalNameException, RepositoryException
      {
         this.nodeTypeName = getNodeTypeName(config);
         this.condition = getCondition(config);
         this.boost = getNodeBoost(config);
         this.propConfigs = new HashMap<InternalQName, PropertyConfig>();
         this.namePatterns = new ArrayList<NamePattern>();
         createPropertyConfigs(config, propConfigs, namePatterns);
      }

      /**
       * Returns the name of the node type where this rule applies to.
       * 
       * @return name of the node type.
       */
      public InternalQName getNodeTypeName()
      {
         return nodeTypeName;
      }

      /**
       * @return the value for the node boost.
       */
      public float getNodeBoost()
      {
         return boost;
      }

      /**
       * Returns <code>true</code> if the property with the given name is
       * indexed according to this rule.
       * 
       * @param propertyName
       *            the name of a property.
       * @return <code>true</code> if the property is indexed;
       *         <code>false</code> otherwise.
       */
      public boolean isIndexed(InternalQName propertyName)
      {
         return getConfig(propertyName) != null;
      }

      /**
       * Returns the boost value for the given property name. If there is no
       * configuration entry for the property name the default boost value is
       * returned.
       * 
       * @param propertyName
       *            the name of a property.
       * @return the boost value for the property.
       */
      public float getBoost(InternalQName propertyName)
      {
         PropertyConfig config = getConfig(propertyName);
         if (config != null)
         {
            return config.boost;
         }
         else
         {
            return DEFAULT_BOOST;
         }
      }

      /**
       * Returns <code>true</code> if the property with the given name should
       * be included in the node scope fulltext index. If there is no
       * configuration entry for that propery <code>false</code> is returned.
       * 
       * @param propertyName
       *            the name of a property.
       * @return <code>true</code> if the property should be included in the
       *         node scope fulltext index.
       */
      public boolean isIncludedInNodeScopeIndex(InternalQName propertyName)
      {
         PropertyConfig config = getConfig(propertyName);
         if (config != null)
         {
            return config.nodeScopeIndex;
         }
         else
         {
            return false;
         }
      }

      /**
       * Returns <code>true</code> if the content of the property with the
       * given name should show up in an excerpt. If there is no configuration
       * entry for that property <code>true</code> is returned.
       * 
       * @param propertyName
       *            the name of a property.
       * @return <code>true</code> if the content of the property should be
       *         included in an excerpt; <code>false</code> otherwise.
       */
      public boolean useInExcerpt(InternalQName propertyName)
      {
         PropertyConfig config = getConfig(propertyName);
         if (config != null)
         {
            return config.useInExcerpt;
         }
         else
         {
            return true;
         }
      }

      /**
       * Returns <code>true</code> if this rule applies to the given node
       * <code>state</code>.
       * 
       * @param state
       *            the state to check.
       * @return <code>true</code> the rule applies to the given node;
       *         <code>false</code> otherwise.
       */
      public boolean appliesTo(NodeData state)
      {
         if (!nodeTypeName.equals(state.getPrimaryTypeName()))
         {
            return false;
         }
         if (condition == null)
         {
            return true;
         }
         else
         {
            return condition.evaluate(state);
         }
      }

      // -------------------------< internal
      // >---------------------------------

      /**
       * @param propertyName
       *            name of a property.
       * @return the property configuration or <code>null</code> if this
       *         indexing rule does not contain a configuration for the given
       *         property.
       */
      private PropertyConfig getConfig(InternalQName propertyName)
      {
         PropertyConfig config = propConfigs.get(propertyName);
         if (config != null)
         {
            return config;
         }
         else if (namePatterns.size() > 0)
         {
            QPath path = new QPath(new QPathEntry[]{new QPathEntry(propertyName, 1)});
            // check patterns
            for (Iterator<NamePattern> it = namePatterns.iterator(); it.hasNext();)
            {
               NamePattern np = it.next();
               if (np.matches(path))
               {
                  return np.getConfig();
               }
            }
         }
         return null;
      }

      /**
       * Reads the node type of the root node of the indexing rule.
       * 
       * @param config
       *            the configuration.
       * @return the name of the node type.
       * @throws IllegalNameException
       *             if the node type name contains illegal characters.
       * @throws RepositoryException
       */
      private InternalQName getNodeTypeName(Node config) throws IllegalNameException, RepositoryException
      {
         String ntString = config.getAttributes().getNamedItem("nodeType").getNodeValue();
         return resolver.parseJCRName(ntString).getInternalName();
      }

      /**
       * Returns the node boost from the <code>config</code>.
       * 
       * @param config
       *            the configuration.
       * @return the configured node boost or the default boost if none is
       *         configured.
       */
      private float getNodeBoost(Node config)
      {
         Node boost = config.getAttributes().getNamedItem("boost");
         if (boost != null)
         {
            try
            {
               return Float.parseFloat(boost.getNodeValue());
            }
            catch (NumberFormatException e)
            {
               if (LOG.isTraceEnabled())
               {
                  LOG.trace("An exception occurred: " + e.getMessage());
               }
            }
         }
         return DEFAULT_BOOST;
      }
   }

   /**
    * Simple class that holds boost and nodeScopeIndex flag.
    */
   private class PropertyConfig
   {

      /**
       * The boost value for a property.
       */
      final float boost;

      /**
       * Flag that indicates whether a property is included in the node scope
       * fulltext index of its parent.
       */
      final boolean nodeScopeIndex;

      /**
       * Flag that indicates whether the content of a property should be used
       * to create an excerpt.
       */
      final boolean useInExcerpt;

      PropertyConfig(float boost, boolean nodeScopeIndex, boolean useInExcerpt)
      {
         this.boost = boost;
         this.nodeScopeIndex = nodeScopeIndex;
         this.useInExcerpt = useInExcerpt;
      }
   }

   private class PathExpression
   {

      static final int SELF = 0;

      static final int CHILD = 1;

      static final int ANCESTOR = 2;

      static final int PARENT = 3;

      private final int axis;

      private final InternalQName elementTest;

      private final InternalQName nameTest;

      private final InternalQName propertyName;

      private final String propertyValue;

      PathExpression(int axis, InternalQName elementTest, InternalQName nameTest, InternalQName propertyName,
         String propertyValue)
      {
         this.axis = axis;
         this.elementTest = elementTest;
         this.nameTest = nameTest;
         this.propertyName = propertyName;
         this.propertyValue = propertyValue;
      }

      /**
       * Evaluates this expression and returns <code>true</code> if the
       * condition matches using <code>state</code> as the context node state.
       * 
       * @param context
       *            the context from where the expression should be evaluated.
       * @return expression result.
       */
      boolean evaluate(final NodeData context)
      {
         // get iterator along specified axis
         Iterator<NodeData> nodeStates;
         if (axis == SELF)
         {
            nodeStates = Collections.singletonList(context).iterator();
         }
         else if (axis == CHILD)
         {
            List<NodeData> childs;
            try
            {
               childs = ism.getChildNodesData(context);
               nodeStates = childs.iterator();
            }
            catch (RepositoryException e)
            {
               nodeStates = Collections.<NodeData> emptyList().iterator();
            }
         }
         else if (axis == ANCESTOR)
         {
            try
            {
               nodeStates = new Iterator<NodeData>()
               {

                  private NodeData next = (NodeData)ism.getItemData(context.getParentIdentifier());

                  public void remove()
                  {
                     throw new UnsupportedOperationException();
                  }

                  public boolean hasNext()
                  {
                     return next != null;
                  }

                  public NodeData next()
                  {
                     NodeData tmp = next;
                     try
                     {
                        if (next.getParentIdentifier() != null)
                        {
                           next = (NodeData)ism.getItemData(next.getParentIdentifier());
                        }
                        else
                        {
                           next = null;
                        }
                     }
                     catch (RepositoryException e)
                     {
                        next = null;
                     }
                     return tmp;
                  }
               };
            }
            catch (RepositoryException e)
            {
               nodeStates = Collections.<NodeData>emptyList().iterator();
            }
         }
         else if (axis == PARENT)
         {
            try
            {
               if (context.getParentIdentifier() != null)
               {
                  NodeData state = (NodeData)ism.getItemData(context.getParentIdentifier());
                  nodeStates = Collections.singletonList(state).iterator();
               }
               else
               {
                  nodeStates = Collections.<NodeData>emptyList().iterator();
               }
            }
            catch (RepositoryException e)
            {
               nodeStates = Collections.<NodeData>emptyList().iterator();
            }
         }
         else
         {
            // unsupported axis
            nodeStates = Collections.<NodeData>emptyList().iterator();
         }

         // check node type, name and property value for each
         while (nodeStates.hasNext())
         {
            try
            {
               NodeData current = nodeStates.next();
               if ((elementTest != null) && !current.getPrimaryTypeName().equals(elementTest))
               {
                  continue;
               }
               if ((nameTest != null) && !current.getQPath().getName().equals(nameTest))
               {
                  continue;
               }

               List<PropertyData> childProps = ism.getChildPropertiesData(current);

               PropertyData propState = null;
               for (PropertyData propertyData : childProps)
               {
                  if (propertyData.getQPath().getName().equals(propertyName))
                  {
                     propState = propertyData;
                     break;
                  }

               }
               if (propState == null)
               {
                  continue;
               }

               List<ValueData> values = propState.getValues();

               // if (values.get(i).toString().equals(propertyValue)) {
               // return true;
               // }

               if (propState.getType() == PropertyType.BINARY)
               {
                  // skip binary values
                  continue;
               }

               for (int i = 0; i < values.size(); i++)
               {
                  String val = ValueDataUtil.getString(values.get(i));
                  if (val.equals(propertyValue))
                  {
                     return true;
                  }
               }
            }
            catch (RepositoryException e)
            {
               LOG.error(e.getLocalizedMessage());
            }
         }
         return false;
      }

   }

   public Analyzer addPropertyAnalyzer(String propertyName, Analyzer analyzer)
   {
      return analyzers.put(propertyName, analyzer);
   }
}
