/*
 * 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.jackrabbit.core.query.lucene;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;

import javax.jcr.NamespaceException;
import javax.jcr.RepositoryException;

import org.apache.commons.collections.iterators.AbstractIteratorDecorator;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.HierarchyManagerImpl;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistryListener;
import org.apache.jackrabbit.core.nodetype.xml.AdditionalNamespaceResolver;
import org.apache.jackrabbit.core.query.QueryHandlerContext;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.ItemStateManager;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.conversion.NameResolver;
import org.apache.jackrabbit.spi.commons.conversion.ParsingNameResolver;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.Pattern;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.lucene.analysis.Analyzer;
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;

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

    /**
     * The logger instance for this class
     */
    private static final Logger log = LoggerFactory.getLogger(IndexingConfigurationImpl.class);

    /**
     * The path factory instance.
     */
    private static final PathFactory PATH_FACTORY = PathFactoryImpl.getInstance();

    /**
     * An Iterator over an empty list of node states.
     */
    private static final Iterator<NodeState> EMPTY_NODESTATE_ITERATOR;
    static {
        Collection<NodeState> empty = Collections.emptyList();
        EMPTY_NODESTATE_ITERATOR = empty.iterator();
    }

    
    /**
     * The indexing configuration.
     */
    private Element configuration;

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

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

    /**
     * A hierarchy resolver for the item state manager.
     */
    private HierarchyManager hmgr;

    /**
     * The node type registry.
     */
    private NodeTypeRegistry ntReg;

    /**
     * The {@link IndexingRule}s inside this configuration.
     */
    private Map<Name, List<IndexingRule>> configElements = new HashMap<Name, 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>();

    /**
     * {@inheritDoc}
     */
    public void init(Element config,
                     QueryHandlerContext context,
                     NamespaceMappings nsMappings) throws Exception {
        configuration = config;
        ism = context.getItemStateManager();
        hmgr = new HierarchyManagerImpl(context.getRootId(), ism);
        NamespaceResolver nsResolver = new AdditionalNamespaceResolver(getNamespaces(config));
        resolver = new ParsingNameResolver(NameFactoryImpl.getInstance(), nsResolver);
        ntReg = context.getNodeTypeRegistry();
        ntReg.addListener(this);

        refreshIndexRules();
        List<AggregateRule> idxAggregates = new ArrayList<AggregateRule>();
        NodeList indexingConfigs = config.getChildNodes();
        for (int i = 0; i < indexingConfigs.getLength(); i++) {
            Node configNode = indexingConfigs.item(i);
            if (configNode.getNodeName().equals("aggregate")) {
                idxAggregates.add(new AggregateRuleImpl(
                        configNode, resolver, ism, hmgr));
            } 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")) {
                        Analyzer analyzer = JackrabbitAnalyzer.getAnalyzerInstance(
                                analyzerNode.getAttributes().getNamedItem("class").getNodeValue());
                        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
                                Name propName = resolver.getQName(getTextContent(propertyNode));
                                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.getLocalName()
                                            + " has been configured for multiple analyzers. "
                                            + " Last configured analyzer is used");
                                }
                            }
                        }
                    }
                }
            }

        }
        if (idxAggregates.isEmpty()) {
            aggregateRules = null;
        } else {
            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(NodeState state, Name propertyName) {
        IndexingRule rule = getApplicableIndexingRule(state);
        if (rule != null) {
            return rule.isIndexed(propertyName);
        }
        // none of the configs matches -> index property
        return true;
    }

    /**
     * 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(NodeState state, Name 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(NodeState 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(NodeState state,
                                              Name 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(NodeState state, Name 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>Name</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>Name</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;
    }

    //--------------------------< NodeTypeRegistryListener >--------------------

    public void nodeTypeRegistered(Name ntName) {
        try {
            refreshIndexRules();
        } catch (Exception e) {
            log.warn("Unable to refresh index rules", e);
        }
    }

    public void nodeTypeReRegistered(Name ntName) {
        // not interested
    }

    public void nodeTypesUnregistered(Collection<Name> names) {
        // not interested
    }

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

    /**
     * Refreshes the index rules in {@link #configElements} based on the current
     * node types available in the node type registry.
     *
     * @throws Exception if an error occurs while refreshing the rules.
     */
    private void refreshIndexRules() throws Exception {
        Map<Name, List<IndexingRule>> nt2rules = new HashMap<Name, List<IndexingRule>>();
        Name[] ntNames = ntReg.getRegisteredNodeTypes();
        NodeList indexingConfigs = configuration.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());
                for (Name ntName : ntNames) {
                    if (ntReg.getEffectiveNodeType(ntName).includesNodeType(element.getNodeTypeName())) {
                        List<IndexingRule> perNtConfig = nt2rules.get(ntName);
                        if (perNtConfig == null) {
                            perNtConfig = new ArrayList<IndexingRule>();
                            nt2rules.put(ntName, perNtConfig);
                        }
                        log.debug("Registering it for name '{}'", ntName);
                        perNtConfig.add(new IndexingRule(element, ntName));
                    }
                }
            }
        }
        configElements = nt2rules;
    }


    /**
     * 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(NodeState state) {
        List<IndexingRule> rules = null;
        List<IndexingRule> r = configElements.get(state.getNodeTypeName());
        if (r != null) {
            rules = new ArrayList<IndexingRule>();
            rules.addAll(r);
        }

        for (Name name : state.getMixinTypeNames()) {
            r = configElements.get(name);
            if (r != null) {
                if (rules == null) {
                    rules = new ArrayList<IndexingRule>();
                }
                rules.addAll(r);
            }
        }

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

        // 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>Name</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 NamespaceException if the node type contains an unknown
     *                                prefix.
     */
    private void createPropertyConfigs(Node config,
                                       Map<Name, PropertyConfig> propConfigs,
                                       List<NamePattern> namePatterns)
            throws IllegalNameException, NamespaceException {
        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) {
                        // use default
                    }
                }

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

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

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

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

                if (isRegexp) {
                    namePatterns.add(new NamePattern(
                            getTextContent(n), pc, resolver));
                } else {
                    Name propName = resolver.getQName(getTextContent(n));
                    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 NamespaceException if a name contains an unknown prefix.
     */
    private PathExpression getCondition(Node config)
            throws MalformedPathException, IllegalNameException, NamespaceException {
        Node conditionAttr = config.getAttributes().getNamedItem("condition");
        if (conditionAttr == null) {
            return null;
        }
        String conditionString = conditionAttr.getNodeValue();
        int idx;
        int axis;
        Name elementTest = null;
        Name nameTest = null;
        Name 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.getQName(ISO9075.decode(name));
                }
                idx = conditionString.indexOf(")/@", colon);
                String type = conditionString.substring(colon + 1, idx).trim();
                elementTest = resolver.getQName(ISO9075.decode(type));
                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.getQName(ISO9075.decode(name));
                    }
                    idx += name.length() + "/@".length();
                }
            }

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

            // parse string value
            int quote = conditionString.indexOf('\'', eq) + 1;
            propertyValue = conditionString.substring(quote,
                    conditionString.indexOf('\'', quote));
        } catch (IndexOutOfBoundsException e) {
            throw new MalformedPathException(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) {
        StringBuffer content = new StringBuffer();
        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 NamespaceException if the prefix of the name pattern cannot
         *                            be resolved.
         */
        private NamePattern(String pattern,
                            PropertyConfig config,
                            NameResolver resolver)
                throws IllegalNameException, NamespaceException {
            String uri = Name.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 {
                    // match only single namespace
                    // use a dummy local name to get namespace uri
                    uri = resolver.getQName(prefix + ":a").getNamespaceURI();
                }
                localPattern = pattern.substring(idx + 1);
            }
            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(Path 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 Name nodeTypeName;

        /**
         * Map of {@link PropertyConfig}. Key=Name of property.
         */
        private final Map<Name, 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, Name 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 NamespaceException if a name contains an unknown prefix.
         */
        IndexingRule(Node config)
                throws MalformedPathException, IllegalNameException, NamespaceException {
            this.nodeTypeName = getNodeTypeName(config);
            this.condition = getCondition(config);
            this.boost = getNodeBoost(config);
            this.propConfigs = new HashMap<Name, 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 Name 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(Name 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(Name 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(Name propertyName) {
            PropertyConfig config = getConfig(propertyName);
            return config != null && config.nodeScopeIndex;
        }

        /**
         * 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(Name propertyName) {
            PropertyConfig config = getConfig(propertyName);
            return config == null || config.useInExcerpt;
        }

        /**
         * 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(NodeState state) {
            if (!nodeTypeName.equals(state.getNodeTypeName())) {
                return false;
            }
            return condition == null || 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(Name propertyName) {
            PropertyConfig config = propConfigs.get(propertyName);
            if (config != null) {
                return config;
            } else if (namePatterns.size() > 0) {
                Path path = PATH_FACTORY.create(propertyName);
                // check patterns
                for (NamePattern np : namePatterns) {
                    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 NamespaceException if the node type contains an unknown
         *                                prefix.
         */
        private Name getNodeTypeName(Node config)
                throws IllegalNameException, NamespaceException {
            String ntString = config.getAttributes().getNamedItem("nodeType").getNodeValue();
            return resolver.getQName(ntString);
        }

        /**
         * 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) {
                    // return default boost
                }
            }
            return DEFAULT_BOOST;
        }
    }

    /**
     * Simple class that holds boost and nodeScopeIndex flag.
     */
    private static 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 Name elementTest;

        private final Name nameTest;

        private final Name propertyName;

        private final String propertyValue;

        PathExpression(int axis, Name elementTest, Name nameTest,
                       Name 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 NodeState context) {
            // get iterator along specified axis
            Iterator<NodeState> nodeStates;
            if (axis == SELF) {
                nodeStates = Collections.singletonList(context).iterator();
            } else if (axis == CHILD) {
                nodeStates = (Iterator<NodeState>) new AbstractIteratorDecorator(
                        context.getChildNodeEntries().iterator()) {
                    public Object next() {
                        ChildNodeEntry cne =
                                (ChildNodeEntry) super.next();
                        try {
                            return ism.getItemState(cne.getId());
                        } catch (ItemStateException e) {
                            NoSuchElementException nsee = new NoSuchElementException("No node with id " + cne.getId() + " found in child axis");
                            nsee.initCause(e);
                            throw nsee;
                        }
                    }
                };
            } else if (axis == ANCESTOR) {
                try {
                    nodeStates = new Iterator<NodeState>() {

                        private NodeState next = context.getParentId() == null ? null :
                                (NodeState) ism.getItemState(context.getParentId());

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

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

                        public NodeState next() {
                            NodeState tmp = next;
                            try {
                                if (next.getParentId() != null) {
                                    next = (NodeState) ism.getItemState(next.getParentId());
                                } else {
                                    next = null;
                                }
                            } catch (ItemStateException e) {
                                next = null;
                            }
                            return tmp;
                        }
                    };
                } catch (ItemStateException e) {
                    nodeStates = EMPTY_NODESTATE_ITERATOR;
                }
            } else if (axis == PARENT) {
                try {
                    if (context.getParentId() != null) {
                        NodeState state = (NodeState) ism.getItemState(context.getParentId());
                        nodeStates = Collections.singletonList(state).iterator();
                    } else {
                        nodeStates = EMPTY_NODESTATE_ITERATOR;
                    }
                } catch (ItemStateException e) {
                    nodeStates = EMPTY_NODESTATE_ITERATOR;
                }
            } else {
                // unsupported axis
                nodeStates = EMPTY_NODESTATE_ITERATOR;
            }

            // check node type, name and property value for each
            while (nodeStates.hasNext()) {
                try {
                    NodeState current = nodeStates.next();
                    if (elementTest != null
                            && !current.getNodeTypeName().equals(elementTest)) {
                        continue;
                    }
                    if (nameTest != null
                            && !hmgr.getName(current.getNodeId()).equals(nameTest)) {
                        continue;
                    }
                    if (!current.hasPropertyName(propertyName)) {
                        continue;
                    }
                    PropertyId propId = new PropertyId(
                            current.getNodeId(), propertyName);
                    PropertyState propState =
                            (PropertyState) ism.getItemState(propId);
                    InternalValue[] values = propState.getValues();
                    for (InternalValue value : values) {
                        if (value.toString().equals(propertyValue)) {
                            return true;
                        }
                    }
                } catch (RepositoryException e) {
                    // move on to next one
                } catch (ItemStateException e) {
                    // move on to next one
                }
            }
            return false;
        }
    }
}
