org.apache.jackrabbit.core.query.lucene.JahiaNodeIndexer.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jackrabbit.core.query.lucene.JahiaNodeIndexer.java

Source

/**
 * ==========================================================================================
 * =                   JAHIA'S DUAL LICENSING - IMPORTANT INFORMATION                       =
 * ==========================================================================================
 *
 *                                 http://www.jahia.com
 *
 *     Copyright (C) 2002-2017 Jahia Solutions Group SA. All rights reserved.
 *
 *     THIS FILE IS AVAILABLE UNDER TWO DIFFERENT LICENSES:
 *     1/GPL OR 2/JSEL
 *
 *     1/ GPL
 *     ==================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE GPL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     This program is free software: you can redistribute it and/or modify
 *     it under the terms of the GNU General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 *
 *     This program is distributed in the hope that it will be useful,
 *     but WITHOUT ANY WARRANTY; without even the implied warranty of
 *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 *     GNU General Public License for more details.
 *
 *     You should have received a copy of the GNU General Public License
 *     along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 *
 *     2/ JSEL - Commercial and Supported Versions of the program
 *     ===================================================================================
 *
 *     IF YOU DECIDE TO CHOOSE THE JSEL LICENSE, YOU MUST COMPLY WITH THE FOLLOWING TERMS:
 *
 *     Alternatively, commercial and supported versions of the program - also known as
 *     Enterprise Distributions - must be used in accordance with the terms and conditions
 *     contained in a separate written agreement between you and Jahia Solutions Group SA.
 *
 *     If you are unsure which license is appropriate for your use,
 *     please contact the sales department at sales@jahia.com.
 */
package org.apache.jackrabbit.core.query.lucene;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.query.QueryHandlerContext;
import org.apache.jackrabbit.core.state.*;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.value.InternalValueFactory;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.solr.schema.DateField;
import org.apache.solr.schema.SortableDoubleField;
import org.apache.solr.schema.SortableLongField;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.Parser;
import org.jahia.api.Constants;
import org.jahia.services.SpringContextSingleton;
import org.jahia.services.content.nodetypes.ExtendedNodeType;
import org.jahia.services.content.nodetypes.ExtendedPropertyDefinition;
import org.jahia.services.content.nodetypes.NodeTypeRegistry;
import org.jahia.services.content.nodetypes.SelectorType;
import org.jahia.services.textextraction.TextExtractionService;
import org.jahia.utils.LuceneUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.NamespaceRegistry;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.NoSuchNodeTypeException;

import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.concurrent.Executor;

/**
 * Creates a lucene <code>Document</code> object from a {@link javax.jcr.Node} and use Jahia sepecific definitions for index creation.
 */
public class JahiaNodeIndexer extends NodeIndexer {
    /**
     * The logger instance for this class.
     */
    protected static final Logger logger = LoggerFactory.getLogger(JahiaNodeIndexer.class);

    /**
     * Prefix for all field names that are facet indexed by property name.
     */
    public static final String FACET_PREFIX = "FACET:";

    public static final String TRANSLATED_NODE_PARENT = "_:TRANSLATED_PARENT".intern();
    public static final String TRANSLATION_LANGUAGE = "_:TRANSLATION_LANGUAGE".intern();

    public static final String ACL_UUID = "_:ACL_UUID".intern();
    public static final Name J_ACL = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS, "acl");
    public static final Name J_ACL_INHERITED = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS, "inherit");
    public static final Name J_ACE_PRINCIPAL = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS,
            "principal");
    public static final Name J_ACE_GRANT = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS, "aceType");
    public static final Name J_ACE_ROLES = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS, "roles");

    public static final String CHECK_VISIBILITY = "_:CHECK_VISIBILITY".intern();

    private static final Name J_EXTRACTED_TEXT = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS,
            "extractedText");

    public static final Name J_VISIBILITY = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS,
            "conditionalVisibility");

    public static final String PUBLISHED = "_:PUBLISHED".intern();
    public static final Name J_PUBLISHED = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS, "published");

    public static final String FACET_HIERARCHY = "_:FACET_HIERARCHY".intern();

    public static final Name J_INVALID_LANGUAGES = NameFactoryImpl.getInstance().create(Constants.JAHIA_NS,
            "invalidLanguages");
    public static final String INVALID_LANGUAGES = "_:INVALID_LANGUAGES".intern();
    /**
     * The persistent node type registry
     */
    protected final NodeTypeRegistry nodeTypeRegistry;

    /**
     * The persistent namespace registry
     */
    protected final NamespaceRegistry namespaceRegistry;

    /**
     * The hierarchy manager
     */
    protected final HierarchyManager hierarchyMgr;

    /**
     * The <code>ExtendedNodeType</code> of the node to index, lazily resolved from its type name
     */
    private ExtendedNodeType nodeType;
    /**
     * The associated node's type name.
     */
    private final Name nodeTypeName;

    /**
     * If set to <code>true</code> the fulltext field is also stored with site/locale suffix
     */
    protected boolean supportSpellchecking = false;

    private static Name siteTypeName = null;

    private static Name siteFolderTypeName = null;

    private static final DateField dateType = new DateField();
    private static final SortableDoubleField doubleType = new SortableDoubleField();
    private static final SortableLongField longType = new SortableLongField();

    private boolean addAclUuidInIndex = true;

    private boolean useOptimizedACEIndexation = false;

    private transient String site;

    private transient Map<String, ExtendedPropertyDefinition> fieldNameToPropDef = new HashMap<String, ExtendedPropertyDefinition>(
            17);

    protected JahiaNodeIndexer(NodeState node, ItemStateManager stateProvider, NamespaceMappings mappings,
            Executor executor, Parser parser, QueryHandlerContext context, NodeTypeRegistry typeRegistry,
            NamespaceRegistry nameRegistry) {
        super(node, stateProvider, mappings, executor, parser);
        this.nodeTypeRegistry = typeRegistry;
        this.namespaceRegistry = nameRegistry;
        this.hierarchyMgr = context.getHierarchyManager();
        this.nodeTypeName = node.getNodeTypeName();

        try {
            if (siteTypeName == null && nodeTypeRegistry != null) {
                ExtendedNodeType siteNodeType = nodeTypeRegistry.getNodeType(Constants.JAHIANT_VIRTUALSITE);
                if (siteNodeType != null) {
                    siteTypeName = NameFactoryImpl.getInstance().create(siteNodeType.getNameObject().getUri(),
                            siteNodeType.getLocalName());
                    siteNodeType = nodeTypeRegistry.getNodeType(Constants.JAHIANT_VIRTUALSITES_FOLDER);
                    siteFolderTypeName = NameFactoryImpl.getInstance().create(siteNodeType.getNameObject().getUri(),
                            siteNodeType.getLocalName());
                }
            }
        } catch (NoSuchNodeTypeException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(e.getMessage(), e);
            }
        }
    }

    protected String getTypeNameAsString() throws RepositoryException {
        return getTypeNameAsString(nodeTypeName, namespaceRegistry);
    }

    /**
     * Convert name object to jcr-name string
     * @param nodeTypeName the name
     * @param namespaceRegistry the namespace registry
     * @return
     * @throws RepositoryException
     */
    protected static String getTypeNameAsString(Name nodeTypeName, NamespaceRegistry namespaceRegistry)
            throws RepositoryException {
        return namespaceRegistry.getPrefix(nodeTypeName.getNamespaceURI()) + ":" + nodeTypeName.getLocalName();
    }

    /**
     * Returns <code>true</code> if the content of the property with the given name should the used to create an excerpt.
     *
     * @param propertyName the name of a property.
     * @return <code>true</code> if it should be used to create an excerpt; <code>false</code> otherwise.
     */
    protected boolean useInExcerpt(Name propertyName) {
        boolean useInExcerpt = super.useInExcerpt(propertyName);
        if (useInExcerpt) {
            ExtendedPropertyDefinition propDef = getExtendedPropertyDefinition(getPropertyName(propertyName));
            useInExcerpt = propDef == null || propDef.isFullTextSearchable();
        }
        return useInExcerpt;
    }

    protected String getPropertyName(Name name) {
        StringBuilder propertyNameBuilder = new StringBuilder();

        try {
            propertyNameBuilder.append(namespaceRegistry.getPrefix(name.getNamespaceURI()));
        } catch (RepositoryException e) {
            if (logger.isDebugEnabled()) {
                logger.debug("Cannot get namespace prefix for: " + name.getNamespaceURI(), e);
            }
        }

        if (propertyNameBuilder.length() > 0) {
            propertyNameBuilder.append(":");
        }
        propertyNameBuilder.append(name.getLocalName());
        return propertyNameBuilder.toString();
    }

    protected String resolveSite() {
        if (site == null) {
            try {
                NodeState current = node;
                do {
                    if (isNodeType(current, siteTypeName)) {
                        NodeState siteParent = (NodeState) stateProvider.getItemState(current.getParentId());
                        if (isNodeType(siteParent, siteFolderTypeName)) {
                            site = siteParent.getChildNodeEntry(current.getNodeId()).getName().getLocalName();
                            break;
                        }
                    }
                    NodeId id = current.getParentId();
                    if (id != null) {
                        current = (NodeState) stateProvider.getItemState(id);
                    } else {
                        current = null;
                    }
                } while (current != null);
            } catch (RepositoryException e) {
            } catch (NoSuchItemStateException e) {
            } catch (ItemStateException e) {
            }
        }

        return site;
    }

    private boolean isNodeType(NodeState nodeState, Name typeName) throws RepositoryException {
        if (typeName != null) {
            Name primary = nodeState.getNodeTypeName();
            if (primary.equals(typeName)) {
                return true;
            }
            Set<Name> mixins = nodeState.getMixinTypeNames();
            if (mixins.contains(typeName)) {
                return true;
            }
        }
        return false;
    }

    protected ExtendedNodeType getNodeType() {
        if (nodeType == null) {
            try {
                nodeType = nodeTypeRegistry.getNodeType(getTypeNameAsString());
            } catch (RepositoryException e) {
                logger.error("Couldn't resolve type: " + nodeTypeName.getNamespaceURI() + ":"
                        + nodeTypeName.getLocalName());
            }
        }

        return nodeType;
    }

    protected ExtendedPropertyDefinition getExtendedPropertyDefinition(String fieldName) {
        ExtendedPropertyDefinition propDef = fieldNameToPropDef.get(fieldName);
        if (propDef == null) {
            try {
                propDef = getPropertyDefinition(fieldName);
            } catch (Exception e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Error finding property associated with field named " + fieldName, e);
                }
            }

            if (propDef != null) {
                fieldNameToPropDef.put(fieldName, propDef);
            }
        }
        return propDef;
    }

    protected ExtendedPropertyDefinition getPropertyDefinition(String fieldName)
            throws RepositoryException, ItemStateException {
        return getPropertyDefinitionFor(fieldName, getNodeType(), node);
    }

    protected ExtendedPropertyDefinition getPropertyDefinitionFor(String fieldName, ExtendedNodeType nodeType,
            NodeState node) throws RepositoryException {
        ExtendedPropertyDefinition propDef = nodeType.getPropertyDefinitionsAsMap().get(fieldName);
        if (propDef == null) {
            for (Name mixinTypeName : node.getMixinTypeNames()) {
                ExtendedNodeType mixinType = nodeTypeRegistry
                        .getNodeType(getTypeNameAsString(mixinTypeName, namespaceRegistry));
                propDef = mixinType.getPropertyDefinitionsAsMap().get(fieldName);
                if (propDef != null) {
                    break;
                }
            }
        }
        return propDef;
    }

    /**
     * Returns the boost value for the given property name.
     *
     * @param propertyName the name of a property.
     * @return the boost value for the given property name.
     */
    protected float getPropertyBoost(Name propertyName) {
        float scoreBoost = super.getPropertyBoost(propertyName);
        if (Float.compare(scoreBoost, 1.0F) == 0) {
            ExtendedPropertyDefinition propDef = getExtendedPropertyDefinition(getPropertyName(propertyName));
            if (propDef != null) {
                scoreBoost = (float) propDef.getScoreboost();
            }
        }
        return scoreBoost;
    }

    /**
     * Returns <code>true</code> if the property with the given name should also be added to the node scope index.
     *
     * @param propertyName the name of a property.
     * @return <code>true</code> if it should be added to the node scope index; <code>false</code> otherwise.
     */
    protected boolean isIncludedInNodeIndex(Name propertyName) {
        boolean isIncludedInNodeIndex = super.isIncludedInNodeIndex(propertyName);
        if (isIncludedInNodeIndex) {
            ExtendedPropertyDefinition propDef = getExtendedPropertyDefinition(getPropertyName(propertyName));
            isIncludedInNodeIndex = propDef == null || propDef.isFullTextSearchable();
        }
        return isIncludedInNodeIndex;
    }

    /**
     * Returns <code>true</code> if the property with the given name should be indexed.
     *
     * @param propertyName name of a property.
     * @return <code>true</code> if the property should be fulltext indexed; <code>false</code> otherwise.
     */
    protected boolean isIndexed(Name propertyName) {
        boolean isIndexed = super.isIndexed(propertyName);
        if (isIndexed) {
            ExtendedPropertyDefinition propDef = getExtendedPropertyDefinition(getPropertyName(propertyName));
            isIndexed = propDef == null || propDef.getIndex() != ExtendedPropertyDefinition.INDEXED_NO;
        }
        return isIndexed;
    }

    /**
     * Adds the string value to the document both as the named field and optionally for full text indexing if <code>tokenized</code> is
     * <code>true</code>.
     * <p/>
     * The Jahia specific functionality is to strip off HTML markup from richtext fields.
     *
     * @param doc                The document to which to add the field
     * @param fieldName          The name of the field to add
     * @param internalValue      The value for the field to add to the document.
     * @param tokenized          If <code>true</code> the string is also tokenized and fulltext indexed.
     * @param includeInNodeIndex If <code>true</code> the string is also tokenized and added to the node scope fulltext index.
     * @param boost              the boost value for this string field.
     * @param useInExcerpt       If <code>true</code> the string may show up in an excerpt.
     */
    @Override
    protected void addStringValue(Document doc, String fieldName, String internalValue, boolean tokenized,
            boolean includeInNodeIndex, float boost, boolean useInExcerpt) {

        final String propertyName = getPropertyNameFromFieldname(fieldName);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(propertyName);

        if (definition != null && SelectorType.RICHTEXT == definition.getSelector()) {
            try {
                Metadata metadata = new Metadata();
                metadata.set(Metadata.CONTENT_TYPE, "text/html");
                metadata.set(Metadata.CONTENT_ENCODING, InternalValueFactory.DEFAULT_ENCODING);

                TextExtractionService textExtractor = (TextExtractionService) SpringContextSingleton
                        .getBean("org.jahia.services.textextraction.TextExtractionService");
                internalValue = textExtractor.parse(
                        new ByteArrayInputStream(internalValue.getBytes(InternalValueFactory.DEFAULT_ENCODING)),
                        metadata);
            } catch (Exception e) {
                internalValue = StringEscapeUtils.unescapeHtml(internalValue);
            }
        }
        if (internalValue == null) {
            return;
        }
        super.addStringValue(doc, fieldName, internalValue, tokenized, includeInNodeIndex, boost, useInExcerpt);
        if (tokenized) {
            if (internalValue.isEmpty()) {
                return;
            }

            if (includeInNodeIndex && isSupportSpellchecking()
                    && getIndexingConfig().shouldPropertyBeSpellchecked(propertyName)) {
                String site = resolveSite();
                if (site != null) {
                    doc.add(createFulltextField(getFullTextFieldName(site), internalValue, false));
                }
            }
        }
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, internalValue);
        }
    }

    protected String getFullTextFieldName(String site) {
        return LuceneUtils.getFullTextFieldName(site, null);
    }

    private String getPropertyNameFromFieldname(String fieldName) {
        String propertyName = fieldName;
        int pos = fieldName.indexOf(':');
        if (pos > -1) {
            try {
                String prefix = namespaceRegistry.getPrefix(mappings.getURI(fieldName.substring(0, pos)));
                propertyName = !StringUtils.isEmpty(prefix) ? (prefix + fieldName.substring(pos))
                        : fieldName.substring(pos + 1);
            } catch (RepositoryException e) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Cannot convert Lucene fieldName '" + fieldName + "' to property name", e);
                }
            }
        }
        return propertyName;
    }

    /**
     * Adds the value to the document both as faceted field which will be indexed with a keyword analyzer which does not modify the term.
     *
     * @param doc           The document to which to add the field
     * @param fieldName     The name of the field to add
     * @param internalValue The value for the field to add to the document.
     */
    protected void addFacetValue(Document doc, String fieldName, Object internalValue) {
        // simple String
        String stringValue = internalValue.toString();
        if (stringValue.length() == 0) {
            return;
        }
        // create facet index on property
        int idx = fieldName.indexOf(':');
        fieldName = fieldName.substring(0, idx + 1) + FACET_PREFIX + fieldName.substring(idx + 1);
        Field f = new Field(fieldName, true, stringValue, Field.Store.NO, Field.Index.ANALYZED,
                Field.TermVector.NO);
        doc.add(f);

    }

    /**
     * Adds the value to the document both as faceted field which will be indexed with a keyword analyzer which does not modify the term.
     *
     * @param doc           The document to which to add the field
     * @param fieldName     The name of the field to add
     * @param internalValue The value for the field to add to the document.
     */
    protected void addHierarchicalFacetValue(Document doc, String fieldName, Object internalValue) {
        final String stringValue = (String) internalValue.toString();
        if (stringValue.length() == 0) {
            return;
        }
        ItemId itemId = (ItemId) internalValue;
        int idx = fieldName.indexOf(':');
        fieldName = fieldName.substring(0, idx + 1) + FACET_PREFIX + fieldName.substring(idx + 1);

        final List<String> hierarchyPaths = new ArrayList<String>();
        final List<String> parentIds = new ArrayList<String>();
        try {
            NodeState node = (NodeState) stateProvider.getItemState(itemId);
            Name typeName = node.getNodeTypeName();
            boolean isCategoryType = typeName.toString().equals("{" + Constants.JAHIANT_NS + "}" + "category");
            NodeState parent = (NodeState) stateProvider.getItemState(node.getParentId());

            while (typeName.equals(parent.getNodeTypeName())) {
                hierarchyPaths
                        .add(StringUtils.remove(resolver.getJCRPath(hierarchyMgr.getPath(node.getNodeId())), "0:"));
                parentIds.add(node.getNodeId().toString());
                node = parent;
                parent = (NodeState) stateProvider.getItemState(node.getParentId());
            }
            String jcrPath = resolver.getJCRPath(hierarchyMgr.getPath(node.getNodeId()));
            // we stop either at the root (/) or in case of categories also at /sites/systemsite 
            while (!"/".equals(jcrPath) && (!isCategoryType || !("0:/0:sites/0:systemsite").equals(jcrPath))) {
                parentIds.add(node.getNodeId().toString());
                node = (NodeState) stateProvider.getItemState(node.getParentId());
                jcrPath = resolver.getJCRPath(hierarchyMgr.getPath(node.getNodeId()));
            }
        } catch (NoSuchItemStateException e) {
            if (logger.isDebugEnabled()) {
                logger.debug(e.getMessage(), e);
            }
        } catch (ItemStateException e) {
            logger.error(e.getMessage(), e);
        } catch (RepositoryException e) {
            logger.error(e.getMessage(), e);
        }

        int hierarchyIndex = hierarchyPaths.size();
        for (String path : hierarchyPaths) {
            doc.add(new Field(fieldName, true, hierarchyIndex + path, Field.Store.NO, Field.Index.ANALYZED,
                    Field.TermVector.NO));
            hierarchyIndex--;
        }
        for (String id : parentIds) {
            doc.add(new Field(FACET_HIERARCHY, false, id, Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS,
                    Field.TermVector.NO));
        }
    }

    /**
     * Creates a fulltext field for the string <code>value</code>.
     *
     * @param value the string value.
     * @param store if the value of the field should be stored.
     * @return a lucene field.
     */
    protected Field createFulltextField(String fieldName, String value, boolean store) {
        if (store) {
            // We would be able to store the field compressed or not depending
            // on a criterion but then we could not determine later is this field
            // has been compressed or not, so we choose to store it uncompressed
            return new Field(fieldName, true, value, Field.Store.YES, Field.Index.ANALYZED, Field.TermVector.NO);
        } else {
            return new Field(fieldName, true, value, Field.Store.NO, Field.Index.ANALYZED, Field.TermVector.NO);
        }
    }

    public boolean isSupportSpellchecking() {
        return supportSpellchecking;
    }

    public void setSupportSpellchecking(boolean supportSpellchecking) {
        this.supportSpellchecking = supportSpellchecking;
    }

    @Override
    protected void addCalendarValue(Document doc, String fieldName, Calendar internalValue) {
        super.addCalendarValue(doc, fieldName, internalValue);
        Calendar value = (Calendar) internalValue;
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, dateType.toInternal(new Date(value.getTimeInMillis())));
        }
    }

    @Override
    protected void addBinaryValue(Document doc, String fieldName, InternalValue internalValue) {
        // we disable the binary indexing by Jackrabbit and only index our j:extractedText property
        try {
            String propName = mappings.getPrefix(Constants.JAHIA_NS) + ":extractedText";
            if (!propName.equals(fieldName)) {
                return;
            }
            long timer = System.currentTimeMillis();
            String value = internalValue.getString();
            addStringValue(doc, fieldName, value, true, isIncludedInNodeIndex(J_EXTRACTED_TEXT),
                    getPropertyBoost(J_EXTRACTED_TEXT), useInExcerpt(J_EXTRACTED_TEXT));
            if (logger.isDebugEnabled()) {
                logger.debug("Indexed j:extractedText of length {} in {} ms", value.length(),
                        System.currentTimeMillis() - timer);
            }
        } catch (Exception e) {
            if (logger.isDebugEnabled()) {
                logger.warn("Error indexing content of the j:extractedText property", e);
            } else {
                logger.warn("Error indexing content of the j:extractedText property. Cause: " + e.getMessage());
            }
        }
    }

    @Override
    protected void addBooleanValue(Document doc, String fieldName, Object internalValue) {
        super.addBooleanValue(doc, fieldName, internalValue);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, internalValue.toString());
        }
    }

    @Override
    protected void addDoubleValue(Document doc, String fieldName, double internalValue) {
        super.addDoubleValue(doc, fieldName, internalValue);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, doubleType.toInternal(Double.toString(internalValue)));
        }
    }

    @Override
    protected void addLongValue(Document doc, String fieldName, long internalValue) {
        super.addLongValue(doc, fieldName, internalValue);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, longType.toInternal(Long.toString(internalValue)));
        }
    }

    @Override
    protected void addReferenceValue(Document doc, String fieldName, NodeId internalValue, boolean weak) {
        super.addReferenceValue(doc, fieldName, internalValue, weak);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            if (definition.isHierarchical()) {
                addHierarchicalFacetValue(doc, fieldName, internalValue);
            } else {
                addFacetValue(doc, fieldName, internalValue);
            }
        }
    }

    @Override
    protected void addNameValue(Document doc, String fieldName, Name internalValue) {
        super.addNameValue(doc, fieldName, internalValue);
        ExtendedPropertyDefinition definition = getExtendedPropertyDefinition(
                getPropertyNameFromFieldname(fieldName));
        if (definition != null && definition.isFacetable()) {
            addFacetValue(doc, fieldName, ((Name) internalValue).getNamespaceURI());
        }
    }

    @Override
    public Document createDoc() throws RepositoryException {
        // Clean up nodestate before starting indexing, as ISM cache may contain removed entries
        cleanupNodeProperties();

        Document doc = super.createDoc();
        if (isAddAclUuidInIndex() && isIndexed(J_ACL)) {
            addAclUuid(doc);
        }
        if (isIndexed(J_VISIBILITY) && node.hasChildNodeEntry(J_VISIBILITY)) {
            doc.add(new Field(CHECK_VISIBILITY, false, "1", Field.Store.YES, Field.Index.NOT_ANALYZED_NO_NORMS,
                    Field.TermVector.NO));
        }
        if (isIndexed(J_PUBLISHED) && node.getPropertyNames().contains(J_PUBLISHED)) {
            PropertyId id = new PropertyId(node.getNodeId(), J_PUBLISHED);
            try {
                PropertyState propState = (PropertyState) stateProvider.getItemState(id);

                doc.add(new Field(PUBLISHED, false, propState.getValues()[0].getString(), Field.Store.YES,
                        Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO));
            } catch (ItemStateException e) {
                logger.error(e.getMessage(), e);
            }
        }

        if (isIndexed(J_INVALID_LANGUAGES) && node.getPropertyNames().contains(J_INVALID_LANGUAGES)) {
            PropertyId id = new PropertyId(node.getNodeId(), J_INVALID_LANGUAGES);
            try {
                PropertyState propState = (PropertyState) stateProvider.getItemState(id);

                final InternalValue[] values = propState.getValues();
                for (InternalValue value : values) {
                    doc.add(new Field(INVALID_LANGUAGES, false, value.getString(), Field.Store.YES,
                            Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO));
                }
            } catch (ItemStateException e) {
                logger.error(e.getMessage(), e);
            }
        }

        return doc;
    }

    private void cleanupNodeProperties() {
        Set<Name> props = node.getPropertyNames();
        Set<Name> toRemove = null;
        NodeId nodeId = node.getNodeId();
        for (Name propName : props) {
            try {
                if (!stateProvider.hasItemState(new PropertyId(nodeId, propName))) {
                    if (toRemove == null) {
                        toRemove = new HashSet<>();
                    }
                    toRemove.add(propName);
                }
            } catch (Exception e) {
                //
            }
        }
        if (toRemove != null) {
            for (Name name : toRemove) {
                logger.debug("Removed non-existing property {} from {}", name, nodeId);
                node.removePropertyName(name);
            }
        }
    }

    protected JahiaIndexingConfigurationImpl getIndexingConfig() {
        return (JahiaIndexingConfigurationImpl) indexingConfig;
    }

    protected void addAclUuid(Document doc) throws RepositoryException {
        List<String> acls = new ArrayList<String>();
        try {
            NodeState currentNode = node;
            while (currentNode != null) {
                ChildNodeEntry aclChildNode = currentNode.getChildNodeEntry(J_ACL, 1);
                if (aclChildNode != null) {
                    NodeState ns = (NodeState) stateProvider.getItemState(aclChildNode.getId());
                    StringBuilder ace = new StringBuilder(currentNode.getId().toString());
                    if (ns.getChildNodeEntries().size() == 1 && useOptimizedACEIndexation) {
                        ChildNodeEntry childNodeEntry = ns.getChildNodeEntries().get(0);
                        PropertyId principalPropId = new PropertyId(childNodeEntry.getId(), J_ACE_PRINCIPAL);
                        PropertyState principal = (PropertyState) stateProvider.getItemState(principalPropId);
                        InternalValue internalValue = principal.getValues()[0];
                        final String principalValue = internalValue.getString();
                        if (principalValue.startsWith("u:")) {
                            PropertyId grantPropId = new PropertyId(childNodeEntry.getId(), J_ACE_GRANT);
                            PropertyState grant = (PropertyState) stateProvider.getItemState(grantPropId);

                            PropertyId rolesPropId = new PropertyId(childNodeEntry.getId(), J_ACE_ROLES);
                            PropertyState roles = (PropertyState) stateProvider.getItemState(rolesPropId);

                            ace.append("/");
                            if (grant.getValues()[0].getString().equals("GRANT")) {
                                for (InternalValue value : roles.getValues()) {
                                    ace.append(value.getName().getLocalName()).append("/");
                                }
                            }
                            ace.append(principalValue.substring(2));
                        }
                    }

                    acls.add(0, ace.toString());

                    PropertyId propId = new PropertyId(aclChildNode.getId(), J_ACL_INHERITED);
                    try {
                        PropertyState ps = (PropertyState) stateProvider.getItemState(propId);
                        if (ps.getValues().length == 1) {
                            if (!ps.getValues()[0].getBoolean()) {
                                break;
                            }
                        }
                    } catch (ItemStateException e) {

                    }
                }
                if (currentNode.getParentId() != null) {
                    currentNode = (NodeState) stateProvider.getItemState(currentNode.getParentId());
                } else {
                    currentNode = null;
                }
            }
        } catch (NoSuchItemStateException e) {
            throwRepositoryException(e);
        } catch (ItemStateException e) {
            throwRepositoryException(e);
        }
        doc.add(new Field(ACL_UUID, false, StringUtils.join(acls, " "), Field.Store.YES,
                Field.Index.NOT_ANALYZED_NO_NORMS, Field.TermVector.NO));
    }

    /**
     * Returns <code>true</code> if ACL-UUID should be resolved and stored in index.
     * This can have a negative effect on performance, when setting rights on a node,
     * which has a large subtree using the same rights, as all these nodes will need
     * to be reindexed. On the other side the advantage is that the queries are faster,
     * as the user rights are resolved faster.
     *
     * @return Returns <code>true</code> if ACL-UUID should be resolved and stored in index.
     */
    public boolean isAddAclUuidInIndex() {
        return addAclUuidInIndex;
    }

    public void setAddAclUuidInIndex(boolean addAclUuidInIndex) {
        this.addAclUuidInIndex = addAclUuidInIndex;
    }

    /**
     * Does this indexer use optimized ACE indexation. Set by the JahiaSearchIndex based
     * on a list of node types allowing this optimization.
     * @return
     */
    public boolean isUseOptimizedACEIndexation() {
        return useOptimizedACEIndexation;
    }

    public void setUseOptimizedACEIndexation(boolean useOptimizedACEIndexation) {
        this.useOptimizedACEIndexation = useOptimizedACEIndexation;
    }

    public static JahiaNodeIndexer createNodeIndexer(NodeState node, ItemStateManager itemStateManager,
            NamespaceMappings nsMappings, Executor executor, Parser parser, QueryHandlerContext context) {
        final NodeTypeRegistry typeRegistry = NodeTypeRegistry.getInstance();
        final NamespaceRegistry namespaceRegistry = context.getNamespaceRegistry();
        try {

            if (Constants.JAHIANT_TRANSLATION
                    .equals(getTypeNameAsString(node.getNodeTypeName(), namespaceRegistry))) {
                return new JahiaTranslationNodeIndexer(node, itemStateManager, nsMappings, executor, parser,
                        context, typeRegistry, namespaceRegistry);
            } else {
                return new JahiaNodeIndexer(node, itemStateManager, nsMappings, executor, parser, context,
                        typeRegistry, namespaceRegistry);
            }
        } catch (RepositoryException e) {
            throw new RuntimeException(e);
        }
    }
}