/*
 * ModeShape (http://www.modeshape.org)
 *
 * Licensed 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.modeshape.jcr;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.jcr.Binary;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Value;
import javax.jcr.ValueFactory;
import javax.jcr.ValueFormatException;
import javax.jcr.nodetype.ConstraintViolationException;
import org.modeshape.common.SystemFailureException;
import org.modeshape.common.annotation.NotThreadSafe;
import org.modeshape.common.collection.Collections;
import org.modeshape.common.collection.LinkedHashMultimap;
import org.modeshape.common.text.TextDecoder;
import org.modeshape.common.text.XmlNameEncoder;
import org.modeshape.common.util.Base64;
import org.modeshape.common.util.StringUtil;
import org.modeshape.jcr.cache.CachedNode;
import org.modeshape.jcr.cache.CachedNode.ReferenceType;
import org.modeshape.jcr.cache.MutableCachedNode;
import org.modeshape.jcr.cache.NodeKey;
import org.modeshape.jcr.cache.ReferrerCounts;
import org.modeshape.jcr.cache.SessionCache;
import org.modeshape.jcr.value.Name;
import org.modeshape.jcr.value.NameFactory;
import org.modeshape.jcr.value.NamespaceRegistry;
import org.modeshape.jcr.value.Path;
import org.modeshape.jcr.value.PathFactory;
import org.modeshape.jcr.value.Property;
import org.modeshape.jcr.value.PropertyFactory;
import org.modeshape.jcr.value.basic.NodeKeyReference;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * Content handler that provides SAX-based event handling that maps incoming documents to the repository based on the
 * functionality described in section 7.3 of the JCR 1.0.1 specification.
 * <p>
 * Each content handler is only intended to be used once and discarded. This class is <b>NOT</b> thread-safe.
 * </p>
 * 
 * @see JcrSession#getImportContentHandler(String, int)
 * @see JcrWorkspace#getImportContentHandler(String, int)
 */
@NotThreadSafe
class JcrContentHandler extends DefaultHandler {

    /**
     * Encoder to properly escape XML names.
     * 
     * @see XmlNameEncoder
     */
    protected static final TextDecoder SYSTEM_VIEW_NAME_DECODER = new XmlNameEncoder();
    protected static final TextDecoder DOCUMENT_VIEW_NAME_DECODER = JcrDocumentViewExporter.NAME_DECODER;
    protected static final TextDecoder DOCUMENT_VIEW_VALUE_DECODER = JcrDocumentViewExporter.VALUE_DECODER;
    protected static final List<String> INTERNAL_MIXINS = Arrays.asList(JcrMixLexicon.VERSIONABLE.getString().toLowerCase());

    private static final String ALT_XML_SCHEMA_NAMESPACE_PREFIX = "xsd";

    protected final NamespaceRegistry namespaces;
    protected final int uuidBehavior;
    protected final boolean retentionInfoRetained;
    protected final boolean lifecycleInfoRetained;
    protected final List<AbstractJcrNode> nodesForPostProcessing = new LinkedList<>();
    protected final Map<String, NodeKey> uuidToNodeKeyMapping = new HashMap<>();
    protected final Set<NodeKey> importedNodeKeys = new HashSet<>();
    protected final Map<NodeKey, String> shareIdsToUUIDMap = new HashMap<>();
    protected final Map<NodeKey, ReferrerCounts> referrersByNodeKey = new HashMap<>();
    protected final LinkedHashMultimap<NodeKey, ReferenceProperty> allReferenceProperties = LinkedHashMultimap.create();
    protected SessionCache cache;
    protected final String primaryTypeName;
    protected final String mixinTypesName;
    protected final String uuidName;

    private final JcrSession session;
    private final ExecutionContext context;
    private final NameFactory nameFactory;
    private final PathFactory pathFactory;
    private final org.modeshape.jcr.value.ValueFactory<String> stringFactory;
    private final ValueFactory jcrValueFactory;
    private final JcrNodeTypeManager nodeTypes;
    private final org.modeshape.jcr.api.NamespaceRegistry jcrNamespaceRegistry;
    private final boolean saveWhenCompleted;
    private final String systemWorkspaceKey;

    private AbstractJcrNode currentNode;
    private ContentHandler delegate;


    JcrContentHandler( JcrSession session,
                       AbstractJcrNode parent,
                       int uuidBehavior,
                       boolean saveWhenCompleted,
                       boolean retentionInfoRetained,
                       boolean lifecycleInfoRetained ) throws PathNotFoundException, RepositoryException {
        assert session != null;
        assert uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW
               || uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING
               || uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING
               || uuidBehavior == ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW;

        this.session = session;
        this.session.initBaseVersionKeys();
        this.context = this.session.context();
        this.namespaces = context.getNamespaceRegistry();
        this.nameFactory = context.getValueFactories().getNameFactory();
        this.pathFactory = context.getValueFactories().getPathFactory();
        this.stringFactory = context.getValueFactories().getStringFactory();
        this.uuidBehavior = uuidBehavior;
        this.retentionInfoRetained = retentionInfoRetained;
        this.lifecycleInfoRetained = lifecycleInfoRetained;
        this.saveWhenCompleted = saveWhenCompleted;

        this.cache = session.cache();

        this.currentNode = parent;

        this.jcrValueFactory = session.getValueFactory();
        this.nodeTypes = session.nodeTypeManager();
        this.jcrNamespaceRegistry = (org.modeshape.jcr.api.NamespaceRegistry)session.workspace().getNamespaceRegistry();

        this.primaryTypeName = JcrLexicon.PRIMARY_TYPE.getString(this.namespaces);
        this.mixinTypesName = JcrLexicon.MIXIN_TYPES.getString(this.namespaces);
        this.uuidName = JcrLexicon.UUID.getString(this.namespaces);
        this.systemWorkspaceKey = session.repository().systemWorkspaceKey();
    }

    protected final JcrSession session() {
        return session;
    }

    protected final NamespaceRegistry namespaces() {
        return namespaces;
    }

    protected final JcrNodeTypeManager nodeTypes() {
        return nodeTypes;
    }

    protected final JcrNodeType nodeTypeFor( String name ) {
        return nodeTypes.getNodeType(nameFor(name));
    }

    protected final Map<Name, Integer> propertyTypesFor( String primaryTypeName ) {
        Map<Name, Integer> propertyTypesMap = new HashMap<>();
        JcrNodeType nodeType = nodeTypeFor(primaryTypeName);
        if (nodeType == null) {
            // nt:share falls in this category
            return propertyTypesMap;
        }
        for (JcrPropertyDefinition propertyDefinition : nodeType.getPropertyDefinitions()) {
            propertyTypesMap.put(propertyDefinition.getInternalName(), propertyDefinition.getRequiredType());
        }
        return propertyTypesMap;
    }

    protected final String stringFor( Object name ) {
        return stringFactory.create(name);
    }

    protected final Name nameFor( String name ) {
        return nameFactory.create(name);
    }

    protected final Name nameFor( String namespaceUri,
                                  String localName ) {
        return nameFactory.create(namespaceUri, localName);
    }

    protected final Path pathFor( Path parentPath,
                                  Name... names ) {
        return pathFactory.create(parentPath, names);
    }

    protected final Value valueFor( String value,
                                    int type ) throws ValueFormatException {
        return jcrValueFactory.createValue(value, type);
    }

    protected final Value valueFor( InputStream stream ) throws RepositoryException {
        Binary binary = jcrValueFactory.createBinary(stream);
        return jcrValueFactory.createValue(binary);
    }

    protected final SessionCache cache() {
        return cache;
    }

    protected final boolean isInternal(Name propertyName) {
        return
                propertyName.getNamespaceUri().equals(JcrLexicon.Namespace.URI) ||
                propertyName.getNamespaceUri().equals(JcrNtLexicon.Namespace.URI) ||
                propertyName.getNamespaceUri().equals(ModeShapeLexicon.NAMESPACE.getNamespaceUri());
    }

    protected void postProcessNodes() throws SAXException {
        try {
            // first make sure all the necessary reference properties have been set
            processReferences();
            
            for (AbstractJcrNode node : nodesForPostProcessing) {
                MutableCachedNode mutable = node.mutable();

                // ---------------
                // mix:versionable
                // ---------------
                if (node.isNodeType(JcrMixLexicon.VERSIONABLE)) {

                    // Does the versionable node already have a reference to the version history?
                    // If so, then we ignore it because we'll use our own key ...

                    // Does the versionable node already have a base version?
                    AbstractJcrProperty baseVersionProp = node.getProperty(JcrLexicon.BASE_VERSION);
                    if (baseVersionProp != null) {
                        // we rely on the fact that the base version ref is exported with full key
                        NodeKeyReference baseVersionRef = (NodeKeyReference)baseVersionProp.getValue().value();
                        String workspaceKey = baseVersionRef.getNodeKey().getWorkspaceKey();
                        //we only register the base version if it comes from the system workspace (if it doesn't come from the
                        //system workspace, it's not valid - e.g. could be coming from an older version of ModeShape)
                        if (systemWorkspaceKey.equals(workspaceKey)) {
                            session.setDesiredBaseVersionKey(node.key(), baseVersionRef.getNodeKey());
                        }
                    }
                }

                // ---------------
                // mix:lockable
                // ---------------
                if (node.isNodeType(JcrMixLexicon.LOCKABLE) && node.isLocked()) {
                    // Nodes should not be locked upon import ...
                    node.unlock();
                }

                // ---------------
                // mix:lifecycle
                // ---------------
                if (node.isNodeType(JcrMixLexicon.LIFECYCLE)) {
                    if (lifecycleInfoRetained && !isValidReference(node, JcrLexicon.LIFECYCLE_POLICY, false)) {
                        // The 'jcr:lifecyclePolicy' REFERENCE values is not valid or does not reference an existing node,
                        // so the 'jcr:lifecyclePolicy' and 'jcr:currentLifecycleState' properties should be removed...
                        mutable.removeProperty(cache, JcrLexicon.LIFECYCLE_POLICY);
                        mutable.removeProperty(cache, JcrLexicon.CURRENT_LIFECYCLE_STATE);
                    }
                }

                // --------------------
                // mix:managedRetention
                // --------------------
                if (node.isNodeType(JcrMixLexicon.MANAGED_RETENTION)) {
                    if (retentionInfoRetained && !isValidReference(node, JcrLexicon.RETENTION_POLICY, false)) {
                        // The 'jcr:retentionPolicy' REFERENCE values is not valid or does not reference an existing node,
                        // so the 'jcr:retentionPolicy', 'jcr:hold' and 'jcr:isDeep' properties should be removed ...
                        mutable.removeProperty(cache, JcrLexicon.HOLD);
                        mutable.removeProperty(cache, JcrLexicon.IS_DEEP);
                        mutable.removeProperty(cache, JcrLexicon.RETENTION_POLICY);
                    }

                }

                // --------------------
                // mix:share
                // --------------------
                if (node.isNodeType(ModeShapeLexicon.SHARE)) {
                    // get the actual key of the shareable node
                    String shareableNodeUUID = shareIdsToUUIDMap.get(node.key());
                    assert shareableNodeUUID != null;
                    NodeKey shareableNodeKey = uuidToNodeKeyMapping.get(shareableNodeUUID);
                    assert shareableNodeKey != null;

                    // unlink the current key from its parent references
                    NodeKey parentKey = mutable.getParentKey(cache);
                    MutableCachedNode parent = cache.mutable(parentKey);
                    parent.removeChild(cache, node.key());

                    // re-link it with the correct key - that of the shareable node
                    parent.linkChild(cache, shareableNodeKey, node.name());
                }
            }
        } catch (RepositoryException e) {
            throw new EnclosingSAXException(e);
        }
    }

    private void processReferences() throws RepositoryException {
        // if there were any reference properties imported, they can only be set on the corresponding nodes *after* all
        // the graph has been imported
        for (Map.Entry<NodeKey, ReferenceProperty> entry : this.allReferenceProperties.entries()) {
            AbstractJcrNode node = session.node(entry.getKey(), null);
            ReferenceProperty referenceProperty = entry.getValue();
            AbstractJcrProperty property = null;
            // set the reference property without validating it first
            if (!referenceProperty.isMultiple()) {
                property = node.setProperty(referenceProperty.name(), referenceProperty.value(), true, true, true,
                                            false);
            } else {
                property = node.setProperty(referenceProperty.name(), referenceProperty.values(),
                                            referenceProperty.type(), true, true, false, true);
            }
            // check if there are any public hard references with constraints in which case we should validate them
            // this should not look at any internal (protected) references as they may be changed later on
            if (property.getType() == PropertyType.REFERENCE &&
                property.getDefinition().getValueConstraints().length != 0 &&
                !property.getDefinition().isProtected()) {   
                if (!isValidReference(property)) {
                    JcrPropertyDefinition defn = property.getDefinition();
                    String name = stringFor(property.name());
                    String path = property.getParent().getPath();
                    throw new ConstraintViolationException(JcrI18n.constraintViolatedOnReference.text(name, path, defn));
                }
            }
        }
        
        // Restore the back references on the nodes which have been removed/replaced by the import and which have referrers
        // outside the graph of nodes that was imported
        for (Map.Entry<NodeKey, ReferrerCounts> entry : referrersByNodeKey.entrySet()) {
            PropertyFactory propFactory = context.getPropertyFactory();
            MutableCachedNode referred = cache.mutable(entry.getKey());
            ReferrerCounts counts = entry.getValue();
            if (referred != null && counts != null) {
                // Add in the strong and weak referrers (that are outside the import scope) that used to be in the node
                // before it was replaced ...
                for (NodeKey key : counts.getStrongReferrers()) {
                    int count = counts.countStrongReferencesFrom(key);
                    for (int i = 0; i != count; ++i) {
                        Property prop = propFactory.create(nameFor(key.toString() + i));
                        referred.addReferrer(cache, prop, key, ReferenceType.STRONG);
                    }
                }
                for (NodeKey key : counts.getWeakReferrers()) {
                    int count = counts.countWeakReferencesFrom(key);
                    for (int i = 0; i != count; ++i) {
                        Property prop = propFactory.create(nameFor(key.toString() + i));
                        referred.addReferrer(cache, prop, key, ReferenceType.WEAK);
                    }
                }
            }
        }
    }

    protected boolean isValidReference( AbstractJcrNode node,
                                        Name propertyName,
                                        boolean returnValueIfNoProperty ) throws RepositoryException {
        AbstractJcrProperty property = node.getProperty(propertyName);
        return property == null ? returnValueIfNoProperty : isValidReference(property);
    }

    protected boolean isValidReference( AbstractJcrProperty property ) throws RepositoryException {
        JcrPropertyDefinition defn = property.getDefinition();
        if (defn == null) return false;
        if (property.isMultiple()) {
            for (Value value : property.getValues()) {
                if (!defn.canCastToTypeAndSatisfyConstraints(value, session)) {
                    // We know it's not valid, so return ...
                    return false;
                }
            }
            // All values appeared to be valid ...
            return true;
        }
        // Just a single value ...
        return defn.canCastToTypeAndSatisfyConstraints(property.getValue(), session);
    }

    @Override
    public void characters( char[] ch,
                            int start,
                            int length ) throws SAXException {
        assert this.delegate != null;
        delegate.characters(ch, start, length);
    }

    @Override
    public void endDocument() throws SAXException {
        postProcessNodes();
        if (saveWhenCompleted) {
            try {
                session.save();
            } catch (RepositoryException e) {
                throw new SAXException(e);
            }
        }
        super.endDocument();
    }

    @Override
    public void endElement( String uri,
                            String localName,
                            String name ) throws SAXException {
        assert this.delegate != null;
        delegate.endElement(uri, localName, name);
    }

    @Override
    public void startElement( String uri,
                              String localName,
                              String name,
                              Attributes atts ) throws SAXException {
        checkDelegate(uri);
        assert this.delegate != null;

        delegate.startElement(uri, localName, name, atts);
    }

    private void checkDelegate( String namespaceUri ) {
        if (delegate != null) return;

        if (JcrSvLexicon.Namespace.URI.equals(namespaceUri)) {
            this.delegate = new SystemViewContentHandler(this.currentNode);
        } else {
            this.delegate = new DocumentViewContentHandler(this.currentNode);
        }
    }

    protected static byte[] decodeBase64( String value ) throws IOException {
        try {
            return Base64.decode(value.getBytes("UTF-8"));
        } catch (IOException e) {
            // try re-reading, in case this was an export from a prior ModeShape version that used URL_SAFE ...
            return Base64.decode(value, Base64.URL_SAFE);
        }
    }

    protected static String decodeBase64AsString( String value ) throws IOException {
        byte[] decoded = decodeBase64(value);
        return new String(decoded, "UTF-8");
    }

    @Override
    public void startPrefixMapping( String prefix,
                                    String uri ) throws SAXException {
        try {
            if (ALT_XML_SCHEMA_NAMESPACE_PREFIX.equals(prefix) && uri.equals(JcrNamespaceRegistry.XML_SCHEMA_NAMESPACE_URI)) {
                prefix = JcrNamespaceRegistry.XML_SCHEMA_NAMESPACE_PREFIX;
            }

            // Read from the workspace's ModeShape registry, as its semantics are more friendly
            String existingUri = namespaces.getNamespaceForPrefix(prefix);

            if (existingUri != null) {
                if (existingUri.equals(uri)) {
                    // prefix/uri mapping is already in registry
                    return;
                }
                // The prefix is used and it does not match the registered namespace. Therefore, register using
                // a generated namespace ...
                this.jcrNamespaceRegistry.registerNamespace(uri);
            } else {
                // It is not yet registered, so register through the JCR workspace to ensure consistency
                this.jcrNamespaceRegistry.registerNamespace(prefix, uri);
            }
        } catch (RepositoryException re) {
            throw new EnclosingSAXException(re);
        }
    }

    class EnclosingSAXException extends SAXException {

        private static final long serialVersionUID = -1044992767566435542L;

        EnclosingSAXException( Exception e ) {
            super(e);

        }

    }

    // ----------------------------------------------------------------------------------------------------------------
    // NodeHandler framework ...
    // ----------------------------------------------------------------------------------------------------------------

    @SuppressWarnings( "unused" )
    protected abstract class NodeHandler {
        public void finish() throws SAXException {
        }

        public AbstractJcrNode node() throws SAXException {
            return null;
        }

        public NodeHandler parentHandler() {
            return null;
        }

        public boolean ignoreAllChildren() {
            return false;
        }

        public void addPropertyValue( Name name,
                                      String value,
                                      boolean forceMultiValued,
                                      int propertyType,
                                      TextDecoder decoder ) throws EnclosingSAXException {
        }

        protected String name() {
            try {
                Path path = node().path();
                return path.isRoot() ? "" : stringFor(path.getLastSegment());
            } catch (Exception e) {
                throw new SystemFailureException(e);
            }
        }

        @Override
        public String toString() {
            NodeHandler parent = parentHandler();
            if (parent != null) {
                return parent.toString() + "/" + name();
            }
            try {
                return node().getPath();
            } catch (Throwable e) {
                try {
                    return node().toString();
                } catch (SAXException e2) {
                    throw new SystemFailureException(e2);
                }
            }
        }
    }

    /**
     * Some nodes need additional post-processing upon import, and this set of property names is used to come up with the nodes
     * that may need to be post-processed.
     * <p>
     * Really, the nodes that need to be post-processed are best found using the node types of each node. However, that is more
     * expensive to compute. Thus, we'll collect the candidate nodes that are candidates for post-processing, then in the
     * post-processing we can more effectively and efficiently use the node types.
     * </p>
     * <p>
     * Currently, we want to post-process nodes that contain repository-level semantics. In other words, nodes that are of the
     * following node types:
     * <ul>
     * <li><code>mix:versionable</code></li>
     * <li><code>mix:lockable</code></li>
     * <li><code>mix:lifecycle</code></li>
     * <li><code>mix:managedRetention</code></li>
     * </ul>
     * The <code>mix:simpleVersionable</code> would normally also be included here, except that the <code>jcr:isCheckedOut</code>
     * property is a boolean value that doesn't need any particular post-processing.
     * </p>
     * <p>
     * Some of these node types has a mandatory property, so the names of these mandatory properties are used to quickly determine
     * candidates for post-processing. In cases where there is no mandatory property, then the set of all properties for that node
     * type are included:
     * <ul>
     * <li><code>mix:versionable</code> --> <code>jcr:baseVersion</code> (mandatory)</li>
     * <li><code>mix:lockable</code> --> <code>jcr:lockOwner</code> and <code>jcr:lockIsDeep</code></li>
     * <li><code>mix:lifecycle</code> --> <code>jcr:lifecyclePolicy</code> and <code>jcr:currentLifecycleState</code></li>
     * <li><code>mix:managedRetention</code> --> <code>jcr:hold</code>, <code>jcr:isDeep</code>, and
     * <code>jcr:retentionPolicy</code></li>
     * </ul>
     * </p>
     */
    protected static final Set<Name> PROPERTIES_FOR_POST_PROCESSING = Collections.unmodifiableSet(
    /* 'mix:lockable' has two optional properties */
    JcrLexicon.LOCK_IS_DEEP, JcrLexicon.LOCK_OWNER,
    /* 'mix:versionable' has several mandatory properties, but we only need to check one */
    JcrLexicon.BASE_VERSION,
    /* 'mix:lifecycle' has two optional properties */
    JcrLexicon.LIFECYCLE_POLICY, JcrLexicon.CURRENT_LIFECYCLE_STATE,
    /* 'mix:managedRetention' has three optional properties */
    JcrLexicon.HOLD, JcrLexicon.IS_DEEP, JcrLexicon.RETENTION_POLICY);

    protected class BasicNodeHandler extends NodeHandler {
        private final Map<Name, List<Value>> properties;
        private final Set<Name> multiValuedPropertyNames;
        private final Name nodeName;
        private NodeHandler parentHandler;
        private AbstractJcrNode node;
        private final int uuidBehavior;
        private boolean postProcessed = false;
        private boolean ignoreAllChildren = false;

        protected BasicNodeHandler( Name name,
                                    NodeHandler parentHandler,
                                    int uuidBehavior ) {
            this.nodeName = name;
            this.parentHandler = parentHandler;
            this.properties = new HashMap<>();
            this.multiValuedPropertyNames = new HashSet<>();
            this.uuidBehavior = uuidBehavior;
        }

        @Override
        public void finish() throws SAXException {
            node();
        }

        @Override
        public boolean ignoreAllChildren() {
            return ignoreAllChildren;
        }

        @Override
        protected String name() {
            return stringFor(nodeName);
        }

        @Override
        public AbstractJcrNode node() throws SAXException {
            if (node == null) create();
            assert node != null;
            return node;
        }

        @Override
        public NodeHandler parentHandler() {
            return parentHandler;
        }

        @Override
        public void addPropertyValue( Name name,
                                      String value,
                                      boolean forceMultiValued,
                                      int propertyType,
                                      TextDecoder decoder ) throws EnclosingSAXException {
            if (forceMultiValued) {
                this.multiValuedPropertyNames.add(name);
            }

            try {
                if (node != null) {
                    if (JcrLexicon.PRIMARY_TYPE.equals(name)) return;
                    if (JcrLexicon.MIXIN_TYPES.equals(name)) return;
                    if (JcrLexicon.UUID.equals(name)) return;

                    // The node was already created, so set the property using the editor ...
                    node.setProperty(name, (JcrValue)valueFor(value, propertyType), true, true, true, false);
                } else {
                    // The node hasn't been created yet, so just enqueue the property value into the map ...
                    List<Value> values = properties.get(name);
                    if (values == null) {
                        values = new ArrayList<>();
                        properties.put(name, values);
                    }
                    if (forceMultiValued && value.indexOf(" ") > 0) {
                        String[] stringValues = value.split(" ");
                        for (String stringValue : stringValues) {
                            processPropertyValue(name, stringValue, propertyType, decoder, values);                            
                        }
                    }
                    else {
                        processPropertyValue(name, value, propertyType, decoder, values);
                    }
                }
                if (!postProcessed && PROPERTIES_FOR_POST_PROCESSING.contains(name)) {
                    postProcessed = true;
                }
            } catch (IOException | SAXException | RepositoryException ioe) {
                throw new EnclosingSAXException(ioe);
            }
        }

        private void processPropertyValue( Name name, String value, int propertyType, TextDecoder decoder,
                                           List<Value> values ) throws UnsupportedEncodingException, RepositoryException, SAXException {
            if (propertyType == PropertyType.BINARY) {
                Base64.InputStream is = new Base64.InputStream(new ByteArrayInputStream(value.getBytes("UTF-8")));
                values.add(valueFor(is));
            } else {
                if (decoder != null) value = decoder.decode(value);
                if (value != null && propertyType == PropertyType.STRING) {
                    // Strings and binaries can be empty -- other data types cannot
                    values.add(valueFor(value, propertyType));
                } else if (!StringUtil.isBlank(value) && isReference(propertyType)) {
                    
                        boolean isInternalReference = isInternal(name);
                        if (!isInternalReference) {
                            // we only prepend the parent information for non-internal references
                            value = parentHandler().node().key().withId(value).toString();
                        }
                        // we only have the identifier of the node, so try to use the parent to determine the workspace &
                        // source key
                        values.add(valueFor(value, propertyType));
                    
                } else if (!StringUtil.isBlank(value)) {
                    values.add(valueFor(value, propertyType));
                }
            }
        }

        protected void create() throws SAXException {
            try {
                AbstractJcrNode parent = parentHandler.node();
                final NodeKey parentKey = parent.key();
                assert parent != null;

                // Figure out the key for the node ...
                NodeKey key = null;
                List<Value> rawUuid = properties.get(JcrLexicon.UUID);
                String uuid = null;
                boolean shareableNodeAlreadyExists = false;
                if (rawUuid != null) {
                    assert rawUuid.size() == 1;
                    uuid = rawUuid.get(0).getString();
                    key = parentKey.withId(uuid);

                    try {
                        // Deal with any existing node ...
                        AbstractJcrNode existingNode = session().node(key, null, parentKey);
                        switch (uuidBehavior) {
                            case ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING:
                                parent = existingNode.getParent();
                                // Attention: this *does not* remove the entry from the DB. Therefore, it's always
                                // accessible
                                // to the workspace cache and thus to the current session !!!.
                                // Therefore, *old properties, mixins etc* will be accessible on the new child created later on
                                // until a session.save() is performed.
                                preRemoveNode(existingNode);
                                existingNode.remove();
                                break;
                            case ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW:
                                key = cache().getRootKey().withRandomId();
                                break;
                            case ImportUUIDBehavior.IMPORT_UUID_COLLISION_REMOVE_EXISTING:
                                if (existingNode.path().isAtOrAbove(parent.path())) {
                                    String text = JcrI18n.cannotRemoveParentNodeOfTarget.text(existingNode.getPath(),
                                                                                              key,
                                                                                              parent.getPath());
                                    throw new ConstraintViolationException(text);
                                }
                                // Attention: this *does not* remove the entry from the DB. Therefore, it's always
                                // accessible
                                // to the workspace cache and thus to the current session !!!.
                                // Therefore, *old properties, mixins etc* will be accessible on the new child created later on
                                // until a session.save() is performed.
                                preRemoveNode(existingNode);
                                existingNode.remove();
                                break;
                            case ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW:
                                if (existingNode.isShareable()) {
                                    shareableNodeAlreadyExists = true;
                                } else {
                                    throw new ItemExistsException(JcrI18n.itemAlreadyExistsWithUuid.text(key,
                                                                                                         session().workspace()
                                                                                                                  .getName(),
                                                                                                         existingNode.getPath()));
                                }
                        }
                    } catch (ItemNotFoundException e) {
                        // there wasn't an existing item, so just continue
                    }

                }

                // See if the node was already autocreated by the parent
                AbstractJcrNode existingNode = parent.getNodeIfExists(nodeName);
                boolean nodeAlreadyExists = existingNode != null && existingNode.getDefinition().isAutoCreated();

                // Create the new node ...
                AbstractJcrNode child;
                if (!nodeAlreadyExists) {
                    List<Value> primaryTypeValueList = properties.get(JcrLexicon.PRIMARY_TYPE);
                    String typeName = primaryTypeValueList != null ? primaryTypeValueList.get(0).getString() : null;
                    Name primaryTypeName = nameFor(typeName);

                    if (JcrNtLexicon.SHARE.equals(primaryTypeName)) {
                        assert key != null;
                        assert uuid != null;

                        // check if we already have the key of the shareable node
                        NodeKey shareableNodeKey = uuidToNodeKeyMapping.get(uuid);
                        if (shareableNodeKey != null) {
                            // we already know the key of the shareable node, so we need to just link it and return
                            parent.mutable().linkChild(cache, shareableNodeKey, nodeName);
                            node = session().node(shareableNodeKey, null, parentKey);
                        } else {
                            // we haven't processed the shareable node yet, so we need to make sure we process the share later.
                            parent.mutable().linkChild(cache, key, nodeName);
                            node = session().node(key, null, parentKey);
                            // make sure we post-process the share and set the correct id
                            nodesForPostProcessing.add(node);
                            // save the original UUID of the share to be able to track it back to the shareable node
                            shareIdsToUUIDMap.put(key, uuid);
                        }
                        ignoreAllChildren = true;
                        return;
                    }

                    // store the node key that we created for this UUID, so we can create shares
                    uuidToNodeKeyMapping.put(uuid, key);

                    if (shareableNodeAlreadyExists && key != null) {
                        parent.mutable().linkChild(cache, key, nodeName);
                        node = session().node(key, null, parentKey);
                        ignoreAllChildren = true;
                        return;
                    }

                    // Otherwise, it's just a regular node...
                    child = parent.addChildNode(nodeName, primaryTypeName, key, true, false);
                } else {
                    child = existingNode;
                }
                assert child != null;

                // Set the properties on the new node ...

                // Set the mixin types first (before we set any properties that may require the mixins to be present) ...
                List<Value> mixinTypeValueList = properties.get(JcrLexicon.MIXIN_TYPES);
                if (mixinTypeValueList != null) {
                    for (Value value : mixinTypeValueList) {
                        String mixinName = value.getString();
                        // in the case when keys are being reused, the old node at that key is visible (with all its properties)
                        // via the WS cache -> db. Therefore, there might be the case when even though the child was created
                        // via addChild(), the old node with all the old properties and mixins is still visible at the key() and
                        // so the "new child" reports the mixin as already present (even though it's not)
                        boolean addMixinInternally = (child.isNodeType(mixinName) && !nodeAlreadyExists) ||
                                                     INTERNAL_MIXINS.contains(mixinName.toLowerCase());
                        if (addMixinInternally) {
                            child.mutable().addMixin(child.sessionCache(), nameFor(mixinName));
                        } else {
                            child.addMixin(mixinName);
                        }
                    }
                }

                for (Map.Entry<Name, List<Value>> entry : properties.entrySet()) {
                    Name propertyName = entry.getKey();

                    // These are all handled earlier ...
                    if (JcrLexicon.PRIMARY_TYPE.equals(propertyName)) {
                        continue;
                    }
                    if (JcrLexicon.MIXIN_TYPES.equals(propertyName)) {
                        continue;
                    }
                    if (JcrLexicon.UUID.equals(propertyName)) {
                        continue;
                    }

                    List<Value> values = entry.getValue();

                    boolean allowEmptyValues = !isInternal(propertyName) || JcrLexicon.DATA.equals(propertyName);
                    if (values.size() == 1 && !this.multiValuedPropertyNames.contains(propertyName)) {
                        JcrValue value = (JcrValue)values.get(0);
                        if (!allowEmptyValues && StringUtil.isBlank(value.getString())) {
                            //if an empty value has creeped here it means we weren't able to perform proper type validation earlier
                            continue;
                        }
                        if (isReference(value.getType())) {
                            // if this is a reference, we won't set it on the node until we've finished loading all the nodes
                            ReferenceProperty referenceProperty = new ReferenceProperty(propertyName, value);
                            allReferenceProperties.put(child.key(), referenceProperty);
                            nodesForPostProcessing.add(child);
                        } else {
                            // Don't check references or the protected status ...
                            child.setProperty(propertyName, value, true, true, true, false);
                        }
                    } else {
                        if (!allowEmptyValues) {
                            for (Iterator<Value> iterator = values.iterator(); iterator.hasNext();) {
                                if (StringUtil.isBlank(iterator.next().getString())) {
                                    iterator.remove();
                                }
                            }
                        }
                        if (!values.isEmpty()) {
                            Value[] processedValues = values.toArray(new Value[values.size()]);
                            if (isReference(processedValues[0].getType())) {
                                // if this is a reference, we won't set it on the node until we've finished loading all the nodes
                                ReferenceProperty referenceProperty = new ReferenceProperty(propertyName, processedValues);
                                allReferenceProperties.put(child.key(), referenceProperty);
                                nodesForPostProcessing.add(child);
                            } else {
                                // Don't check references or the protected status ...
                                child.setProperty(propertyName, processedValues, PropertyType.UNDEFINED, true, true, false,
                                                  true);
                            }
                        }
                    }
                }

                node = child;
                importedNodeKeys.add(node.key());

                if (postProcessed) {
                    // This node needs to be post-processed ...
                    nodesForPostProcessing.add(node);
                }

            } catch (RepositoryException re) {
                throw new EnclosingSAXException(re);
            }
        }

        /**
         * Handle any operations before a node is removed or replaced by an imported node.
         *
         * @param removedNode the removed node
         */
        protected void preRemoveNode( AbstractJcrNode removedNode ) {
            // Figure out if the node has backreferences ...
            CachedNode node;
            try {
                node = removedNode.node();
                ReferrerCounts referrers = node.getReferrerCounts(cache);
                if (referrers != null) referrersByNodeKey.put(node.getKey(), referrers);
            } catch (ItemNotFoundException | InvalidItemStateException err) {
                // do nothing ...
            }
        }
    }

    protected boolean isReference( int propertyType ) {
        return (propertyType == PropertyType.REFERENCE ||
            propertyType == PropertyType.WEAKREFERENCE ||
            propertyType == org.modeshape.jcr.api.PropertyType.SIMPLE_REFERENCE);
    }

    protected class ExistingNodeHandler extends NodeHandler {
        private final AbstractJcrNode node;
        private final NodeHandler parentHandler;

        protected ExistingNodeHandler( AbstractJcrNode node,
                                       NodeHandler parentHandler ) {
            this.node = node;
            this.parentHandler = parentHandler;
        }

        @Override
        public AbstractJcrNode node() {
            return node;
        }

        @Override
        public NodeHandler parentHandler() {
            return parentHandler;
        }

        @Override
        public void addPropertyValue( Name propertyName,
                                      String value,
                                      boolean forceMultiValued,
                                      int propertyType,
                                      TextDecoder decoder ) {
            throw new UnsupportedOperationException();
        }
    }

    protected class JcrRootHandler extends ExistingNodeHandler {
        protected JcrRootHandler( AbstractJcrNode root ) {
            super(root, null);
        }

        @Override
        public void addPropertyValue( Name propertyName,
                                      String value,
                                      boolean forceMultiValued,
                                      int propertyType,
                                      TextDecoder decoder ) {
            // do nothing ...
        }
    }

    protected class IgnoreBranchHandler extends NodeHandler {
        private NodeHandler parentHandler;

        protected IgnoreBranchHandler( NodeHandler parentHandler ) {
            this.parentHandler = parentHandler;
        }

        @Override
        public NodeHandler parentHandler() {
            return parentHandler;
        }
    }

    protected class JcrSystemHandler extends IgnoreBranchHandler {

        protected JcrSystemHandler( NodeHandler parentHandler ) {
            super(parentHandler);
        }
    }

    protected interface NodeHandlerFactory {
        NodeHandler createFor( Name nodeName,
                               NodeHandler parentHandler,
                               int uuidBehavior ) throws SAXException;
    }

    protected class StandardNodeHandlerFactory implements NodeHandlerFactory {
        @Override
        public NodeHandler createFor( Name name,
                                      NodeHandler parentHandler,
                                      int uuidBehavior ) throws SAXException {
            if (parentHandler instanceof IgnoreBranchHandler || parentHandler.ignoreAllChildren()) {
                return new IgnoreBranchHandler(parentHandler);
            }
            if (JcrLexicon.ROOT.equals(name)) {
                try {
                    JcrRootNode rootNode = session().getRootNode();
                    return new JcrRootHandler(rootNode);
                } catch (RepositoryException re) {
                    throw new EnclosingSAXException(re);
                }
            }
            if (JcrLexicon.SYSTEM.equals(name)) {
                // Always do this, regardless of where the "jcr:system" branch is located ...
                return new JcrSystemHandler(parentHandler);
            }
            return new BasicNodeHandler(name, parentHandler, uuidBehavior);
        }
    }

    private class SystemViewContentHandler extends DefaultHandler {
        private final String svNameName;
        private final String svTypeName;
        private final String svMultipleName;
        private NodeHandler current;
        private final NodeHandlerFactory nodeHandlerFactory;
        private String currentPropertyName;
        private int currentPropertyType;
        private boolean currentPropertyValueIsBase64Encoded;
        private boolean currentPropertyIsMultiValued;
        private final StringBuilder currentPropertyValue;

        SystemViewContentHandler( AbstractJcrNode parent ) {
            super();
            this.svNameName = JcrSvLexicon.NAME.getString(namespaces());
            this.svTypeName = JcrSvLexicon.TYPE.getString(namespaces());
            this.svMultipleName = JcrSvLexicon.MULTIPLE.getString(namespaces());
            this.current = new ExistingNodeHandler(parent, null);
            this.nodeHandlerFactory = new StandardNodeHandlerFactory();
            this.currentPropertyValue = new StringBuilder();
        }

        @Override
        public void startElement( String uri,
                                  String localName,
                                  String name,
                                  Attributes atts ) throws SAXException {
            // Always reset the string builder at the beginning of an element
            currentPropertyValue.setLength(0);
            currentPropertyValueIsBase64Encoded = false;
            if ("node".equals(localName)) {
                // Finish the parent handler ...
                current.finish();
                // Create a new handler for this element ...
                String nodeName = atts.getValue(SYSTEM_VIEW_NAME_DECODER.decode(svNameName));
                current = nodeHandlerFactory.createFor(nameFor(nodeName), current, uuidBehavior);
            } else if ("property".equals(localName)) {
                currentPropertyName = atts.getValue(SYSTEM_VIEW_NAME_DECODER.decode(svNameName));
                currentPropertyType = org.modeshape.jcr.api.PropertyType.valueFromName(atts.getValue(svTypeName));

                String svMultiple = atts.getValue(svMultipleName);
                currentPropertyIsMultiValued = Boolean.TRUE.equals(Boolean.valueOf(svMultiple));
            } else if ("value".equals(localName)) {
                // See if there is an "xsi:type" attribute on this element, which means the property value contained
                // characters that cannot be represented in XML without escaping. See Section 11.2, Item 11.b ...
                String xsiType = atts.getValue("http://www.w3.org/2001/XMLSchema-instance", "type");
                if (StringUtil.isBlank(xsiType)) {
                    return;
                }

                String propertyPrefix = namespaces.getPrefixForNamespaceUri("http://www.w3.org/2001/XMLSchema", false);
                if (StringUtil.isBlank(propertyPrefix)) {
                    return;
                }
                String base64TypeName = propertyPrefix + ":base64Binary";
                currentPropertyValueIsBase64Encoded = base64TypeName.equals(xsiType);

            } else if (!"value".equals(localName)) {
                throw new IllegalStateException("Unexpected element '" + name + "' in system view");
            }
        }

        @Override
        public void characters( char[] ch,
                                int start,
                                int length ) {
            currentPropertyValue.append(ch, start, length);
        }

        @Override
        public void endElement( String uri,
                                String localName,
                                String name ) throws SAXException {
            switch (localName) {
                case "node":
                    current.finish(); // make sure the node is created
                    current = current.parentHandler();
                    break;
                case "value":
                    // Add the content for the current property ...
                    String currentPropertyString = currentPropertyValue.toString();
                    if (currentPropertyValueIsBase64Encoded) {
                        // The current string is a base64 encoded string, so we need to decode it first ...
                        try {
                            currentPropertyString = decodeBase64AsString(currentPropertyString);
                        } catch (IOException ioe) {
                            throw new EnclosingSAXException(ioe);
                        }
                    }
                    current.addPropertyValue(nameFor(currentPropertyName),
                                             currentPropertyString,
                                             currentPropertyIsMultiValued,
                                             currentPropertyType,
                                             SYSTEM_VIEW_NAME_DECODER);
                    break;
                case "property":
                    break;
                default:
                    throw new IllegalStateException("Unexpected element '" + name + "' in system view");
            }
        }
    }

    private class DocumentViewContentHandler extends DefaultHandler {
        private NodeHandler current;
        private final NodeHandlerFactory nodeHandlerFactory;

        DocumentViewContentHandler( AbstractJcrNode currentNode ) {
            super();
            this.current = new ExistingNodeHandler(currentNode, null);
            this.nodeHandlerFactory = new StandardNodeHandlerFactory();
        }

        @Override
        public void startElement( String uri,
                                  String localName,
                                  String name,
                                  Attributes atts ) throws SAXException {

            // Create the new handler for the new node ...
            String decodedLocalName = DOCUMENT_VIEW_NAME_DECODER.decode(localName);
            current = nodeHandlerFactory.createFor(nameFor(uri, decodedLocalName), current, uuidBehavior);

            List<String> allTypes = new ArrayList<>();
            Map<Name, String> propertiesNamesValues = new HashMap<>();
            for (int i = 0; i < atts.getLength(); i++) {
                Name propertyName = nameFor(atts.getURI(i), DOCUMENT_VIEW_NAME_DECODER.decode(atts.getLocalName(i)));
                String value = atts.getValue(i);
                if (value != null) {
                    propertiesNamesValues.put(propertyName, value);
                    if (JcrLexicon.PRIMARY_TYPE.equals(propertyName) || JcrLexicon.MIXIN_TYPES.equals(propertyName)) {
                        allTypes.add(value);
                    }
                }
            }

            Map<Name, Integer> propertyTypes = new HashMap<>();
            for (String typeName : allTypes) {
                propertyTypes.putAll(propertyTypesFor(typeName));
            }

            for (Map.Entry<Name, String> entry : propertiesNamesValues.entrySet()) {
                Name propertyName = entry.getKey();
                Integer propertyDefinitionType = propertyTypes.get(propertyName);
                int propertyType = propertyDefinitionType != null ? propertyDefinitionType : PropertyType.STRING;
                String value = entry.getValue();
                boolean isMultiValued = value.indexOf(" ") > 0; 
                current.addPropertyValue(propertyName, value, isMultiValued, propertyType, DOCUMENT_VIEW_VALUE_DECODER);
            }

            // Now create the node ...
            current.finish();
        }

        @Override
        public void endElement( String uri,
                                String localName,
                                String name ) throws SAXException {
            current.finish();
            current = current.parentHandler();
        }

        @Override
        public void characters( char[] ch,
                                int start,
                                int length ) throws SAXException {
            String value = new String(ch, start, length);
            value = value.trim();
            if (value.length() == 0) return;
            // Create a 'jcr:xmltext' child node with a single 'jcr:xmlcharacters' property ...
            current = nodeHandlerFactory.createFor(JcrLexicon.XMLTEXT, current, uuidBehavior);
            current.addPropertyValue(JcrLexicon.PRIMARY_TYPE,
                                     stringFor(JcrNtLexicon.UNSTRUCTURED),
                                     false,
                                     PropertyType.NAME,
                                     DOCUMENT_VIEW_NAME_DECODER);
            current.addPropertyValue(JcrLexicon.XMLCHARACTERS, value, false, PropertyType.STRING, null);// don't decode value
            current.finish();
            // Pop the stack ...
            current = current.parentHandler();
        }
    }
    
    private class ReferenceProperty {
        private final Name name;
        private final Value[] values;
        private final boolean singleValued;

        protected ReferenceProperty(Name name, Value value) {
            assert name != null;
            this.name = name;

            assert value != null;
            this.values = new Value[] {value};
            this.singleValued = true;
        } 
        
        protected ReferenceProperty( Name name, Value[] values ) {
            assert name != null;
            this.name = name;
            assert values != null && values.length > 0;
            this.values = values;
            this.singleValued = false;
        }
        
        protected int type() {
            return value().getType();
        }
        
        protected Name name() {
            return name;
        }
        
        protected boolean isMultiple() {
            return !singleValued;
        }
        
        protected JcrValue value() {
            return (JcrValue)values[0];
        }
        
        protected Value[] values() {
            return values;
        }
    }
}
