/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.core;

import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import javax.jcr.AccessDeniedException;
import javax.jcr.Binary;
import javax.jcr.InvalidItemStateException;
import javax.jcr.InvalidLifecycleTransitionException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ItemVisitor;
import javax.jcr.NamespaceException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.ValueFormatException;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.lock.LockManager;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.PropertyDefinition;
import javax.jcr.query.Query;
import javax.jcr.query.QueryResult;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.VersionManager;

import org.apache.jackrabbit.commons.JcrUtils;
import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter;
import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter;
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.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException;
import org.apache.jackrabbit.core.nodetype.NodeTypeImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.query.QueryManagerImpl;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.QItemDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.conversion.NameException;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.apache.jackrabbit.util.ChildrenCollectorFilter;
import org.apache.jackrabbit.util.ISO9075;
import org.apache.jackrabbit.value.ValueHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ISCHECKEDOUT;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_LIFECYCLE_POLICY;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_CURRENT_LIFECYCLE_STATE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_LIFECYCLE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_REFERENCEABLE;
import org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl;
import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl;

/**
 * <code>NodeImpl</code> implements the <code>Node</code> interface.
 */
public class NodeImpl extends ItemImpl implements Node {

    private static Logger log = LoggerFactory.getLogger(NodeImpl.class);

    // flag set in status passed to getOrCreateProperty if property was created
    protected static final short CREATED = 0;

    /** node data (avoids casting <code>ItemImpl.data</code>) */
    private final AbstractNodeData data;

    /**
     * Protected constructor.
     *
     * @param itemMgr    the <code>ItemManager</code> that created this <code>Node</code> instance
     * @param session    the <code>Session</code> through which this <code>Node</code> is acquired
     * @param data       the node data
     */
    protected NodeImpl(ItemManager itemMgr, SessionImpl session, AbstractNodeData data) {
        super(itemMgr, session, data);
        this.data = data;
        // paranoid sanity check
        NodeTypeRegistry ntReg = session.getNodeTypeManager().getNodeTypeRegistry();
        final NodeState state = data.getNodeState();
        if (!ntReg.isRegistered(state.getNodeTypeName())) {
            /**
             * todo need proper way of handling inconsistent/corrupt node type references
             * e.g. 'flag' nodes that refer to non-registered node types
             */
            log.warn("Fallback to nt:unstructured due to unknown node type '"
                    + state.getNodeTypeName() + "' of " + this);
            data.getNodeState().setNodeTypeName(NameConstants.NT_UNSTRUCTURED);
        }
        List<Name> unknown = null;
        for (Name mixinName : state.getMixinTypeNames()) {
            if (!ntReg.isRegistered(mixinName)) {
                if (unknown == null) {
                    unknown = new ArrayList<Name>();
                }
                unknown.add(mixinName);
                log.warn("Ignoring unknown mixin type '" + mixinName +
                        "' of " + this);
            }
        }
        if (unknown != null) {
            // ignore unknown mixin type names
            Set<Name> known = new HashSet<Name>(state.getMixinTypeNames());
            known.removeAll(unknown);
            state.setMixinTypeNames(known);
        }
    }

    /**
     * Returns the node-state associated with this node.
     *
     * @return state associated with this node
     */
    NodeState getNodeState() {
        return data.getNodeState();
    }

    /**
     * Returns the id of the property at <code>relPath</code> or <code>null</code>
     * if no property exists at <code>relPath</code>.
     * <p/>
     * Note that access rights are not checked.
     *
     * @param relPath relative path of a (possible) property
     * @return the id of the property at <code>relPath</code> or
     *         <code>null</code> if no property exists at <code>relPath</code>
     * @throws RepositoryException if <code>relPath</code> is not a valid
     *                             relative path
     */
    protected PropertyId resolveRelativePropertyPath(String relPath)
            throws RepositoryException {
        try {
            /**
             * first check if relPath is just a name (in which case we don't
             * have to build & resolve absolute path)
             */
            if (relPath.indexOf('/') == -1) {
                Name propName = session.getQName(relPath);
                // check if property entry exists
                NodeState thisState = data.getNodeState();
                if (thisState.hasPropertyName(propName)) {
                    return new PropertyId(thisState.getNodeId(), propName);
                } else {
                    // there's no property with that name
                    return null;
                }
            }
            /**
             * build and resolve absolute path
             */
            Path p = PathFactoryImpl.getInstance().create(
                    getPrimaryPath(), session.getQPath(relPath), true);
            return session.getHierarchyManager().resolvePropertyPath(p);
        } catch (NameException e) {
            String msg = "failed to resolve path " + relPath + " relative to " + this;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * Returns the id of the node at <code>relPath</code> or <code>null</code>
     * if no node exists at <code>relPath</code>.
     * <p/>
     * Note that access rights are not checked.
     *
     * @param relPath relative path of a (possible) node
     * @return the id of the node at <code>relPath</code> or
     *         <code>null</code> if no node exists at <code>relPath</code>
     * @throws RepositoryException if <code>relPath</code> is not a valid
     *                             relative path
     */
    protected NodeId resolveRelativeNodePath(String relPath)
            throws RepositoryException {

        Path p = resolveRelativePath(relPath);
        return getNodeId(p);
    }

    /**
     * Resolve a relative path given as string into a <code>Path</code>. If
     * a <code>NameException</code> occurs, it will be rethrown embedded
     * into a <code>RepositoryException</code>
     *
     * @param relPath relative path
     * @return <code>Path</code> object
     * @throws RepositoryException if an error occurs
     */
    private Path resolveRelativePath(String relPath) throws RepositoryException {
        try {
            return session.getQPath(relPath);
        } catch (NameException e) {
            String msg = "failed to resolve path " + relPath + " relative to " + this;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * Returns the id of the node at <code>p</code> or <code>null</code>
     * if no node exists at <code>p</code>.
     * <p/>
     * Note that access rights are not checked.
     *
     * @param p relative path of a (possible) node
     * @return the id of the node at <code>p</code> or
     *         <code>null</code> if no node exists at <code>p</code>
     * @throws RepositoryException if <code>relPath</code> is not a valid
     *                             relative path
     */
    private NodeId getNodeId(Path p) throws RepositoryException {
        if (p.getLength() == 1) {
            Path.Element pe = p.getNameElement();
            if (pe.denotesName()) {
                // check if node entry exists
                NodeState thisState = data.getNodeState();
                int index = pe.getIndex();
                if (index == 0) {
                    index = 1;
                }
                ChildNodeEntry cne =
                        thisState.getChildNodeEntry(pe.getName(), index);
                if (cne != null) {
                    return cne.getId();
                } else {
                    // there's no child node with that name
                    return null;
                }
            }
        }
        /**
         * build and resolve absolute path
         */
        p = PathFactoryImpl.getInstance().create(getPrimaryPath(), p, true);
        return session.getHierarchyManager().resolveNodePath(p);
    }

    /**
     * Determines if there are pending unsaved changes either on <i>this</i>
     * node or on any node or property in the subtree below it.
     *
     * @return <code>true</code> if there are pending unsaved changes,
     *         <code>false</code> otherwise.
     * @throws RepositoryException if an error occurred
     */
    protected boolean hasPendingChanges() throws RepositoryException {
        if (isTransient()) {
            return true;
        }
        Iterator<ItemState> iter = stateMgr.getDescendantTransientItemStates((NodeId) id);
        return iter.hasNext();
    }

    protected synchronized ItemState getOrCreateTransientItemState()
            throws RepositoryException {

        synchronized (data) {
            if (!isTransient()) {
                try {
                    // make transient (copy-on-write)
                    NodeState transientState =
                            stateMgr.createTransientNodeState(
                                    data.getNodeState(), ItemState.STATUS_EXISTING_MODIFIED);
                    // replace persistent with transient state
                    data.setState(transientState);
                } catch (ItemStateException ise) {
                    String msg = "failed to create transient state";
                    log.debug(msg);
                    throw new RepositoryException(msg, ise);
                }
            }
            return getItemState();
        }
    }

    /**
     * @param name
     * @param type
     * @param multiValued
     * @param exactTypeMatch
     * @param status
     * @return
     * @throws ConstraintViolationException if no applicable property definition
     *                                      could be found
     * @throws RepositoryException          if another error occurs
     */
    protected PropertyImpl getOrCreateProperty(String name, int type,
                                               boolean multiValued,
                                               boolean exactTypeMatch,
                                               BitSet status)
            throws ConstraintViolationException, RepositoryException {
        try {
            return getOrCreateProperty(
                    session.getQName(name), type,
                    multiValued, exactTypeMatch, status);
        } catch (NameException e) {
            throw new RepositoryException("invalid property name: " + name, e);
        }
    }

    /**
     * @param name
     * @param type
     * @param multiValued
     * @param exactTypeMatch
     * @param status
     * @return
     * @throws ConstraintViolationException if no applicable property definition
     *                                      could be found
     * @throws RepositoryException          if another error occurs
     */
    protected synchronized PropertyImpl getOrCreateProperty(Name name, int type,
                                                            boolean multiValued,
                                                            boolean exactTypeMatch,
                                                            BitSet status)
            throws ConstraintViolationException, RepositoryException {
        status.clear();

        if (isNew() && !hasProperty(name)) {
            // this is a new node and the property does not exist yet
            // -> no need to check item manager
            PropertyDefinitionImpl def = getApplicablePropertyDefinition(
                    name, type, multiValued, exactTypeMatch);
            PropertyImpl prop = createChildProperty(name, type, def);
            status.set(CREATED);
            return prop;
        }


        /*
         * Please note, that this implementation does not win a price for beauty
         * or speed. It's never a good idea to use exceptions for semantical
         * control flow.
         * However, compared to the previous version, this one is thread save
         * and makes the test/get block atomic in respect to transactional
         * commits. the test/set can still fail.
         *
         * Old Version:

            NodeState thisState = (NodeState) state;
            if (thisState.hasPropertyName(name)) {
                /**
                 * the following call will throw ItemNotFoundException if the
                 * current session doesn't have read access
                 /
                return getProperty(name);
            }
            [...create block...]

        */
        try {
            PropertyId propId = new PropertyId(getNodeId(), name);
            return (PropertyImpl) itemMgr.getItem(propId);
        } catch (AccessDeniedException ade) {
            throw new ItemNotFoundException(name.toString());
        } catch (ItemNotFoundException e) {
            // does not exist yet:
            // find definition for the specified property and create property
            PropertyDefinitionImpl def = getApplicablePropertyDefinition(
                    name, type, multiValued, exactTypeMatch);
            PropertyImpl prop = createChildProperty(name, type, def);
            status.set(CREATED);
            return prop;
        }
    }

    /**
     * Creates a new property with the given name and <code>type</code> hint and
     * property definition. If the given property definition is not of type
     * <code>UNDEFINED</code>, then it takes precendence over the
     * <code>type</code> hint.
     *
     * @param name the name of the property to create.
     * @param type the type hint.
     * @param def  the associated property definition.
     * @return the property instance.
     * @throws RepositoryException if the property cannot be created.
     */
    protected synchronized PropertyImpl createChildProperty(Name name, int type,
                                                            PropertyDefinitionImpl def)
            throws RepositoryException {

        // create a new property state
        PropertyState propState;
        try {
            QPropertyDefinition propDef = def.unwrap();
            if (def.getRequiredType() != PropertyType.UNDEFINED) {
                type = def.getRequiredType();
            }
            propState =
                    stateMgr.createTransientPropertyState(getNodeId(), name,
                            ItemState.STATUS_NEW);
            propState.setType(type);
            propState.setMultiValued(propDef.isMultiple());
            // compute system generated values if necessary
            InternalValue[] genValues = session.getNodeTypeInstanceHandler()
                    .computeSystemGeneratedPropertyValues(data.getNodeState(), propDef);
            if (genValues == null) {
                genValues = InternalValue.create(propDef.getDefaultValues());
            }
            if (genValues != null) {
                propState.setValues(genValues);
            }
        } catch (ItemStateException ise) {
            String msg = "failed to add property " + name + " to " + this;
            log.debug(msg);
            throw new RepositoryException(msg, ise);
        }

        // create Property instance wrapping new property state
        // NOTE: since the property is not yet connected to its parent, avoid
        // calling ItemManager#getItem(ItemId) which may include a permission
        // check (with subsequent usage of the hierarachy-mgr -> error).
        // just let the mgr create the new property that is known to exist and
        // which has not been accessed before.
        PropertyImpl prop = (PropertyImpl) itemMgr.createItemInstance(propState);

        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        // add new property entry
        thisState.addPropertyName(name);

        return prop;
    }

    protected synchronized NodeImpl createChildNode(Name name,
                                                    NodeTypeImpl nodeType,
                                                    NodeId id)
            throws RepositoryException {
        // create a new node state
        NodeState nodeState;
        try {
            if (id == null) {
                id = new NodeId();
            }
            nodeState =
                    stateMgr.createTransientNodeState(id, nodeType.getQName(),
                            getNodeId(), ItemState.STATUS_NEW);
        } catch (ItemStateException ise) {
            String msg = "failed to add child node " + name + " to " + this;
            log.debug(msg);
            throw new RepositoryException(msg, ise);
        }

        // create Node instance wrapping new node state
        NodeImpl node;
        try {
            // NOTE: since the node is not yet connected to its parent, avoid
            // calling ItemManager#getItem(ItemId) which may include a permission
            // check (with subsequent usage of the hierarachy-mgr -> error).
            // just let the mgr create the new node that is known to exist and
            // which has not been accessed before.
            node = (NodeImpl) itemMgr.createItemInstance(nodeState);
        } catch (RepositoryException re) {
            // something went wrong
            stateMgr.disposeTransientItemState(nodeState);
            // re-throw
            throw re;
        }

        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        // add new child node entry
        thisState.addChildNodeEntry(name, nodeState.getNodeId());

        // add 'auto-create' properties defined in node type
        for (PropertyDefinition aPda : nodeType.getAutoCreatedPropertyDefinitions()) {
            PropertyDefinitionImpl pd = (PropertyDefinitionImpl) aPda;
            node.createChildProperty(pd.unwrap().getName(), pd.getRequiredType(), pd);
        }

        // recursively add 'auto-create' child nodes defined in node type
        for (NodeDefinition aNda : nodeType.getAutoCreatedNodeDefinitions()) {
            NodeDefinitionImpl nd = (NodeDefinitionImpl) aNda;
            node.createChildNode(nd.unwrap().getName(), (NodeTypeImpl) nd.getDefaultPrimaryType(), null);
        }

        return node;
    }

    protected void renameChildNode(Name oldName, int index, NodeId id,
                                   Name newName)
            throws RepositoryException {
        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        thisState.renameChildNodeEntry(oldName, index, newName);
    }

    protected void removeChildProperty(String propName) throws RepositoryException {
        try {
            removeChildProperty(session.getQName(propName));
        } catch (NameException e) {
            throw new RepositoryException(
                    "invalid property name: " + propName, e);
        }
    }

    protected void removeChildProperty(Name propName) throws RepositoryException {
        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();

        // remove the property entry
        if (!thisState.removePropertyName(propName)) {
            String msg = "failed to remove property " + propName + " of " + this;
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        // remove property
        PropertyId propId = new PropertyId(thisState.getNodeId(), propName);
        itemMgr.getItem(propId).setRemoved();
    }

    protected void removeChildNode(Name nodeName, int index)
            throws RepositoryException {
        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        if (index == 0) {
            index = 1;
        }
        ChildNodeEntry entry =
                thisState.getChildNodeEntry(nodeName, index);
        if (entry == null) {
            String msg = "failed to remove child " + nodeName + " of " + this;
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        // notify target of removal
        NodeId childId = entry.getId();
        NodeImpl childNode = itemMgr.getNode(childId, getNodeId());
        childNode.onRemove(getNodeId());

        // remove the child node entry
        if (!thisState.removeChildNodeEntry(nodeName, index)) {
            String msg = "failed to remove child " + nodeName + " of " + this;
            log.debug(msg);
            throw new RepositoryException(msg);
        }
    }

    protected void onRedefine(QNodeDefinition def) throws RepositoryException {
        NodeDefinitionImpl newDef =
                session.getNodeTypeManager().getNodeDefinition(def);
        // modify the state of 'this', i.e. the target node
        getOrCreateTransientItemState();
        // set new definition
        data.setDefinition(newDef);
    }

    protected void onRemove(NodeId parentId) throws RepositoryException {
        // modify the state of 'this', i.e. the target node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();

        // remove this node from its shared set
        if (thisState.isShareable()) {
            if (thisState.removeShare(parentId) > 0) {
                // this state is still connected to some parents, so
                // leave the child node entries and properties

                // set state of this instance to 'invalid'
                data.setStatus(STATUS_INVALIDATED);
                // notify the item manager that this instance has been
                // temporarily invalidated
                itemMgr.itemInvalidated(id, data);
                return;
            }
        }

        if (thisState.hasChildNodeEntries()) {
            // remove child nodes
            // use temp array to avoid ConcurrentModificationException
            ArrayList<ChildNodeEntry> tmp = new ArrayList<ChildNodeEntry>(thisState.getChildNodeEntries());
            // remove from tail to avoid problems with same-name siblings
            for (int i = tmp.size() - 1; i >= 0; i--) {
                ChildNodeEntry entry =
                        tmp.get(i);
                // recursively remove child node
                NodeId childId = entry.getId();
                //NodeImpl childNode = (NodeImpl) itemMgr.getItem(childId);
                NodeImpl childNode = itemMgr.getNode(childId, getNodeId());
                childNode.onRemove(thisState.getNodeId());
                // remove the child node entry
                thisState.removeChildNodeEntry(entry.getName(), entry.getIndex());
            }
        }

        // remove properties
        // use temp set to avoid ConcurrentModificationException
        HashSet<Name> tmp = new HashSet<Name>(thisState.getPropertyNames());
        for (Name propName : tmp) {
            // remove the property entry
            thisState.removePropertyName(propName);
            // remove property
            PropertyId propId = new PropertyId(thisState.getNodeId(), propName);
            itemMgr.getItem(propId).setRemoved();
        }

        // finally remove this node
        thisState.setParentId(null);
        setRemoved();
    }

    private void setMixinTypesProperty(Set<Name> mixinNames) throws RepositoryException {
        NodeState thisState = data.getNodeState();
        // get or create jcr:mixinTypes property
        PropertyImpl prop;
        if (thisState.hasPropertyName(NameConstants.JCR_MIXINTYPES)) {
            prop = (PropertyImpl) itemMgr.getItem(new PropertyId(thisState.getNodeId(), NameConstants.JCR_MIXINTYPES));
        } else {
            // find definition for the jcr:mixinTypes property and create property
            PropertyDefinitionImpl def = getApplicablePropertyDefinition(
                    NameConstants.JCR_MIXINTYPES, PropertyType.NAME, true, true);
            prop = createChildProperty(NameConstants.JCR_MIXINTYPES, PropertyType.NAME, def);
        }

        if (mixinNames.isEmpty()) {
            // purge empty jcr:mixinTypes property
            removeChildProperty(NameConstants.JCR_MIXINTYPES);
            return;
        }

        // call internalSetValue for setting the jcr:mixinTypes property
        // to avoid checking of the 'protected' flag
        InternalValue[] vals = new InternalValue[mixinNames.size()];
        Iterator<Name> iter = mixinNames.iterator();
        int cnt = 0;
        while (iter.hasNext()) {
            vals[cnt++] = InternalValue.create(iter.next());
        }
        prop.internalSetValue(vals, PropertyType.NAME);
    }

    /**
     * Returns the <code>Name</code>s of this node's mixin types.
     *
     * @return a set of the <code>Name</code>s of this node's mixin types.
     */
    public Set<Name> getMixinTypeNames() {
        return data.getNodeState().getMixinTypeNames();
    }

    /**
     * Returns the effective (i.e. merged and resolved) node type representation
     * of this node's primary and mixin node types.
     *
     * @return the effective node type
     * @throws RepositoryException if an error occurs
     */
    public EffectiveNodeType getEffectiveNodeType() throws RepositoryException {
        try {
            NodeTypeRegistry registry =
                session.getNodeTypeManager().getNodeTypeRegistry();
            return registry.getEffectiveNodeType(
                    data.getNodeState().getNodeTypeName(),
                    data.getNodeState().getMixinTypeNames());
        } catch (NodeTypeConflictException ntce) {
            String msg = "Failed to build effective node type for " + this;
            log.debug(msg);
            throw new RepositoryException(msg, ntce);
        }
    }

    /**
     * Returns the applicable child node definition for a child node with the
     * specified name and node type.
     *
     * @param nodeName
     * @param nodeTypeName
     * @return
     * @throws ConstraintViolationException if no applicable child node definition
     *                                      could be found
     * @throws RepositoryException          if another error occurs
     */
    protected NodeDefinitionImpl getApplicableChildNodeDefinition(Name nodeName,
                                                                  Name nodeTypeName)
            throws ConstraintViolationException, RepositoryException {
        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        QNodeDefinition cnd = getEffectiveNodeType().getApplicableChildNodeDef(
                nodeName, nodeTypeName, ntMgr.getNodeTypeRegistry());
        return ntMgr.getNodeDefinition(cnd);
    }

    /**
     * Returns the applicable property definition for a property with the
     * specified name and type.
     *
     * @param propertyName
     * @param type
     * @param multiValued
     * @param exactTypeMatch
     * @return
     * @throws ConstraintViolationException if no applicable property definition
     *                                      could be found
     * @throws RepositoryException          if another error occurs
     */
    protected PropertyDefinitionImpl getApplicablePropertyDefinition(Name propertyName,
                                                                     int type,
                                                                     boolean multiValued,
                                                                     boolean exactTypeMatch)
            throws ConstraintViolationException, RepositoryException {
        QPropertyDefinition pd;
        if (exactTypeMatch || type == PropertyType.UNDEFINED) {
            pd = getEffectiveNodeType().getApplicablePropertyDef(
                    propertyName, type, multiValued);
        } else {
            try {
                // try to find a definition with matching type first
                pd = getEffectiveNodeType().getApplicablePropertyDef(
                        propertyName, type, multiValued);
            } catch (ConstraintViolationException cve) {
                // none found, now try by ignoring the type
                pd = getEffectiveNodeType().getApplicablePropertyDef(
                        propertyName, PropertyType.UNDEFINED, multiValued);
            }
        }
        return session.getNodeTypeManager().getPropertyDefinition(pd);
    }

    protected void makePersistent() throws InvalidItemStateException {
        if (!isTransient()) {
            log.debug(this + " (" + id + "): there's no transient state to persist");
            return;
        }

        NodeState transientState = data.getNodeState();

        NodeState persistentState = (NodeState) transientState.getOverlayedState();
        if (persistentState == null) {
            // this node is 'new'
            persistentState = stateMgr.createNew(transientState);
        }

        synchronized (persistentState) {
            // check staleness of transient state first
            if (transientState.isStale()) {
                String msg =
                    this + ": the node cannot be saved because it has been"
                    + " modified externally.";
                log.debug(msg);
                throw new InvalidItemStateException(msg);
            }
            // copy state from transient state:
            // parent id's
            persistentState.setParentId(transientState.getParentId());
            // primary type
            persistentState.setNodeTypeName(transientState.getNodeTypeName());
            // mixin types
            persistentState.setMixinTypeNames(transientState.getMixinTypeNames());
            // child node entries
            persistentState.setChildNodeEntries(transientState.getChildNodeEntries());
            // property entries
            persistentState.setPropertyNames(transientState.getPropertyNames());
            // shared set
            persistentState.setSharedSet(transientState.getSharedSet());

            // make state persistent
            stateMgr.store(persistentState);
        }

        // tell state manager to disconnect item state
        stateMgr.disconnectTransientItemState(transientState);
        // swap transient state with persistent state
        data.setState(persistentState);
        // reset status
        data.setStatus(STATUS_NORMAL);

        if (isShareable() && data.getPrimaryParentId() == null) {
            data.setPrimaryParentId(persistentState.getParentId());
        }
    }

    protected void restoreTransient(NodeState transientState)
            throws RepositoryException {
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        if (transientState.getStatus() == ItemState.STATUS_NEW
                && thisState.getStatus() != ItemState.STATUS_NEW) {
            thisState.setStatus(ItemState.STATUS_NEW);
            stateMgr.disconnectTransientItemState(thisState);
        }
        // re-apply transient changes
        thisState.setParentId(transientState.getParentId());
        thisState.setNodeTypeName(transientState.getNodeTypeName());
        thisState.setMixinTypeNames(transientState.getMixinTypeNames());
        thisState.setChildNodeEntries(transientState.getChildNodeEntries());
        thisState.setPropertyNames(transientState.getPropertyNames());
        thisState.setSharedSet(transientState.getSharedSet());
    }

    /**
     * Same as {@link Node#addMixin(String)} except that it takes a
     * <code>Name</code> instead of a <code>String</code>.
     *
     * @see Node#addMixin(String)
     */
    public void addMixin(Name mixinName)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        // check state of this instance
        sanityCheck();

        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT
                | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD;
        int permissions = Permission.NODE_TYPE_MNGMT;
        // special handling of mix:(simple)versionable. since adding the mixin alters
        // the version storage jcr:versionManagement privilege is required
        // in addition.
        if (NameConstants.MIX_VERSIONABLE.equals(mixinName)
                || NameConstants.MIX_SIMPLE_VERSIONABLE.equals(mixinName)) {
            permissions |= Permission.VERSION_MNGMT;
        }
        session.getValidator().checkModify(this, options, permissions);

        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        NodeTypeImpl mixin = ntMgr.getNodeType(mixinName);
        if (!mixin.isMixin()) {
            throw new RepositoryException(mixinName + ": not a mixin node type");
        }

        final Name primaryTypeName = data.getNodeState().getNodeTypeName();
        NodeTypeImpl primaryType = ntMgr.getNodeType(primaryTypeName);
        if (primaryType.isDerivedFrom(mixinName)) {
            // new mixin is already included in primary type
            return;
        }

        // build effective node type of mixin's & primary type in order to detect conflicts
        NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry();
        EffectiveNodeType entExisting;
        try {
            // existing mixin's
            Set<Name> mixins = new HashSet<Name>(data.getNodeState().getMixinTypeNames());

            // build effective node type representing primary type including existing mixin's
            entExisting = ntReg.getEffectiveNodeType(primaryTypeName, mixins);
            if (entExisting.includesNodeType(mixinName)) {
                // new mixin is already included in existing mixin type(s)
                return;
            }

            // add new mixin
            mixins.add(mixinName);
            // try to build new effective node type (will throw in case of conflicts)
            ntReg.getEffectiveNodeType(primaryTypeName, mixins);
        } catch (NodeTypeConflictException e) {
            throw new ConstraintViolationException(e.getMessage(), e);
        }

        // do the actual modifications implied by the new mixin;
        // try to revert the changes in case an exception occurs
        try {
            // modify the state of this node
            NodeState thisState = (NodeState) getOrCreateTransientItemState();
            // add mixin name
            Set<Name> mixins = new HashSet<Name>(thisState.getMixinTypeNames());
            mixins.add(mixinName);
            thisState.setMixinTypeNames(mixins);

            // set jcr:mixinTypes property
            setMixinTypesProperty(mixins);

            // add 'auto-create' properties defined in mixin type
            for (PropertyDefinition aPda : mixin.getAutoCreatedPropertyDefinitions()) {
                PropertyDefinitionImpl pd = (PropertyDefinitionImpl) aPda;
                // make sure that the property is not already defined by primary type
                // or existing mixin's
                NodeTypeImpl declaringNT = (NodeTypeImpl) pd.getDeclaringNodeType();
                if (!entExisting.includesNodeType(declaringNT.getQName())) {
                    createChildProperty(pd.unwrap().getName(), pd.getRequiredType(), pd);
                }
            }

            // recursively add 'auto-create' child nodes defined in mixin type
            for (NodeDefinition aNda : mixin.getAutoCreatedNodeDefinitions()) {
                NodeDefinitionImpl nd = (NodeDefinitionImpl) aNda;
                // make sure that the child node is not already defined by primary type
                // or existing mixin's
                NodeTypeImpl declaringNT = (NodeTypeImpl) nd.getDeclaringNodeType();
                if (!entExisting.includesNodeType(declaringNT.getQName())) {
                    createChildNode(nd.unwrap().getName(), (NodeTypeImpl) nd.getDefaultPrimaryType(), null);
                }
            }
        } catch (RepositoryException re) {
            // try to undo the modifications by removing the mixin
            try {
                removeMixin(mixinName);
            } catch (RepositoryException re1) {
                // silently ignore & fall through
            }
            throw re;
        }
    }

    /**
     * Same as {@link Node#removeMixin(String)} except that it takes a
     * <code>Name</code> instead of a <code>String</code>.
     *
     * @see Node#removeMixin(String)
     */
    public void removeMixin(Name mixinName)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        // check state of this instance
        sanityCheck();

        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT
                | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD;
        int permissions = Permission.NODE_TYPE_MNGMT;
        session.getValidator().checkModify(this, options, permissions);

        // check if mixin is assigned
        final NodeState state = data.getNodeState();
        if (!state.getMixinTypeNames().contains(mixinName)) {
            throw new NoSuchNodeTypeException();
        }

        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry();

        // build effective node type of remaining mixin's & primary type
        Set<Name> remainingMixins = new HashSet<Name>(state.getMixinTypeNames());
        // remove name of target mixin
        remainingMixins.remove(mixinName);
        EffectiveNodeType entResulting;
        try {
            // build effective node type representing primary type including remaining mixin's
            entResulting = ntReg.getEffectiveNodeType(
                    state.getNodeTypeName(), remainingMixins);
        } catch (NodeTypeConflictException e) {
            throw new ConstraintViolationException(e.getMessage(), e);
        }

        /**
         * mix:referenceable needs special handling because it has
         * special semantics:
         * it can only be removed if there no more references to this node
         */
        NodeTypeImpl mixin = ntMgr.getNodeType(mixinName);
        if ((NameConstants.MIX_REFERENCEABLE.equals(mixinName)
                || mixin.isDerivedFrom(NameConstants.MIX_REFERENCEABLE))
                && !entResulting.includesNodeType(NameConstants.MIX_REFERENCEABLE)) {
            // removing this mixin would effectively remove mix:referenceable:
            // make sure no references exist
            PropertyIterator iter = getReferences();
            if (iter.hasNext()) {
                throw new ConstraintViolationException(mixinName + " can not be removed: the node is being referenced"
                        + " through at least one property of type REFERENCE");
            }
        }

        /*
         * mix:lockable: the mixin cannot be removed if the node is currently
         * locked even if the editing session is the lock holder.
         */
        if ((NameConstants.MIX_LOCKABLE.equals(mixinName)
                || mixin.isDerivedFrom(NameConstants.MIX_LOCKABLE))
                && !entResulting.includesNodeType(NameConstants.MIX_LOCKABLE)
                && isLocked()) {
            throw new ConstraintViolationException(mixinName + " can not be removed: the node is locked.");
        }


        // modify the state of this node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        thisState.setMixinTypeNames(remainingMixins);

        // set jcr:mixinTypes property
        setMixinTypesProperty(remainingMixins);

        // shortcut
        if (mixin.getChildNodeDefinitions().length == 0
                && mixin.getPropertyDefinitions().length == 0) {
            // the node type has neither property nor child node definitions,
            // i.e. we're done
            return;
        }

        // walk through properties and child nodes and remove those that aren't
        // accomodated by the resulting new effective node type (see JCR-2130)
        boolean success = false;
        try {
            // use temp set to avoid ConcurrentModificationException
            HashSet<Name> set = new HashSet<Name>(thisState.getPropertyNames());
            for (Name propName : set) {
                PropertyState propState = (PropertyState) stateMgr.getItemState(new PropertyId(thisState.getNodeId(), propName));
                // check if property has been defined by mixin type (or one of its supertypes)
                PropertyDefinition def = itemMgr.getDefinition(propState);
                NodeTypeImpl declaringNT = (NodeTypeImpl) def.getDeclaringNodeType();
                if (!entResulting.includesNodeType(declaringNT.getQName())) {
                    // the resulting effective node type doesn't include the
                    // node type that declared this property

                    // try to find new applicable definition first and
                    // redefine property if possible (JCR-2130)
                    try {
                        PropertyImpl prop = (PropertyImpl) itemMgr.getItem(propState.getId());
                        if (prop.getDefinition().isProtected()) {
                            // remove 'orphaned' protected properties immediately
                            removeChildProperty(propName);
                            continue;
                        }
                        PropertyDefinitionImpl pdi = getApplicablePropertyDefinition(
                                propName, propState.getType(),
                                propState.isMultiValued(), false);
                        if (pdi.getRequiredType() != PropertyType.UNDEFINED
                                && pdi.getRequiredType() != propState.getType()) {
                            // value conversion required
                            if (propState.isMultiValued()) {
                                // convert value
                                Value[] values =
                                        ValueHelper.convert(
                                                prop.getValues(),
                                                pdi.getRequiredType(),
                                                session.getValueFactory());
                                // redefine property
                                prop.onRedefine(pdi.unwrap());
                                // set converted values
                                prop.setValue(values);
                            } else {
                                // convert value
                                Value value =
                                        ValueHelper.convert(
                                                prop.getValue(),
                                                pdi.getRequiredType(),
                                                session.getValueFactory());
                                // redefine property
                                prop.onRedefine(pdi.unwrap());
                                // set converted values
                                prop.setValue(value);
                            }
                        } else {
                            // redefine property
                            prop.onRedefine(pdi.unwrap());
                        }
                    } catch (ValueFormatException vfe) {
                        // value conversion failed, remove it
                        removeChildProperty(propName);
                    } catch (ConstraintViolationException cve) {
                        // no suitable definition found for this property,
                        // remove it
                        removeChildProperty(propName);
                    }
                }
            }
            // use temp array to avoid ConcurrentModificationException
            ArrayList<ChildNodeEntry> list = new ArrayList<ChildNodeEntry>(thisState.getChildNodeEntries());
            // start from tail to avoid problems with same-name siblings
            for (int i = list.size() - 1; i >= 0; i--) {
                ChildNodeEntry entry = list.get(i);
                NodeState nodeState = (NodeState) stateMgr.getItemState(entry.getId());
                NodeDefinition def = itemMgr.getDefinition(nodeState);
                // check if node has been defined by mixin type (or one of its supertypes)
                NodeTypeImpl declaringNT = (NodeTypeImpl) def.getDeclaringNodeType();
                if (!entResulting.includesNodeType(declaringNT.getQName())) {
                    // the resulting effective node type doesn't include the
                    // node type that declared this child node

                    try {
                        NodeImpl node = (NodeImpl) itemMgr.getItem(nodeState.getId());
                        if (node.getDefinition().isProtected()) {
                            // remove 'orphaned' protected child node immediately
                            removeChildNode(entry.getName(), entry.getIndex());
                            continue;
                        }
                        NodeDefinitionImpl ndi = getApplicableChildNodeDefinition(
                                entry.getName(),
                                nodeState.getNodeTypeName());
                        // redefine node
                        node.onRedefine(ndi.unwrap());
                    } catch (ConstraintViolationException cve) {
                        // no suitable definition found for this child node,
                        // remove it
                        removeChildNode(entry.getName(), entry.getIndex());
                    }
                }
            }
            success = true;
        } catch (ItemStateException e) {
            throw new RepositoryException("Failed to clean up child items defined by removed mixin " + session.getJCRName(mixinName));
        } finally {
            if (!success) {
                // TODO JCR-1914: revert changes made to jcr:mixinTypes
            }
        }
    }

    /**
     * Same as {@link Node#isNodeType(String)} except that it takes a
     * <code>Name</code> instead of a <code>String</code>.
     *
     * @param ntName name of node type
     * @return <code>true</code> if this node is of the specified node type;
     *         otherwise <code>false</code>
     */
    public boolean isNodeType(Name ntName) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        // first do trivial checks without using type hierarchy
        Name primary = data.getNodeState().getNodeTypeName();
        if (ntName.equals(primary)) {
            return true;
        }
        Set<Name> mixins = data.getNodeState().getMixinTypeNames();
        if (mixins.contains(ntName)) {
            return true;
        }

        // check effective node type
        try {
            NodeTypeRegistry registry =
                session.getNodeTypeManager().getNodeTypeRegistry();
            EffectiveNodeType type =
                registry.getEffectiveNodeType(primary, mixins);
            return type.includesNodeType(ntName);
        } catch (NodeTypeConflictException e) {
            String msg = "Failed to build effective node type for " + this;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * Checks various pre-conditions that are common to all
     * <code>setProperty()</code> methods. The checks performed are:
     * <ul>
     * <li>this node must be checked-out</li>
     * <li>this node must not be locked by somebody else</li>
     * </ul>
     * Note that certain checks are performed by the respective
     * <code>Property.setValue()</code> methods.
     *
     * @throws VersionException    if this node is not checked-out
     * @throws LockException       if this node is locked by somebody else
     * @throws RepositoryException if another error occurs
     * @see javax.jcr.Node#setProperty
     */
    protected void checkSetProperty()
            throws VersionException, LockException, RepositoryException {
        // make sure this node is checked-out and is not locked
        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT;
        session.getValidator().checkModify(this, options, Permission.NONE);
    }

    /**
     * Sets the internal value of a property without checking any constraints.
     * <p/>
     * Note that no type conversion is being performed, i.e. it's the caller's
     * responsibility to make sure that the type of the given value is compatible
     * with the specified property's definition.
     * @param name
     * @param value
     * @return
     * @throws ValueFormatException
     * @throws RepositoryException
     */
    protected Property internalSetProperty(Name name, InternalValue value)
            throws ValueFormatException, RepositoryException {
        int type;
        if (value == null) {
            type = PropertyType.UNDEFINED;
        } else {
            type = value.getType();
        }

        BitSet status = new BitSet();
        PropertyImpl prop = getOrCreateProperty(name, type, false, true, status);
        try {
            if (value == null) {
                prop.internalSetValue(null, type);
            } else {
                prop.internalSetValue(new InternalValue[]{value}, type);
            }
        } catch (RepositoryException re) {
            if (status.get(CREATED)) {
                // setting value failed, get rid of newly created property
                removeChildProperty(name);
            }
            // rethrow
            throw re;
        }
        return prop;
    }

    /**
     * Sets the internal value of a property without checking any constraints.
     * <p/>
     * Note that no type conversion is being performed, i.e. it's the caller's
     * responsibility to make sure that the type of the given values is compatible
     * with the specified property's definition.
     *
     * @param name
     * @param values
     * @return
     * @throws ValueFormatException
     * @throws RepositoryException
     */
    protected Property internalSetProperty(Name name, InternalValue[] values)
            throws ValueFormatException, RepositoryException {
        int type;
        if (values == null || values.length == 0
                || values[0] == null) {
            type = PropertyType.UNDEFINED;
        } else {
            type = values[0].getType();
        }
        return internalSetProperty(name, values, type);
    }

    /**
     * Sets the internal value of a property without checking any constraints.
     * <p/>
     * Note that no type conversion is being performed, i.e. it's the caller's
     * responsibility to make sure that the type of the given values is compatible
     * with the specified property's definition.
     *
     * @param name
     * @param values
     * @param type
     * @return
     * @throws ValueFormatException
     * @throws RepositoryException
     */
    protected Property internalSetProperty(Name name, InternalValue[] values,
                                           int type)
            throws ValueFormatException, RepositoryException {
        BitSet status = new BitSet();
        PropertyImpl prop = getOrCreateProperty(name, type, true, true, status);
        try {
            prop.internalSetValue(values, type);
        } catch (RepositoryException re) {
            if (status.get(CREATED)) {
                // setting value failed, get rid of newly created property
                removeChildProperty(name);
            }
            // rethrow
            throw re;
        }
        return prop;
    }

    /**
     * Returns the child node of <code>this</code> node with the specified
     * <code>name</code>.
     *
     * @param name The name of the child node to retrieve.
     * @return The child node with the specified <code>name</code>.
     * @throws ItemNotFoundException If no child node exists with the
     *                               specified name.
     * @throws RepositoryException   If another error occurs.
     */
    public NodeImpl getNode(Name name) throws ItemNotFoundException, RepositoryException {
        return getNode(name, 1);
    }

    /**
     * Returns the child node of <code>this</code> node with the specified
     * <code>name</code>.
     *
     * @param name The name of the child node to retrieve.
     * @param index The index of the child node to retrieve (in the case of same-name siblings).
     * @return The child node with the specified <code>name</code>.
     * @throws ItemNotFoundException If no child node exists with the
     *                               specified name.
     * @throws RepositoryException   If another error occurs.
     */
    public NodeImpl getNode(Name name, int index)
            throws ItemNotFoundException, RepositoryException {
        // check state of this instance
        sanityCheck();

        NodeState thisState = data.getNodeState();
        if (index == 0) {
            index = 1;
        }
        ChildNodeEntry cne = thisState.getChildNodeEntry(name, index);
        if (cne == null) {
            throw new ItemNotFoundException();
        }
        try {
            return itemMgr.getNode(cne.getId(), getNodeId());
        } catch (AccessDeniedException ade) {
            throw new ItemNotFoundException();
        }
    }

    /**
     * Indicates whether a child node with the specified <code>name</code> exists.
     * Returns <code>true</code> if the child node exists and <code>false</code>
     * otherwise.
     *
     * @param name The name of the child node.
     * @return <code>true</code> if the child node exists; <code>false</code> otherwise.
     * @throws RepositoryException If an unspecified error occurs.
     */
    public boolean hasNode(Name name) throws RepositoryException {
        return hasNode(name, 1);
    }

    /**
     * Indicates whether a child node with the specified <code>name</code> exists.
     * Returns <code>true</code> if the child node exists and <code>false</code>
     * otherwise.
     *
     * @param name The name of the child node.
     * @param index The index of the child node (in the case of same-name siblings).
     * @return <code>true</code> if the child node exists; <code>false</code> otherwise.
     * @throws RepositoryException If an unspecified error occurs.
     */
    public boolean hasNode(Name name, int index) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        NodeState thisState = data.getNodeState();
        if (index == 0) {
            index = 1;
        }
        ChildNodeEntry cne = thisState.getChildNodeEntry(name, index);
        if (cne == null) {
            return false;
        }
        return itemMgr.itemExists(cne.getId());
    }

    /**
     * Returns the property of <code>this</code> node with the specified
     * <code>name</code>.
     *
     * @param name The name of the property to retrieve.
     * @return The property with the specified <code>name</code>.
     * @throws ItemNotFoundException If no property exists with the
     *                               specified name.
     * @throws RepositoryException   If another error occurs.
     */
    public PropertyImpl getProperty(Name name)
            throws ItemNotFoundException, RepositoryException {
        // check state of this instance
        sanityCheck();

        PropertyId propId = new PropertyId(getNodeId(), name);
        try {
            return (PropertyImpl) itemMgr.getItem(propId);
        } catch (AccessDeniedException ade) {
            throw new ItemNotFoundException(name.toString());
        }
    }

    /**
     * Indicates whether a property with the specified <code>name</code> exists.
     * Returns <code>true</code> if the property exists and <code>false</code>
     * otherwise.
     *
     * @param name The name of the property.
     * @return <code>true</code> if the property exists; <code>false</code> otherwise.
     * @throws RepositoryException If an unspecified error occurs.
     */
    public boolean hasProperty(Name name) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        NodeState thisState = data.getNodeState();
        if (!thisState.hasPropertyName(name)) {
            return false;
        }
        PropertyId propId = new PropertyId(thisState.getNodeId(), name);

        return itemMgr.itemExists(propId);
    }

    /**
     * Same as <code>{@link Node#addNode(String, String)}</code> except that
     * this method takes <code>Name</code> arguments instead of
     * <code>String</code>s and has an additional <code>uuid</code> argument.
     * <p/>
     * <b>Important Notice:</b> This method is for internal use only! Passing
     * already assigned uuid's might lead to unexpected results and
     * data corruption in the worst case.
     *
     * @param nodeName     name of the new node
     * @param nodeTypeName name of the new node's node type or <code>null</code>
     *                     if it should be determined automatically
     * @param id           id of the new node or <code>null</code> if a new
     *                     id should be assigned
     * @return the newly added node
     * @throws RepositoryException if the node can not added
     */
    public synchronized NodeImpl addNode(
            Name nodeName, Name nodeTypeName, NodeId id)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        Path nodePath = PathFactoryImpl.getInstance().create(
                getPrimaryPath(), nodeName, true);

        // Check the explicitly specified node type (if any)
        NodeTypeImpl nt = null;
        if (nodeTypeName != null) {
            nt = session.getNodeTypeManager().getNodeType(nodeTypeName);
            if (nt.isMixin()) {
                throw new ConstraintViolationException(
                        "Unable to add a node with a mixin node type: "
                        + session.getJCRName(nodeTypeName));
            } else if (nt.isAbstract()) {
                throw new ConstraintViolationException(
                        "Unable to add a node with an abstract node type: "
                        + session.getJCRName(nodeTypeName));
            } else {
                // adding a node with explicit specifying the node type name
                // requires the editing session to have nt_management privilege.
                session.getAccessManager().checkPermission(
                        nodePath, Permission.NODE_TYPE_MNGMT);
            }
        }

        // Get the applicable child node definition for this node.
        NodeDefinitionImpl def;
        try {
            def = getApplicableChildNodeDefinition(nodeName, nodeTypeName);
        } catch (RepositoryException e) {
            throw new ConstraintViolationException(
                    "No child node definition for "
                    + session.getJCRName(nodeName) + " found in " + this, e);
        }

        // Use default node type from child node definition if needed
        if (nt == null) {
            nt = (NodeTypeImpl) def.getDefaultPrimaryType();
        }

        // check for name collisions
        NodeState thisState = data.getNodeState();
        ChildNodeEntry cne = thisState.getChildNodeEntry(nodeName, 1);
        if (cne != null) {
            // there's already a child node entry with that name;
            // check same-name sibling setting of new node
            if (!def.allowsSameNameSiblings()) {
                throw new ItemExistsException(
                        "This node already exists: "
                        + itemMgr.safeGetJCRPath(nodePath));
            }
            // check same-name sibling setting of existing node
            NodeImpl existing = itemMgr.getNode(cne.getId(), getNodeId());
            if (!existing.getDefinition().allowsSameNameSiblings()) {
                throw new ItemExistsException(
                        "Same-name siblings not allowed for " + existing);
            }
        }

        // check protected flag of parent (i.e. this) node and retention/hold
        // make sure this node is checked-out and not locked by another session.
        int options =
            ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT
            | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD
            | ItemValidator.CHECK_RETENTION;
        session.getValidator().checkModify(this, options, Permission.NONE);

        // now do create the child node
        return createChildNode(nodeName, nt, id);
    }

    /**
     * Same as <code>{@link Node#setProperty(String, Value[])}</code> except that
     * this method takes a <code>Name</code> name argument instead of a
     * <code>String</code>.
     *
     * @param name
     * @param values
     * @return
     * @throws ValueFormatException
     * @throws VersionException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws RepositoryException
     */
    public PropertyImpl setProperty(Name name, Value[] values)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        int type = PropertyType.UNDEFINED;
        if (values != null) {
            for (Value v : values) {
                // use the type of the first value
                if (v != null) {
                    type = v.getType();
                    break;
                }
            }
        }

        return setProperty(name, values, type, false);
    }

    /**
     * Same as <code>{@link Node#setProperty(String, Value[], int)}</code> except
     * that this method takes a <code>Name</code> name argument instead of a
     * <code>String</code>.
     *
     * @param name
     * @param values
     * @param type
     * @return
     * @throws ValueFormatException
     * @throws VersionException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws RepositoryException
     */
    public PropertyImpl setProperty(Name name, Value[] values, int type)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        return setProperty(name, values, type, true);
    }

    /**
     * Same as <code>{@link Node#setProperty(String, Value)}</code> except that
     * this method takes a <code>Name</code> name argument instead of a
     * <code>String</code>.
     *
     * @param name
     * @param value
     * @return
     * @throws ValueFormatException
     * @throws VersionException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws RepositoryException
     */
    public PropertyImpl setProperty(Name name, Value value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        return setProperty(name, value, false);
    }

    /**
     * @see ItemImpl#getQName()
     */
    public Name getQName() throws RepositoryException {
        HierarchyManager hierMgr = session.getHierarchyManager();
        Name name;

        if (!isShareable()) {
            name = hierMgr.getName(id);
        } else {
            name = hierMgr.getName(getNodeId(), getParentId());
        }
        return name;
    }

    /**
     * Returns the identifier of this <code>Node</code>.
     *
     * @return the id of this <code>Node</code>
     */
    public NodeId getNodeId() {
        return (NodeId) id;
    }

    /**
     * Same as <code>{@link Node#orderBefore(String, String)}</code> except that
     * this method takes a <code>Path.Element</code> arguments instead of
     * <code>String</code>s.
     *
     * @param srcName
     * @param dstName
     * @throws UnsupportedRepositoryOperationException
     * @throws VersionException
     * @throws ConstraintViolationException
     * @throws ItemNotFoundException
     * @throws LockException
     * @throws RepositoryException
     */
    public synchronized void orderBefore(Path.Element srcName,
                                         Path.Element dstName)
            throws UnsupportedRepositoryOperationException, VersionException,
            ConstraintViolationException, ItemNotFoundException, LockException,
            RepositoryException {

        // check state of this instance
        sanityCheck();

        if (!getPrimaryNodeType().hasOrderableChildNodes()) {
            throw new UnsupportedRepositoryOperationException(
                    "child node ordering not supported on " + this);
        }

        // check arguments
        if (srcName.equals(dstName)) {
            // there's nothing to do
            return;
        }

        // check existence
        if (!hasNode(srcName.getName(), srcName.getIndex())) {
            String name;
            try {
                Path.Element[] path = new Path.Element[] { srcName };
                name = session.getJCRPath(new PathBuilder(path).getPath());
            } catch (NameException e) {
                name = srcName.toString();
            } catch (NamespaceException e) {
                name = srcName.toString();
            }
            throw new ItemNotFoundException(
                    this + " has no child node with name " + name);
        }
        if (dstName != null && !hasNode(dstName.getName(), dstName.getIndex())) {
            String name;
            try {
                Path.Element[] path = new Path.Element[] { dstName };
                name = session.getJCRPath(new PathBuilder(path).getPath());
            } catch (NameException e) {
                name = dstName.toString();
            } catch (NamespaceException e) {
                name = dstName.toString();
            }
            throw new ItemNotFoundException(
                    this + " has no child node with name " + name);
        }

        // make sure this node is checked-out and neither protected nor locked
        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT
                | ItemValidator.CHECK_CONSTRAINTS;
        session.getValidator().checkModify(this, options, Permission.NONE);

        ArrayList<ChildNodeEntry> list = new ArrayList<ChildNodeEntry>(data.getNodeState().getChildNodeEntries());
        int srcInd = -1, destInd = -1;
        for (int i = 0; i < list.size(); i++) {
            ChildNodeEntry entry = list.get(i);
            if (srcInd == -1) {
                if (entry.getName().equals(srcName.getName())
                        && (entry.getIndex() == srcName.getIndex()
                        || srcName.getIndex() == 0 && entry.getIndex() == 1)) {
                    srcInd = i;
                }
            }
            if (destInd == -1 && dstName != null) {
                if (entry.getName().equals(dstName.getName())
                        && (entry.getIndex() == dstName.getIndex()
                        || dstName.getIndex() == 0 && entry.getIndex() == 1)) {
                    destInd = i;
                    if (srcInd != -1) {
                        break;
                    }
                }
            } else {
                if (srcInd != -1) {
                    break;
                }
            }
        }

        // check if resulting order would be different to current order
        if (destInd == -1) {
            if (srcInd == list.size() - 1) {
                // no change, we're done
                return;
            }
        } else {
            if ((destInd - srcInd) == 1) {
                // no change, we're done
                return;
            }
        }

        // reorder list
        if (destInd == -1) {
            list.add(list.remove(srcInd));
        } else {
            if (srcInd < destInd) {
                list.add(destInd, list.get(srcInd));
                list.remove(srcInd);
            } else {
                list.add(destInd, list.remove(srcInd));
            }
        }

        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        thisState.setChildNodeEntries(list);
    }

    /**
     * Replaces the child node with the specified <code>id</code>
     * by a new child node with the same id and specified <code>nodeName</code>,
     * <code>nodeTypeName</code> and <code>mixinNames</code>.
     *
     * @param id           id of the child node to be replaced
     * @param nodeName     name of the new node
     * @param nodeTypeName name of the new node's node type
     * @param mixinNames   name of the new node's mixin types
     *
     * @return the new child node replacing the existing child
     * @throws ItemNotFoundException
     * @throws NoSuchNodeTypeException
     * @throws VersionException
     * @throws ConstraintViolationException
     * @throws LockException
     * @throws RepositoryException
     */
    public synchronized NodeImpl replaceChildNode(NodeId id, Name nodeName,
                                                  Name nodeTypeName,
                                                  Name[] mixinNames)
            throws ItemNotFoundException, NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        // check state of this instance
        sanityCheck();

        Node existing = (Node) itemMgr.getItem(id);

        // 'replace' is actually a 'remove existing/add new' operation;
        // this unfortunately changes the order of this node's
        // child node entries (JCR-1055);
        // => backup list of child node entries beforehand in order
        // to restore it afterwards
        NodeState state = data.getNodeState();
        ChildNodeEntry cneExisting = state.getChildNodeEntry(id);
        if (cneExisting == null) {
            throw new ItemNotFoundException(
                    this + ": no child node entry with id " + id);
        }
        List<ChildNodeEntry> cneList = new ArrayList<ChildNodeEntry>(state.getChildNodeEntries());

        // remove existing
        existing.remove();

        // create new child node
        NodeImpl node = addNode(nodeName, nodeTypeName, id);
        if (mixinNames != null) {
            for (Name mixinName : mixinNames) {
                node.addMixin(mixinName);
            }
        }

        // fetch <code>state</code> again, as it changed while removing child
        state = data.getNodeState();

        // restore list of child node entries (JCR-1055)
        if (cneExisting.getName().equals(nodeName)) {
            // restore original child node list
            state.setChildNodeEntries(cneList);
        } else {
            // replace child node entry with different name
            // but preserving original position
            state.removeAllChildNodeEntries();
            for (ChildNodeEntry cne : cneList) {
                if (cne.getId().equals(id)) {
                    // replace entry with different name
                    state.addChildNodeEntry(nodeName, id);
                } else {
                    state.addChildNodeEntry(cne.getName(), cne.getId());
                }
            }
        }

        return node;
    }

    /**
     * Create a child node that is a clone of a shareable node.
     *
     * @param src shareable source node
     * @param name name of new node
     * @return child node
     * @throws ItemExistsException if there already is a child node with the
     *             name given and the definition does not allow creating another one
     * @throws VersionException if this node is not checked out
     * @throws ConstraintViolationException if no definition is found in this
     *             node that would allow creating the child node
     * @throws LockException if this node is locked
     * @throws RepositoryException if some other error occurs
     */
    public synchronized NodeImpl clone(NodeImpl src, Name name)
            throws ItemExistsException, VersionException,
                   ConstraintViolationException, LockException,
                   RepositoryException {

        Path nodePath;
        try {
            nodePath = PathFactoryImpl.getInstance().create(getPrimaryPath(), name, true);
        } catch (MalformedPathException e) {
            // should never happen
            String msg = "internal error: invalid path " + this;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }

        // (1) make sure that parent node is checked-out
        // (2) check lock status
        // (3) check protected flag of parent (i.e. this) node
        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_CONSTRAINTS;
        session.getValidator().checkModify(this, options, Permission.NONE);

        // (4) check for name collisions
        NodeDefinitionImpl def;
        try {
            def = getApplicableChildNodeDefinition(name, null);
        } catch (RepositoryException re) {
            String msg = "no definition found in parent node's node type for new node";
            log.debug(msg);
            throw new ConstraintViolationException(msg, re);
        }
        NodeState thisState = data.getNodeState();
        ChildNodeEntry cne = thisState.getChildNodeEntry(name, 1);
        if (cne != null) {
            // there's already a child node entry with that name;
            // check same-name sibling setting of new node
            if (!def.allowsSameNameSiblings()) {
                throw new ItemExistsException(itemMgr.safeGetJCRPath(nodePath));
            }
            // check same-name sibling setting of existing node
            NodeId newId = cne.getId();
            if (!((NodeImpl) itemMgr.getItem(newId)).getDefinition().allowsSameNameSiblings()) {
                throw new ItemExistsException(itemMgr.safeGetJCRPath(nodePath));
            }
        }

        // (5) do clone operation
        NodeId parentId = getNodeId();
        src.addShareParent(parentId);

        // (6) modify the state of 'this', i.e. the parent node
        NodeId srcId = src.getNodeId();
        thisState = (NodeState) getOrCreateTransientItemState();
        // add new child node entry
        thisState.addChildNodeEntry(name, srcId);

        return itemMgr.getNode(srcId, parentId);
    }

    // -----------------------------------------------------------------< Item >
    /**
     * {@inheritDoc}
     */
    public boolean isNode() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public String getName() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        final NodeState state = data.getNodeState();
        if (state.getParentId() == null) {
            // this is the root node
            return "";
        }

        HierarchyManager hierMgr = session.getHierarchyManager();
        Name name;

        if (!isShareable()) {
            name = hierMgr.getName(id);
        } else {
            name = hierMgr.getName(getNodeId(), getParentId());
        }
        return session.getJCRName(name);
    }

    /**
     * {@inheritDoc}
     */
    public void accept(ItemVisitor visitor) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        visitor.visit(this);
    }

    /**
     * {@inheritDoc}
     */
    public Node getParent()
            throws ItemNotFoundException, AccessDeniedException, RepositoryException {
        // check state of this instance
        sanityCheck();

        // check if root node
        NodeId parentId = getParentId();
        if (parentId == null) {
            String msg = "root node doesn't have a parent";
            log.debug(msg);
            throw new ItemNotFoundException(msg);
        }
        return (Node) itemMgr.getItem(parentId);
    }

    //----------------------------------------------------------------< Node >

    /**
     * {@inheritDoc}
     */
    public Node addNode(String relPath) throws RepositoryException {
        return addNodeWithUuid(relPath, null, null);
    }

    /**
     * {@inheritDoc}
     */
    public Node addNode(String relPath, String nodeTypeName)
            throws RepositoryException {
        return addNodeWithUuid(relPath, nodeTypeName, null);
    }

    /**
     * Adds a node with the given UUID. You can only add a node with a UUID
     * that is not already assigned to another node in this workspace.
     *
     * @since Apache Jackrabbit 1.6
     * @see <a href="https://issues.apache.org/jira/browse/JCR-1972">JCR-1972</a>
     * @see Node#addNode(String)
     * @param relPath path of the new node
     * @param uuid    UUID of the new node,
     *                or <code>null</code> for a random new UUID
     * @return the newly added node
     * @throws RepositoryException if the node can not be added
     */
    public Node addNodeWithUuid(String relPath, String uuid)
            throws RepositoryException {
        return addNodeWithUuid(relPath, null, uuid);
    }

    /**
     * Adds a node with the given node type and UUID. You can only add a node
     * with a UUID that is not already assigned to another node in this
     * workspace.
     *
     * @since Apache Jackrabbit 1.6
     * @see <a href="https://issues.apache.org/jira/browse/JCR-1972">JCR-1972</a>
     * @see Node#addNode(String, String)
     * @param relPath      path of the new node
     * @param nodeTypeName name of the new node's node type,
     *                     or <code>null</code> for automatic type assignment
     * @param uuid         UUID of the new node,
     *                     or <code>null</code> for a random new UUID
     * @return the newly added node
     * @throws RepositoryException if the node can not be added
     */
    public synchronized Node addNodeWithUuid(
            String relPath, String nodeTypeName, String uuid)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        // Get the canonical path of the new node
        Path path;
        try {
            path = PathFactoryImpl.getInstance().create(
                    getPrimaryPath(), session.getQPath(relPath), true);
        } catch (NameException e) {
            throw new RepositoryException(
                    "Failed to resolve path " + relPath
                    + " relative to " + this, e);
        }

        // Get the last path element and check that it's a simple name
        Path.Element last = path.getNameElement();
        if (!last.denotesName() || last.getIndex() != 0) {
            throw new RepositoryException(
                    "Invalid last path element for adding node "
                    + relPath + " relative to " + this);
        }

        // Get the parent node instance
        NodeImpl parentNode;
        Path parentPath = path.getAncestor(1);
        try {
            parentNode = itemMgr.getNode(parentPath);
        } catch (PathNotFoundException e) {
            if (itemMgr.propertyExists(parentPath)) {
                throw new ConstraintViolationException(
                        "Unable to add a child node to property "
                        + session.getJCRPath(parentPath));
            }
            throw e;
        } catch (AccessDeniedException ade) {
            throw new PathNotFoundException(
                    "Failed to resolve path " + relPath + " relative to " + this);
        }

        // Resolve node type name (if any)
        Name typeName = null;
        if (nodeTypeName != null) {
            typeName = session.getQName(nodeTypeName);
        }

        // Check that the given UUID (if any) does not already exist
        NodeId id = null;
        if (uuid != null) {
            id = new NodeId(uuid);
            if (itemMgr.itemExists(id)) {
                throw new ItemExistsException(
                        "A node with this UUID already exists: " + uuid);
            }
        }

        return parentNode.addNode(last.getName(), typeName, id);
    }

    /**
     * {@inheritDoc}
     */
    public void orderBefore(String srcName, String destName)
            throws UnsupportedRepositoryOperationException, VersionException,
            ConstraintViolationException, ItemNotFoundException, LockException,
            RepositoryException {

        Path.Element insertName;
        try {
            Path p = session.getQPath(srcName);
            // p must be a relative path of length==depth==1 (to eliminate e.g. "..")
            if (p.isAbsolute() || p.getLength() != 1 || p.getDepth() != 1) {
                throw new RepositoryException("invalid name: " + srcName);
            }
            insertName = p.getNameElement();
        } catch (NameException e) {
            String msg = "invalid name: " + srcName;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }

        Path.Element beforeName;
        if (destName != null) {
            try {
                Path p = session.getQPath(destName);
                // p must be a relative path of length==depth==1 (to eliminate e.g. "..")
                if (p.isAbsolute() || p.getLength() != 1 || p.getDepth() != 1) {
                    throw new RepositoryException("invalid name: " + destName);
                }
                beforeName = p.getNameElement();
            } catch (NameException e) {
                String msg = "invalid name: " + destName;
                log.debug(msg);
                throw new RepositoryException(msg, e);
            }
        } else {
            beforeName = null;
        }

        orderBefore(insertName, beforeName);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Value[] values)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        return setProperty(session.getQName(name), values);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Value[] values, int type)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        return setProperty(session.getQName(name), values, type);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, String[] values)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value[] v = null;
        if (values != null) {
            v = ValueHelper.convert(values, PropertyType.STRING, session.getValueFactory());
        }
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, String[] values, int type)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value[] v = null;
        if (values != null) {
            v = ValueHelper.convert(values, type, session.getValueFactory());
        }
        return setProperty(session.getQName(name), v, type, true);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, String value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            v = session.getValueFactory().createValue(value);
        }
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, String value, int type)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            v = session.getValueFactory().createValue(value, type);
        }
        return setProperty(session.getQName(name), v, true);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Value value, int type)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        if (value != null) {
            value = ValueHelper.convert(value, type, session.getValueFactory());
        }
        return setProperty(session.getQName(name), value, true);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Value value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        return setProperty(session.getQName(name), value);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, InputStream value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            v = session.getValueFactory().createValue(value);
        }
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, boolean value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = session.getValueFactory().createValue(value);
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, double value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = session.getValueFactory().createValue(value);
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, long value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = session.getValueFactory().createValue(value);
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Calendar value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            try {
                v = session.getValueFactory().createValue(value);
            } catch (IllegalArgumentException e) {
                // thrown if calendar cannot be formatted as ISO8601
                throw new ValueFormatException(e.getMessage());
            }
        }
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Node value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            try {
                v = session.getValueFactory().createValue(value);
            } catch (UnsupportedRepositoryOperationException e) {
                // happens when node is not referenceable
                throw new ValueFormatException("node is not of type mix:referenceable");
            }
        }
        return setProperty(name, v);
    }

    /**
     * Implementation for <code>setProperty()</code> using a single {@link
     * Value}. The type of the returned property is enforced based on the
     * <code>enforceType</code> flag. If set to <code>true</code>, the returned
     * property is of the passed type if it didn't exist before. If set to
     * <code>false</code>, then the returned property may be of some other type,
     * but still must be based on an existing property definition for the given
     * name and single-valued flag. The resulting type is taken from that
     * definition and the implementation tries to convert the passed value to
     * that type. If that fails, then a {@link ValueFormatException} is thrown.
     *
     * @param name        the name of the property to set.
     * @param value       the value to set. If <code>null</code> the property is
     *                    removed.
     * @param enforceType if the type of <code>value</code> is enforced.
     * @return the <code>Property</code> object set, or <code>null</code> if
     *         this method was used to remove a property (by setting its value
     *         to <code>null</code>).
     * @throws ValueFormatException         if <code>value</code> cannot be
     *                                      converted to the specified type or
     *                                      if the property already exists and
     *                                      is multi-valued.
     * @throws VersionException             if this node is read-only due to a
     *                                      checked-in node and this implementation
     *                                      performs this validation immediately.
     * @throws LockException                if a lock prevents the setting of
     *                                      the property and this implementation
     *                                      performs this validation immediately.
     * @throws ConstraintViolationException if the change would violate a
     *                                      node-type or other constraint and
     *                                      this implementation performs this
     *                                      validation immediately.
     * @throws RepositoryException          if another error occurs.
     */
    protected PropertyImpl setProperty(Name name,
                                       Value value,
                                       boolean enforceType) throws
            ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        // check state of this instance
        sanityCheck();

        // check pre-conditions for setting property
        checkSetProperty();

        int type = PropertyType.UNDEFINED;
        if (value != null) {
            type = value.getType();
        }

        BitSet status = new BitSet();
        PropertyImpl prop = getOrCreateProperty(name, type, false, enforceType, status);
        try {
            prop.setValue(value);
        } catch (RepositoryException re) {
            if (status.get(CREATED)) {
                // setting value failed, get rid of newly created property
                removeChildProperty(name);
            }
            // rethrow
            throw re;
        }
        return prop;
    }

    /**
     * Implementation for <code>setProperty()</code> using a {@link Value}
     * array. The type of the returned property is enforced based on the
     * <code>enforceType</code> flag. If set to <code>true</code>, the returned
     * property is of the passed type if it didn't exist before. If set to
     * <code>false</code>, then the returned property may be of some other type,
     * but still must be based on an existing property definition for the given
     * name and multi-valued flag. The resulting type is taken from that
     * definition and the implementation tries to convert the passed values to
     * that type. If that fails, then a {@link ValueFormatException} is thrown.
     *
     * @param name        the name of the property to set.
     * @param values      the values to set. If <code>null</code> the property
     *                    is removed.
     * @param type        the target type of the values to set.
     * @param enforceType if the target type is enforced.
     * @return the <code>Property</code> object set, or <code>null</code> if
     *         this method was used to remove a property (by setting its value
     *         to <code>null</code>).
     * @throws ValueFormatException         if a value cannot be converted to
     *                                      the specified type or if the
     *                                      property already exists and is not
     *                                      multi-valued.
     * @throws VersionException             if this node is read-only due to a
     *                                      checked-in node and this implementation
     *                                      performs this validation immediately.
     * @throws LockException                if a lock prevents the setting of
     *                                      the property and this implementation
     *                                      performs this validation immediately.
     * @throws ConstraintViolationException if the change would violate a
     *                                      node-type or other constraint and
     *                                      this implementation performs this
     *                                      validation immediately.
     * @throws RepositoryException          if another error occurs.
     */
    protected PropertyImpl setProperty(Name name,
                                       Value[] values,
                                       int type,
                                       boolean enforceType) throws
            ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        // check state of this instance
        sanityCheck();

        // check pre-conditions for setting property
        checkSetProperty();

        BitSet status = new BitSet();
        PropertyImpl prop = getOrCreateProperty(name, type, true, enforceType, status);
        try {
            prop.setValue(values, type);
        } catch (RepositoryException re) {
            if (status.get(CREATED)) {
                // setting value failed, get rid of newly created property
                removeChildProperty(name);
            }
            // rethrow
            throw re;
        }
        return prop;
    }

    /**
     * {@inheritDoc}
     */
    public Node getNode(String relPath)
            throws PathNotFoundException, RepositoryException {

        // check state of this instance
        sanityCheck();

        Path p = resolveRelativePath(relPath);
        NodeId id = getNodeId(p);
        if (id == null) {
            throw new PathNotFoundException(relPath);
        }

        // determine parent as mandated by path
        NodeId parentId = null;
        if (!p.denotesRoot()) {
            parentId = getNodeId(p.getAncestor(1));
        }
        try {
            if (parentId == null) {
                return (NodeImpl) itemMgr.getItem(id);
            }
            // if the node is shareable, it now returns the node with the right
            // parent
            return itemMgr.getNode(id, parentId);
        } catch (AccessDeniedException ade) {
            throw new PathNotFoundException(relPath);
        } catch (ItemNotFoundException infe) {
            throw new PathNotFoundException(relPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator getNodes() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        /**
         * IMPORTANT:
         * an implementation of Node.getNodes()
         * must not use a class derived from TraversingElementVisitor
         * to traverse the hierarchy because this would lead to an infinite
         * recursion!
         */
        try {
            return itemMgr.getChildNodes((NodeId) id);
        } catch (ItemNotFoundException infe) {
            String msg = "failed to list the child nodes of " + this;
            log.debug(msg);
            throw new RepositoryException(msg, infe);
        } catch (AccessDeniedException ade) {
            String msg = "failed to list the child nodes of " + this;
            log.debug(msg);
            throw new RepositoryException(msg, ade);
        }
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getProperties() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        /**
         * IMPORTANT:
         * an implementation of Node.getProperties()
         * must not use a class derived from TraversingElementVisitor
         * to traverse the hierarchy because this would lead to an infinite
         * recursion!
         */
        try {
            return itemMgr.getChildProperties((NodeId) id);
        } catch (ItemNotFoundException infe) {
            String msg = "failed to list the child properties of " + this;
            log.debug(msg);
            throw new RepositoryException(msg, infe);
        } catch (AccessDeniedException ade) {
            String msg = "failed to list the child properties of " + this;
            log.debug(msg);
            throw new RepositoryException(msg, ade);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Property getProperty(String relPath)
            throws PathNotFoundException, RepositoryException {
        // check state of this instance
        sanityCheck();

        PropertyId id = resolveRelativePropertyPath(relPath);
        if (id == null) {
            throw new PathNotFoundException(relPath);
        }
        try {
            return (Property) itemMgr.getItem(id);
        } catch (ItemNotFoundException infe) {
            throw new PathNotFoundException(relPath);
        } catch (AccessDeniedException ade) {
            throw new PathNotFoundException(relPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasNode(String relPath) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        NodeId id = resolveRelativeNodePath(relPath);
        if (id != null) {
            return itemMgr.itemExists(id);
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasNodes() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        /**
         * hasNodes respects the access rights
         * of this node's session, i.e. it will
         * return false if child nodes exist
         * but the session is not granted read-access
         */
        return itemMgr.hasChildNodes((NodeId) id);
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasProperties() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        /**
         * hasProperties respects the access rights
         * of this node's session, i.e. it will
         * return false if properties exist
         * but the session is not granted read-access
         */
        return itemMgr.hasChildProperties((NodeId) id);
    }

    /**
     * {@inheritDoc}
     */
    public boolean isNodeType(String nodeTypeName) throws RepositoryException {
        try {
            return isNodeType(session.getQName(nodeTypeName));
        } catch (NameException e) {
            throw new RepositoryException(
                    "invalid node type name: " + nodeTypeName, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeType getPrimaryNodeType() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return session.getNodeTypeManager().getNodeType(
                data.getNodeState().getNodeTypeName());
    }

    /**
     * {@inheritDoc}
     */
    public NodeType[] getMixinNodeTypes() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        Set<Name> mixinNames = data.getNodeState().getMixinTypeNames();
        if (mixinNames.isEmpty()) {
            return new NodeType[0];
        }
        NodeType[] nta = new NodeType[mixinNames.size()];
        Iterator<Name> iter = mixinNames.iterator();
        int i = 0;
        while (iter.hasNext()) {
            nta[i++] = session.getNodeTypeManager().getNodeType(iter.next());
        }
        return nta;
    }

    /**
     * {@inheritDoc}
     */
    public void addMixin(String mixinName)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        try {
            addMixin(session.getQName(mixinName));
        } catch (NameException e) {
            throw new RepositoryException(
                    "invalid mixin type name: " + mixinName, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeMixin(String mixinName)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        try {
            removeMixin(session.getQName(mixinName));
        } catch (NameException e) {
            throw new RepositoryException(
                    "invalid mixin type name: " + mixinName, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean canAddMixin(String mixinName)
            throws NoSuchNodeTypeException, RepositoryException {
        // check state of this instance
        sanityCheck();

        Name ntName = session.getQName(mixinName);
        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        NodeTypeImpl mixin = ntMgr.getNodeType(ntName);
        if (!mixin.isMixin()) {
            return false;
        }

        int options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_CHECKED_OUT
                | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD;
        int permissions = Permission.NODE_TYPE_MNGMT;
        // special handling of mix:(simple)versionable. since adding the mixin alters
        // the version storage jcr:versionManagement privilege is required
        // in addition.
        if (NameConstants.MIX_VERSIONABLE.equals(ntName)
                || NameConstants.MIX_SIMPLE_VERSIONABLE.equals(ntName)) {
            permissions |= Permission.VERSION_MNGMT;
        }
        if (!session.getValidator().canModify(this, options, permissions)) {
            return false;
        }

        final Name primaryTypeName = data.getNodeState().getNodeTypeName();

        NodeTypeImpl primaryType = ntMgr.getNodeType(primaryTypeName);
        if (primaryType.isDerivedFrom(ntName)) {
            // mixin already inherited -> addMixin is allowed but has no effect.
            return true;
        }

        // build effective node type of mixins & primary type
        // in order to detect conflicts
        NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry();
        EffectiveNodeType entExisting;
        try {
            // existing mixin's
            Set<Name> mixins = new HashSet<Name>(data.getNodeState().getMixinTypeNames());

            // build effective node type representing primary type including existing mixin's
            entExisting = ntReg.getEffectiveNodeType(primaryTypeName, mixins);
            if (entExisting.includesNodeType(ntName)) {
                // the existing mixins already include the mixin to be added.
                // addMixin would succeed without modifying the node.
                return true;
            }

            // add new mixin
            mixins.add(ntName);
            // try to build new effective node type (will throw in case of conflicts)
            ntReg.getEffectiveNodeType(primaryTypeName, mixins);
        } catch (NodeTypeConflictException ntce) {
            return false;
        }

        return true;
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasProperty(String relPath) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        PropertyId id = resolveRelativePropertyPath(relPath);
        if (id != null) {
            return itemMgr.itemExists(id);
        } else {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getReferences() throws RepositoryException {
        return getReferences(null);
    }

    /**
     * {@inheritDoc}
     */
    public NodeDefinition getDefinition() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return data.getNodeDefinition();
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator getNodes(String namePattern) throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return ChildrenCollectorFilter.collectChildNodes(this, namePattern);
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getProperties(String namePattern)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return ChildrenCollectorFilter.collectProperties(this, namePattern);
    }

    /**
     * {@inheritDoc}
     */
    public Item getPrimaryItem()
            throws ItemNotFoundException, RepositoryException {
        // check state of this instance
        sanityCheck();

        String name = getPrimaryNodeType().getPrimaryItemName();
        if (name == null) {
            throw new ItemNotFoundException();
        }
        if (hasProperty(name)) {
            return getProperty(name);
        } else if (hasNode(name)) {
            return getNode(name);
        } else {
            throw new ItemNotFoundException();
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getUUID()
            throws UnsupportedRepositoryOperationException, RepositoryException {
        // check state of this instance
        sanityCheck();

        if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) {
            throw new UnsupportedRepositoryOperationException();
        }

        return getNodeId().toString();
    }

    /**
     * {@inheritDoc}
     */
    public String getCorrespondingNodePath(String workspaceName)
            throws ItemNotFoundException, NoSuchWorkspaceException,
            AccessDeniedException, RepositoryException {
        // check state of this instance
        sanityCheck();

        SessionImpl srcSession = null;
        try {
            // create session on other workspace for current subject
            // (may throw NoSuchWorkspaceException and AccessDeniedException)
            srcSession = rep.createSession(session.getSubject(), workspaceName);

            // search nearest ancestor that is referenceable
            NodeImpl m1 = this;
            while (m1.getDepth() != 0 && !m1.isNodeType(NameConstants.MIX_REFERENCEABLE)) {
                m1 = (NodeImpl) m1.getParent();
            }

            // if root is common ancestor, corresponding path is same as ours
            if (m1.getDepth() == 0) {
                // check existence
                if (!srcSession.getItemManager().nodeExists(getPrimaryPath())) {
                    throw new ItemNotFoundException("Node not found: " + this);
                } else {
                    return getPath();
                }
            }

            // get corresponding ancestor
            Node m2 = srcSession.getNodeByUUID(m1.getUUID());

            // return path of m2, if m1 == n1
            if (m1 == this) {
                return m2.getPath();
            }

            String relPath;
            try {
                Path p = m1.getPrimaryPath().computeRelativePath(getPrimaryPath());
                // use prefix mappings of srcSession
                relPath = session.getJCRPath(p);
            } catch (NameException be) {
                // should never get here...
                String msg = "internal error: failed to determine relative path";
                log.error(msg, be);
                throw new RepositoryException(msg, be);
            }

            if (!m2.hasNode(relPath)) {
                throw new ItemNotFoundException();
            } else {
                return m2.getNode(relPath).getPath();
            }
        } finally {
            if (srcSession != null) {
                // we don't need the other session anymore, logout
                srcSession.logout();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public int getIndex() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        NodeId parentId = getParentId();
        if (parentId == null) {
            // the root node cannot have same-name siblings; always return 1
            return 1;
        }

        try {
            NodeState parent =
                    (NodeState) stateMgr.getItemState(parentId);
            ChildNodeEntry parentEntry =
                    parent.getChildNodeEntry(getNodeId());
            return parentEntry.getIndex();
        } catch (ItemStateException ise) {
            // should never get here...
            String msg = "internal error: failed to determine index";
            log.error(msg, ise);
            throw new RepositoryException(msg, ise);
        }
    }

    //-------------------------------------------------------< shareable nodes >

    /**
     * Returns an iterator over all nodes that are in the shared set of this
     * node. If this node is not shared then the returned iterator contains
     * only this node.
     *
     * @return a <code>NodeIterator</code>
     * @throws RepositoryException if an error occurs.
     * @since JCR 2.0
     */
    public NodeIterator getSharedSet() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        ArrayList<NodeImpl> list = new ArrayList<NodeImpl>();

        if (!isShareable()) {
            list.add(this);
        } else {
            NodeState state = data.getNodeState();
            for (NodeId parentId : state.getSharedSet()) {
                list.add(itemMgr.getNode(getNodeId(), parentId));
            }
        }
        return new NodeIteratorAdapter(list);
    }

    /**
     * A special kind of <code>remove()</code> that removes this node and every
     * other node in the shared set of this node.
     * <p/>
     * This removal must be done atomically, i.e., if one of the nodes cannot be
     * removed, the function throws the exception <code>remove()</code> would
     * have thrown in that case, and none of the nodes are removed.
     * <p/>
     * If this node is not shared this method removes only this node.
     *
     * @throws VersionException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws RepositoryException
     * @see #removeShare()
     * @see Item#remove()
     * @since JCR 2.0
     */
    public void removeSharedSet() throws VersionException, LockException,
            ConstraintViolationException, RepositoryException {

        // check state of this instance
        sanityCheck();

        NodeIterator iter = getSharedSet();
        while (iter.hasNext()) {
            iter.nextNode().removeShare();
        }
    }

    /**
     * A special kind of <code>remove()</code> that removes this node, but does
     * not remove any other node in the shared set of this node.
     * <p/>
     * All of the exceptions defined for <code>remove()</code> apply to this
     * function. In addition, a <code>RepositoryException</code> is thrown if
     * this node cannot be removed without removing another node in the shared
     * set of this node.
     * <p/>
     * If this node is not shared this method removes only this node.
     *
     * @throws VersionException
     * @throws LockException
     * @throws ConstraintViolationException
     * @throws RepositoryException
     * @see #removeSharedSet()
     * @see Item#remove()
     * @since JCR 2.0
     */
    public void removeShare() throws VersionException, LockException,
            ConstraintViolationException, RepositoryException {

        // check state of this instance
        sanityCheck();

        // Standard remove() will remove just this node
        remove();
    }

    /**
     * Helper method, returning a flag that indicates whether this node is
     * shareable.
     *
     * @return <code>true</code> if this node is shareable;
     *         <code>false</code> otherwise.
     * @see NodeState#isShareable()
     */
    boolean isShareable() {
       return data.getNodeState().isShareable();
    }

    /**
     * Helper method, returning the parent id this node is attached to. If this
     * node is shareable, it returns the primary parent id (which remains
     * fixed since shareable nodes are not moveable). Otherwise returns the
     * underlying state's parent id.
     *
     * @return parent id
     */
    NodeId getParentId() {
        return data.getParentId();
    }

    /**
     * Helper method, returning a flag indicating whether this node has
     * the given share-parent.
     *
     * @param parentId parent id
     * @return <code>true</code> if the node has the given shared parent;
     *         <code>false</code> otherwise.
     */
    boolean hasShareParent(NodeId parentId) {
        return data.getNodeState().containsShare(parentId);
    }

    /**
     * Add a share-parent to this node. This method checks, whether:
     * <ul>
     * <li>this node is shareable</li>
     * <li>adding the given would create a share cycle</li>
     * <li>the given parent is already a share-parent</li>
     * </ul>
     * @param parentId parent to add to the shared set
     * @throws RepositoryException if an error occurs
     */
    void addShareParent(NodeId parentId) throws RepositoryException {
        // verify that we're shareable
        if (!isShareable()) {
            String msg = this + " is not shareable.";
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        // detect share cycle
        NodeId srcId = getNodeId();
        HierarchyManager hierMgr = session.getHierarchyManager();
        if (parentId.equals(srcId) || hierMgr.isAncestor(srcId, parentId)) {
            String msg = "This would create a share cycle.";
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        // quickly verify whether the share is already contained before creating
        // a transient state in vain
        NodeState state = data.getNodeState();
        if (!state.containsShare(parentId)) {
            state = (NodeState) getOrCreateTransientItemState();
            if (state.addShare(parentId)) {
                return;
            }
        }
        String msg = "Adding a shareable node twice to the same parent is not supported.";
        log.debug(msg);
        throw new UnsupportedRepositoryOperationException(msg);
    }

    /**
     * {@inheritDoc}
     *
     * Overridden to return a different path for shareable nodes.
     *
     * TODO SN: copies functionality in that is already available in
     *          HierarchyManagerImpl, namely composing a path by
     *          concatenating the parent path + this node's name and index:
     *          rather use hierarchy manager to do this
     */
    public Path getPrimaryPath() throws RepositoryException {
        if (!isShareable()) {
            return super.getPrimaryPath();
        }

        NodeId parentId = getParentId();
        NodeImpl parentNode = (NodeImpl) getParent();
        Path parentPath = parentNode.getPrimaryPath();
        PathBuilder builder = new PathBuilder(parentPath);

        ChildNodeEntry entry =
            parentNode.getNodeState().getChildNodeEntry(getNodeId());
        if (entry == null) {
            String msg = "failed to build path of " + id + ": "
                    + parentId + " has no child entry for "
                    + id;
            log.debug(msg);
            throw new ItemNotFoundException(msg);
        }
        // add to path
        if (entry.getIndex() == 1) {
            builder.addLast(entry.getName());
        } else {
            builder.addLast(entry.getName(), entry.getIndex());
        }
        return builder.getPath();
    }

    //------------------------------< versioning support: public Node methods >

    /**
     * {@inheritDoc}
     */
    public boolean isCheckedOut() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        // try shortcut first:
        // if current node is 'new' we can safely consider it checked-out since
        // otherwise it would had been impossible to add it in the first place
        if (isNew()) {
            return true;
        }

        // search nearest ancestor that is versionable
        // FIXME should not only rely on existence of jcr:isCheckedOut property
        // but also verify that node.isNodeType("mix:versionable")==true;
        // this would have a negative impact on performance though...
        try {
            NodeState state = getNodeState();
            while (!state.hasPropertyName(JCR_ISCHECKEDOUT)) {
                ItemId parentId = state.getParentId();
                if (parentId == null) {
                    // root reached or out of hierarchy
                    return true;
                }
                state = (NodeState)
                    session.getItemStateManager().getItemState(parentId);
            }
            PropertyId id = new PropertyId(state.getNodeId(), JCR_ISCHECKEDOUT);
            PropertyState ps =
                (PropertyState) session.getItemStateManager().getItemState(id);
            return ps.getValues()[0].getBoolean();
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * Returns the version manager of this workspace.
     */
    private VersionManagerImpl getVersionManagerImpl() {
        return session.getWorkspaceImpl().getVersionManagerImpl();
    }

    /**
     * {@inheritDoc}
     */
    public void update(String srcWorkspaceName) throws RepositoryException {
        getVersionManagerImpl().update(this, srcWorkspaceName);
    }

    /**
     * Use {@link VersionManager#checkin(String)} instead
     */
    @Deprecated
    public Version checkin() throws RepositoryException {
        return getVersionManagerImpl().checkin(getPath());
    }

    /**
     * Use {@link VersionManagerImpl#checkin(String, Calendar)} instead
     *
     * @since Apache Jackrabbit 1.6
     * @see <a href="https://issues.apache.org/jira/browse/JCR-1972">JCR-1972</a>
     */
    @Deprecated
    public Version checkin(Calendar created) throws RepositoryException {
        return getVersionManagerImpl().checkin(getPath(), created);
    }

    /**
     * Use {@link VersionManager#checkout(String)} instead
     */
    @Deprecated
    public void checkout() throws RepositoryException {
        getVersionManagerImpl().checkout(getPath());
    }

    /**
     * Use {@link VersionManager#merge(String, String, boolean)} instead
     */
    @Deprecated
    public NodeIterator merge(String srcWorkspace, boolean bestEffort)
            throws RepositoryException {
        return getVersionManagerImpl().merge(
                getPath(), srcWorkspace, bestEffort);
    }

    /**
     * Use {@link VersionManager#cancelMerge(String, Version)} instead
     */
    @Deprecated
    public void cancelMerge(Version version) throws RepositoryException {
        getVersionManagerImpl().cancelMerge(getPath(), version);
    }

    /**
     * Use {@link VersionManager#doneMerge(String, Version)} instead
     */
    @Deprecated
    public void doneMerge(Version version) throws RepositoryException {
        getVersionManagerImpl().doneMerge(getPath(), version);
    }

    /**
     * Use {@link VersionManager#restore(String, String, boolean)} instead
     */
    @Deprecated
    public void restore(String versionName, boolean removeExisting)
            throws RepositoryException {
        getVersionManagerImpl().restore(getPath(), versionName, removeExisting);
    }

    /**
     * Use {@link VersionManager#restore(String, Version, boolean)} instead
     */
    @Deprecated
    public void restore(Version version, boolean removeExisting)
            throws RepositoryException {
        getVersionManagerImpl().restore(this, version, removeExisting);
    }

    /**
     * Use {@link VersionManager#restore(String, Version, boolean)} instead
     */
    @Deprecated
    public void restore(Version version, String relPath, boolean removeExisting)
            throws RepositoryException {
        if (hasNode(relPath)) {
            getVersionManagerImpl().restore((NodeImpl) getNode(relPath), version, removeExisting);
        } else {
            getVersionManagerImpl().restore(
                getPath() + "/" + relPath, version, removeExisting);
        }
    }

    /**
     * Use {@link VersionManager#restoreByLabel(String, String, boolean)}
     * instead
     */
    @Deprecated
    public void restoreByLabel(String versionLabel, boolean removeExisting)
            throws RepositoryException {
        getVersionManagerImpl().restoreByLabel(
                getPath(), versionLabel, removeExisting);
    }

    /**
     * Use {@link VersionManager#getVersionHistory(String)} instead
     */
    @Deprecated
    public VersionHistory getVersionHistory() throws RepositoryException {
        return getVersionManagerImpl().getVersionHistory(getPath());
    }

    /**
     * Use {@link VersionManager#getBaseVersion(String)} instead
     */
    @Deprecated
    public Version getBaseVersion() throws RepositoryException {
        return getVersionManagerImpl().getBaseVersion(getPath());
    }

    //------------------------------------------------------< locking support >
    /**
     * {@inheritDoc}
     */
    public Lock lock(boolean isDeep, boolean isSessionScoped)
            throws UnsupportedRepositoryOperationException, LockException,
            AccessDeniedException, InvalidItemStateException,
            RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = session.getWorkspace().getLockManager();
        return lockMgr.lock(getPath(), isDeep, isSessionScoped, Long.MAX_VALUE, null);
    }

    /**
     * {@inheritDoc}
     */
    public Lock getLock()
            throws UnsupportedRepositoryOperationException, LockException,
            AccessDeniedException, RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = session.getWorkspace().getLockManager();
        return lockMgr.getLock(getPath());
    }

    /**
     * {@inheritDoc}
     */
    public void unlock()
            throws UnsupportedRepositoryOperationException, LockException,
            AccessDeniedException, InvalidItemStateException,
            RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = session.getWorkspace().getLockManager();
        lockMgr.unlock(getPath());
    }

    /**
     * {@inheritDoc}
     */
    public boolean holdsLock() throws RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = session.getWorkspace().getLockManager();
        return lockMgr.holdsLock(getPath());
    }

    /**
     * {@inheritDoc}
     */
    public boolean isLocked() throws RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = session.getWorkspace().getLockManager();
        return lockMgr.isLocked(getPath());
    }

    /**
     * Check whether this node is locked by somebody else.
     *
     * @throws LockException       if this node is locked by somebody else
     * @throws RepositoryException if some other error occurs
     * @deprecated
     */
    protected void checkLock() throws LockException, RepositoryException {
        if (isNew()) {
            // a new node needs no check
            return;
        }
        session.getLockManager().checkLock(this);
    }

    //--------------------------------------------------< new JSR 283 methods >
    /**
     * {@inheritDoc}
     */
    public String getIdentifier() throws RepositoryException {
        return ((NodeId) id).toString();
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getReferences(String name)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        try {
            if (stateMgr.hasNodeReferences(getNodeId())) {
                NodeReferences refs = stateMgr.getNodeReferences(getNodeId());
                // refs.getReferences() returns a list of PropertyId's
                List<PropertyId> idList = refs.getReferences();
                if (name != null) {
                    Name qName;
                    try {
                        qName = session.getQName(name);
                    } catch (NameException e) {
                        throw new RepositoryException("invalid property name: " + name, e);
                    }
                    ArrayList<PropertyId> filteredList = new ArrayList<PropertyId>(idList.size());
                    for (PropertyId propId : idList) {
                        if (propId.getName().equals(qName)) {
                            filteredList.add(propId);
                        }
                    }
                    idList = filteredList;
                }
                return new LazyItemIterator(itemMgr, idList);
            } else {
                // there are no references, return empty iterator
                return PropertyIteratorAdapter.EMPTY;
            }
        } catch (ItemStateException e) {
            String msg = "Unable to retrieve REFERENCE properties that refer to " + id;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getWeakReferences() throws RepositoryException {
        // check state of this instance
        sanityCheck();

        // shortcut if node isn't referenceable
        if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) {
            return PropertyIteratorAdapter.EMPTY;
        }

        Value ref = getSession().getValueFactory().createValue(this, true);
        List<Property> props = new ArrayList<Property>();
        QueryManagerImpl qm = (QueryManagerImpl) session.getWorkspace().getQueryManager();
        for (Node n : qm.getWeaklyReferringNodes(this)) {
            for (PropertyIterator it = n.getProperties(); it.hasNext(); ) {
                Property p = it.nextProperty();
                if (p.getType() == PropertyType.WEAKREFERENCE) {
                    Collection<Value> refs;
                    if (p.isMultiple()) {
                        refs = Arrays.asList(p.getValues());
                    } else {
                        refs = Collections.singleton(p.getValue());
                    }
                    if (refs.contains(ref)) {
                        props.add(p);
                    }
                }
            }
        }
        return new PropertyIteratorAdapter(props);
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getWeakReferences(String name) throws RepositoryException {
        if (name == null) {
            return getWeakReferences();
        }

        // check state of this instance
        sanityCheck();

        // shortcut if node isn't referenceable
        if (!isNodeType(NameConstants.MIX_REFERENCEABLE)) {
            return PropertyIteratorAdapter.EMPTY;
        }

        try {
            StringBuilder stmt = new StringBuilder();
            stmt.append("//*[@").append(ISO9075.encode(name));
            stmt.append(" = '").append(data.getId()).append("']");
            Query q = session.getWorkspace().getQueryManager().createQuery(
                    stmt.toString(), Query.XPATH);
            QueryResult result = q.execute();
            ArrayList<Property> l = new ArrayList<Property>();
            for (NodeIterator nit = result.getNodes(); nit.hasNext();) {
                Node n = nit.nextNode();
                l.add(n.getProperty(name));
            }
            if (l.isEmpty()) {
                return PropertyIteratorAdapter.EMPTY;
            } else {
                return new PropertyIteratorAdapter(l);
            }
        } catch (RepositoryException e) {
            String msg = "Unable to retrieve WEAKREFERENCE properties that refer to " + id;
            log.debug(msg);
            throw new RepositoryException(msg, e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator getNodes(String[] nameGlobs)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return ChildrenCollectorFilter.collectChildNodes(this, nameGlobs);
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getProperties(String[] nameGlobs)
            throws RepositoryException {
        // check state of this instance
        sanityCheck();

        return ChildrenCollectorFilter.collectProperties(this, nameGlobs);
    }

    /**
     * {@inheritDoc}
     */
    public void setPrimaryType(String nodeTypeName)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {
        // check state of this instance
        sanityCheck();

        // make sure this node is checked-out, neither protected nor locked and
        // the editing session has sufficient permission to change the primary type.
        int options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK
                | ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD;
        session.getValidator().checkModify(this, options, Permission.NODE_TYPE_MNGMT);

        final NodeState state = data.getNodeState();
        if (state.getParentId() == null) {
            String msg = "changing the primary type of the root node is not supported";
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        Name ntName = session.getQName(nodeTypeName);
        if (ntName.equals(state.getNodeTypeName())) {
            log.debug("Node already has " + nodeTypeName + " as primary node type.");
            return;
        }

        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        NodeType nt = ntMgr.getNodeType(ntName);
        if (nt.isMixin()) {
            throw new ConstraintViolationException(nodeTypeName + ": not a primary node type.");
        } else if (nt.isAbstract()) {
            throw new ConstraintViolationException(nodeTypeName + ": is an abstract node type.");
        }

        // build effective node type of new primary type & existing mixin's
        // in order to detect conflicts
        NodeTypeRegistry ntReg = ntMgr.getNodeTypeRegistry();
        EffectiveNodeType entNew, entOld, entAll;
        try {
            entNew = ntReg.getEffectiveNodeType(ntName);
            entOld = ntReg.getEffectiveNodeType(state.getNodeTypeName());

            // try to build new effective node type (will throw in case of conflicts)
            entAll = ntReg.getEffectiveNodeType(ntName, state.getMixinTypeNames());
        } catch (NodeTypeConflictException ntce) {
            throw new ConstraintViolationException(ntce.getMessage());
        }

        // get applicable definition for this node using new primary type
        QNodeDefinition nodeDef;
        try {
            NodeImpl parent = (NodeImpl) getParent();
            nodeDef = parent.getApplicableChildNodeDefinition(getQName(), ntName).unwrap();
        } catch (RepositoryException re) {
            String msg = this + ": no applicable definition found in parent node's node type";
            log.debug(msg);
            throw new ConstraintViolationException(msg, re);
        }

        if (!nodeDef.equals(itemMgr.getDefinition(state).unwrap())) {
            onRedefine(nodeDef);
        }

        Set<QItemDefinition> oldDefs = new HashSet<QItemDefinition>(Arrays.asList(entOld.getAllItemDefs()));
        Set<QItemDefinition> newDefs = new HashSet<QItemDefinition>(Arrays.asList(entNew.getAllItemDefs()));
        Set<QItemDefinition> allDefs = new HashSet<QItemDefinition>(Arrays.asList(entAll.getAllItemDefs()));

        // added child item definitions
        Set<QItemDefinition> addedDefs = new HashSet<QItemDefinition>(newDefs);
        addedDefs.removeAll(oldDefs);

        // referential integrity check
        boolean referenceableOld = entOld.includesNodeType(NameConstants.MIX_REFERENCEABLE);
        boolean referenceableNew = entNew.includesNodeType(NameConstants.MIX_REFERENCEABLE);
        if (referenceableOld && !referenceableNew) {
            // node would become non-referenceable;
            // make sure no references exist
            PropertyIterator iter = getReferences();
            if (iter.hasNext()) {
                throw new ConstraintViolationException(
                        "the new primary type cannot be set as it would render "
                                + "this node 'non-referenceable' while it is still being "
                                + "referenced through at least one property of type REFERENCE");
            }
        }

        // do the actual modifications in content as mandated by the new primary type

        // modify the state of this node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        thisState.setNodeTypeName(ntName);

        // set jcr:primaryType property
        internalSetProperty(NameConstants.JCR_PRIMARYTYPE, InternalValue.create(ntName));

        // walk through properties and child nodes and change definition as necessary

        // use temp set to avoid ConcurrentModificationException
        HashSet<Name> set = new HashSet<Name>(thisState.getPropertyNames());
        for (Name propName : set) {
            try {
                PropertyState propState =
                        (PropertyState) stateMgr.getItemState(
                                new PropertyId(thisState.getNodeId(), propName));
                if (!allDefs.contains(itemMgr.getDefinition(propState).unwrap())) {
                    // try to find new applicable definition first and
                    // redefine property if possible
                    try {
                        PropertyImpl prop = (PropertyImpl) itemMgr.getItem(propState.getId());
                        if (prop.getDefinition().isProtected()) {
                            // remove 'orphaned' protected properties immediately
                            removeChildProperty(propName);
                            continue;
                        }
                        PropertyDefinitionImpl pdi = getApplicablePropertyDefinition(
                                propName, propState.getType(),
                                propState.isMultiValued(), false);
                        if (pdi.getRequiredType() != PropertyType.UNDEFINED
                                && pdi.getRequiredType() != propState.getType()) {
                            // value conversion required
                            if (propState.isMultiValued()) {
                                // convert value
                                Value[] values =
                                        ValueHelper.convert(
                                                prop.getValues(),
                                                pdi.getRequiredType(),
                                                session.getValueFactory());
                                // redefine property
                                prop.onRedefine(pdi.unwrap());
                                // set converted values
                                prop.setValue(values);
                            } else {
                                // convert value
                                Value value =
                                        ValueHelper.convert(
                                                prop.getValue(),
                                                pdi.getRequiredType(),
                                                session.getValueFactory());
                                // redefine property
                                prop.onRedefine(pdi.unwrap());
                                // set converted values
                                prop.setValue(value);
                            }
                        } else {
                            // redefine property
                            prop.onRedefine(pdi.unwrap());
                        }
                        // update collection of added definitions
                        addedDefs.remove(pdi.unwrap());
                    } catch (ValueFormatException vfe) {
                        // value conversion failed, remove it
                        removeChildProperty(propName);
                    } catch (ConstraintViolationException cve) {
                        // no suitable definition found for this property,
                        // remove it
                        removeChildProperty(propName);
                    }
                }
            } catch (ItemStateException ise) {
                String msg = propName + ": failed to retrieve property state";
                log.error(msg, ise);
                throw new RepositoryException(msg, ise);
            }
        }

        // use temp array to avoid ConcurrentModificationException
        ArrayList<ChildNodeEntry> list = new ArrayList<ChildNodeEntry>(thisState.getChildNodeEntries());
        // start from tail to avoid problems with same-name siblings
        for (int i = list.size() - 1; i >= 0; i--) {
            ChildNodeEntry entry = list.get(i);
            try {
                NodeState nodeState = (NodeState) stateMgr.getItemState(entry.getId());
                if (!allDefs.contains(itemMgr.getDefinition(nodeState).unwrap())) {
                    // try to find new applicable definition first and
                    // redefine node if possible
                    try {
                        NodeImpl node = (NodeImpl) itemMgr.getItem(nodeState.getId());
                        if (node.getDefinition().isProtected()) {
                            // remove 'orphaned' protected child node immediately
                            removeChildNode(entry.getName(), entry.getIndex());
                            continue;
                        }
                        NodeDefinitionImpl ndi = getApplicableChildNodeDefinition(
                                entry.getName(),
                                nodeState.getNodeTypeName());
                        // redefine node
                        node.onRedefine(ndi.unwrap());
                        // update collection of added definitions
                        addedDefs.remove(ndi.unwrap());
                    } catch (ConstraintViolationException cve) {
                        // no suitable definition found for this child node,
                        // remove it
                        removeChildNode(entry.getName(), entry.getIndex());
                    }
                }
            } catch (ItemStateException ise) {
                String msg = entry.getName() + ": failed to retrieve node state";
                log.error(msg, ise);
                throw new RepositoryException(msg, ise);
            }
        }

        // create items that are defined as auto-created by the new primary node
        // type and at the same time were not present with the old nt
        for (QItemDefinition def : addedDefs) {
            if (def.isAutoCreated()) {
                if (def.definesNode()) {
                    NodeDefinitionImpl ndi = ntMgr.getNodeDefinition((QNodeDefinition) def);
                    createChildNode(def.getName(), (NodeTypeImpl) ndi.getDefaultPrimaryType(), null);
                } else {
                    PropertyDefinitionImpl pdi = ntMgr.getPropertyDefinition((QPropertyDefinition) def);
                    createChildProperty(pdi.unwrap().getName(), pdi.getRequiredType(), pdi);
                }
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, BigDecimal value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            v = session.getValueFactory().createValue(value);
        }
        return setProperty(name, v);
    }

    /**
     * {@inheritDoc}
     */
    public Property setProperty(String name, Binary value)
            throws ValueFormatException, VersionException, LockException,
            ConstraintViolationException, RepositoryException {
        Value v = null;
        if (value != null) {
            v = session.getValueFactory().createValue(value);
        }
        return setProperty(name, v);
    }

    /**
     * Returns all allowed transitions from the current lifecycle state of
     * this node.
     * <p>
     * The lifecycle policy node referenced by the "jcr:lifecyclePolicy"
     * property is expected to contain a "transitions" node with a list of
     * child nodes, one for each transition. These transition nodes must
     * have single-valued string "from" and "to" properties that identify
     * the allowed source and target states of each transition.
     * <p>
     * Note that future versions of Apache Jackrabbit may well use different
     * lifecycle policy implementations.
     *
     * @since Apache Jackrabbit 2.0
     * @return allowed transitions for the current lifecycle state of this node
     * @throws UnsupportedRepositoryOperationException
     *             if this node does not have the mix:lifecycle mixin node type
     * @throws RepositoryException if a repository error occurs
     */
    public String[] getAllowedLifecycleTransistions()
            throws UnsupportedRepositoryOperationException, RepositoryException {
        if (isNodeType(NameConstants.MIX_LIFECYCLE)) {
            Node policy = getProperty(JCR_LIFECYCLE_POLICY).getNode();
            String state = getProperty(JCR_CURRENT_LIFECYCLE_STATE).getString();

            List<String> targetStates = new ArrayList<String>();
            if (policy.hasNode("transitions")) {
                Node transitions = policy.getNode("transitions");
                for (Node transition : JcrUtils.getChildNodes(transitions)) {
                    String from = transition.getProperty("from").getString();
                    if (from.equals(state)) {
                        String to = transition.getProperty("to").getString();
                        targetStates.add(to);
                    }
                }
            }

            return targetStates.toArray(new String[targetStates.size()]);
        } else {
            throw new UnsupportedRepositoryOperationException(
                    "Only nodes with mixin node type mix:lifecycle"
                    + " may participate in a lifecycle: " + this);
        }
    }

    /**
     * Transitions this node through its lifecycle to the given target state.
     *
     * @since Apache Jackrabbit 2.0
     * @see #getAllowedLifecycleTransistions()
     * @param transition target lifecycle state
     * @throws UnsupportedRepositoryOperationException
     *             if this node does not have the mix:lifecycle mixin node type
     * @throws InvalidLifecycleTransitionException
     *             if the given target state is not among the allowed
     *             transitions from the current lifecycle state of this node
     * @throws RepositoryException if a repository error occurs
     */
    public void followLifecycleTransition(String transition)
            throws UnsupportedRepositoryOperationException,
            InvalidLifecycleTransitionException, RepositoryException {
        // getAllowedLifecycleTransitions checks for the mix:lifecycle mixin
        for (String target : getAllowedLifecycleTransistions()) {
            if (target.equals(transition)) {
                PropertyImpl property = getProperty(JCR_CURRENT_LIFECYCLE_STATE);
                property.internalSetValue(
                        new InternalValue[] { InternalValue.create(target) },
                        PropertyType.STRING);
                property.save();
                return;
            }
        }

        // No valid transition found
        throw new InvalidLifecycleTransitionException(
                "Invalid lifecycle transition \""
                + transition  + "\" for " + this);
    }

    /**
     * Assigns the given lifecycle policy to this node and sets the
     * current state to the one given.
     * <p>
     * Note that currently no special checks are made against the given
     * arguments, and that you will need to explicitly persist these changes
     * by calling save().
     * <p>
     * Note that future versions of Apache Jackrabbit may well use different
     * lifecycle policy implementations.
     *
     * @param policy lifecycle policy node
     * @param state current lifecycle state
     * @throws RepositoryException if a repository error occurs
     */
    public void assignLifecyclePolicy(Node policy, String state)
            throws RepositoryException {
        if (!(policy instanceof NodeImpl)
                || !((NodeImpl) policy).isNodeType(MIX_REFERENCEABLE)) {
            throw new RepositoryException(
                    policy + " is not referenceable, so it can not be"
                    + " used as a lifecycle policy");
        }

        addMixin(MIX_LIFECYCLE);
        internalSetProperty(
                JCR_LIFECYCLE_POLICY,
                InternalValue.create(((NodeImpl) policy).getNodeId()));
        internalSetProperty(
                JCR_CURRENT_LIFECYCLE_STATE,
                InternalValue.create(state));
    }

    //--------------------------------------------------------------< Object >

    /**
     * Return a string representation of this node for diagnostic purposes.
     *
     * @return "node /path/to/item"
     */
    public String toString() {
        return "node " + super.toString();
    }

}
