/*
 * 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 static javax.jcr.PropertyType.STRING;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_CURRENT_LIFECYCLE_STATE;
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.MIX_LIFECYCLE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_REFERENCEABLE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_SIMPLE_VERSIONABLE;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE;

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.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
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.ItemDefinition;
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.api.JackrabbitNode;
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.AccessManager;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.session.AddNodeOperation;
import org.apache.jackrabbit.core.session.SessionContext;
import org.apache.jackrabbit.core.session.SessionOperation;
import org.apache.jackrabbit.core.session.SessionWriteOperation;
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.ItemStateManager;
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.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
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.spi.commons.nodetype.NodeDefinitionImpl;
import org.apache.jackrabbit.spi.commons.nodetype.PropertyDefinitionImpl;
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;

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

    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 sessionContext the component context of the associated session
     * @param data       the node data
     */
    protected NodeImpl(
            ItemManager itemMgr, SessionContext sessionContext,
            AbstractNodeData data) {
        super(itemMgr, sessionContext, data);
        this.data = data;
        // paranoid sanity check
        NodeTypeRegistry ntReg = sessionContext.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 {
        Path p = resolveRelativePath(relPath);
        return getPropertyId(p);
    }

    /**
     * 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 sessionContext.getQPath(relPath);
        } catch (NameException e) {
            throw new RepositoryException(
                    "Failed to resolve path " + relPath
                    + " relative to " + this, 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 && p.denotesName()) {
            // check if node entry exists
            ChildNodeEntry cne = data.getNodeState().getChildNodeEntry(
                    p.getName(), p.getNormalizedIndex());
            if (cne != null) {
                return cne.getId();
            } else {
                return null; // there's no child node with that name
            }
        } else {
            // build and resolve absolute path
            try {
                p = PathFactoryImpl.getInstance().create(
                        getPrimaryPath(), p, true);
            } catch (RepositoryException re) {
                // failed to build canonical path
                return null;
            }
            return sessionContext.getHierarchyManager().resolveNodePath(p);
        }
    }

    /**
     * Returns the id of the property 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 PropertyId getPropertyId(Path p) throws RepositoryException {
        if (p.getLength() == 1 && p.denotesName()) {
            // check if property entry exists
            NodeState thisState = data.getNodeState();
            if (p.getIndex() == Path.INDEX_UNDEFINED
                    && thisState.hasPropertyName(p.getName())) {
                return new PropertyId(thisState.getNodeId(), p.getName());
            } else {
                return null; // there's no property with that name
            }
        } else {
            // build and resolve absolute path
            try {
                p = PathFactoryImpl.getInstance().create(
                        getPrimaryPath(), p, true);
            } catch (RepositoryException re) {
                // failed to build canonical path
                return null;
            }
            return sessionContext.getHierarchyManager().resolvePropertyPath(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;
        }
        return !stateMgr.getDescendantTransientItemStates(id).isEmpty();
    }

    @Override
    protected synchronized ItemState getOrCreateTransientItemState()
            throws RepositoryException {

        synchronized (data) {
            if (!isTransient()) {
                try {
                    // make transient (copy-on-write)
                    NodeState transientState =
                            stateMgr.createTransientNodeState(
                                    (NodeState) stateMgr.getItemState(getId()), 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(
                    sessionContext.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...]

        */
        PropertyId propId = new PropertyId(getNodeId(), name);
        try {
            return (PropertyImpl) itemMgr.getItem(propId);
        } catch (AccessDeniedException ade) {
            throw new ItemNotFoundException(name.toString());
        } catch (ItemNotFoundException e) {
            // does not exist yet or has been removed transiently:
            // find definition for the specified property and (re-)create property
            PropertyDefinitionImpl def = getApplicablePropertyDefinition(
                    name, type, multiValued, exactTypeMatch);
            PropertyImpl prop;
            if (stateMgr.hasTransientItemStateInAttic(propId)) {
                // remove from attic
                try {
                    stateMgr.disposeTransientItemStateInAttic(stateMgr.getAttic().getItemState(propId));
                } catch (ItemStateException ise) {
                    // shouldn't happen because we checked if it is in the attic
                    throw new RepositoryException(ise);
                }
                prop = (PropertyImpl) itemMgr.getItem(propId);
                PropertyState state = (PropertyState) prop.getOrCreateTransientItemState();
                state.setMultiValued(multiValued);
                state.setType(type);
                getNodeState().addPropertyName(name);
            } else {
                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 precedence 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
            String userId = sessionContext.getSessionImpl().getUserID();
            new NodeTypeInstanceHandler(userId).setDefaultValues(
                    propState, data.getNodeState(), propDef);
        } 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 = stateMgr.createTransientNodeState(
                id, nodeType.getQName(), getNodeId(), ItemState.STATUS_NEW);

        // 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;
    }

    /**
     *
     * @param oldName
     * @param index
     * @param id
     * @param newName
     * @throws RepositoryException
     * @deprecated use #renameChildNode(NodeId, Name, boolean)
     */
    protected void renameChildNode(Name oldName, int index, NodeId id,
                                   Name newName)
            throws RepositoryException {
        renameChildNode(id, newName, false);
    }

    /**
     *
     * @param id
     * @param newName
     * @param replace
     * @throws RepositoryException
     */
    protected void renameChildNode(NodeId id, Name newName, boolean replace)
            throws RepositoryException {
        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        if (replace) {
            // rename the specified child node by replacing the old
            // child node entry with a new one at the same relative position
            thisState.replaceChildNodeEntry(id, newName, id);
        } else {
            // rename the specified child node by removing the old and adding
            // a new child node entry.
            thisState.renameChildNodeEntry(id, newName);
        }
    }

    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(NodeId childId) throws RepositoryException {
        // modify the state of 'this', i.e. the parent node
        NodeState thisState = (NodeState) getOrCreateTransientItemState();
        ChildNodeEntry entry = thisState.getChildNodeEntry(childId);
        if (entry == null) {
            String msg = "failed to remove child " + childId + " of " + this;
            log.debug(msg);
            throw new RepositoryException(msg);
        }

        // notify target of removal
        try {
            NodeImpl childNode = itemMgr.getNode(childId, getNodeId());
            childNode.onRemove(getNodeId());
        } catch (ItemNotFoundException e) {
            boolean ignoreError = false;
            if (sessionContext.getSessionImpl().autoFixCorruptions()) {
                // it might be an access right problem
                // we need to check if the item doesn't exist in the ism
                ItemStateManager ism = sessionContext.getItemStateManager();
                if (!ism.hasItemState(childId)) {
                    log.warn("Node " + childId + " not found, ignore", e);
                    ignoreError = true;
                }
            }
            if (!ignoreError) {
                throw e;
            }
        }

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

    protected void onRedefine(QNodeDefinition def) throws RepositoryException {
        NodeDefinitionImpl newDef =
            sessionContext.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);
                try {
                    /* omit the read-permission check upon retrieving the
                       child item as this is an internal call to remove the
                       subtree which may contain (protected) child items which
                       are not visible to the caller of the removal. the actual
                       validation of the remove permission however is only
                       executed during Item.save(). 
                     */
                    NodeImpl childNode = itemMgr.getNode(childId, getNodeId(), false);
                    childNode.onRemove(thisState.getNodeId());
                    // remove the child node entry
                } catch (ItemNotFoundException e) {
                    boolean ignoreError = false;
                    if (parentId != null && sessionContext.getSessionImpl().autoFixCorruptions()) {
                        // it might be an access right problem
                        // we need to check if the item doesn't exist in the ism
                        ItemStateManager ism = sessionContext.getItemStateManager();
                        if (!ism.hasItemState(childId)) {
                            log.warn("Child named " + entry.getName() + " (index " + entry.getIndex() + ", " +
                                    "node id " + childId + ") " +
                                    "not found when trying to remove " + getPath() + " " +
                                    "(node id " + getNodeId() + ") - ignored", e);
                            ignoreError = true;
                        }
                    }
                    if (!ignoreError) {
                        throw e;
                    }
                }
                thisState.removeChildNodeEntry(childId);
            }
        }

        // 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);
            /* omit the read-permission check upon retrieving the
               child item as this is an internal call to remove the
               subtree which may contain (protected) child items which
               are not visible to the caller of the removal. the actual
               validation of the remove permission however is only
               executed during Item.save().
             */
            itemMgr.getItem(propId, false).setRemoved();
        }

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

    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 {
            return sessionContext.getNodeTypeRegistry().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 = sessionContext.getNodeTypeManager();
        QNodeDefinition cnd = getEffectiveNodeType().getApplicableChildNodeDef(
                nodeName, nodeTypeName, sessionContext.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 sessionContext.getNodeTypeManager().getPropertyDefinition(pd);
    }

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

        NodeState transientState = data.getNodeState();
        NodeState localState = stateMgr.makePersistent(transientState);

        // swap transient state with local state
        data.setState(localState);
        // reset status
        data.setStatus(STATUS_NORMAL);

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

    protected void restoreTransient(NodeState transientState)
            throws RepositoryException {
        NodeState thisState = null;

        if (!isTransient()) {
            thisState = (NodeState) getOrCreateTransientItemState();
            if (transientState.getStatus() == ItemState.STATUS_NEW
                    && thisState.getStatus() != ItemState.STATUS_NEW) {
                thisState.setStatus(ItemState.STATUS_NEW);
                stateMgr.disconnectTransientItemState(thisState);
            }
            thisState.setParentId(transientState.getParentId());
            thisState.setNodeTypeName(transientState.getNodeTypeName());
        } else {
            // JCR-2503: Re-create transient state in the state manager,
            // because it was removed
            synchronized (data) {
                thisState = stateMgr.createTransientNodeState(
                        (NodeId) transientState.getId(),
                        transientState.getNodeTypeName(),
                        transientState.getParentId(),
                        NodeState.STATUS_NEW);
                data.setState(thisState);
            }
        }

        // re-apply transient changes
        thisState.setMixinTypeNames(transientState.getMixinTypeNames());
        thisState.setChildNodeEntries(transientState.getChildNodeEntries());
        thisState.setPropertyNames(transientState.getPropertyNames());
        thisState.setSharedSet(transientState.getSharedSet());
        thisState.setModCount(transientState.getModCount());
    }

    /**
     * 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 RepositoryException {
        perform(new AddMixinOperation(this, mixinName));
    }

    /**
     * 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 RepositoryException {
        perform(new RemoveMixinOperation(this, mixinName));
    }

    /**
     * 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 {
        // 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 = sessionContext.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;
        sessionContext.getItemValidator().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(final Name name, final int index)
            throws ItemNotFoundException, RepositoryException {
        return perform(new SessionOperation<NodeImpl>() {
            public NodeImpl perform(SessionContext context)
                    throws RepositoryException {
                ChildNodeEntry cne = data.getNodeState().getChildNodeEntry(
                        name, index != 0 ? index : 1);
                if (cne != null) {
                    try {
                        return context.getItemManager().getNode(
                                cne.getId(), getNodeId());
                    } catch (AccessDeniedException e) {
                        throw new ItemNotFoundException();
                    }
                } else {
                    throw new ItemNotFoundException();
                }
            }
            public String toString() {
                return "node.getNode(" + name + "[" + index + "])";
            }
        });
    }

    /**
     * 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(final Name name, final int index)
            throws RepositoryException {
        return perform(new SessionOperation<Boolean>() {
            public Boolean perform(SessionContext context)
                    throws RepositoryException {
                ChildNodeEntry cne = data.getNodeState().getChildNodeEntry(
                        name, index != 0 ? index : 1);
                return cne != null
                    && context.getItemManager().itemExists(cne.getId());
            }
            public String toString() {
                return "node.hasNode(" + name + "[" + index + "])";
            }
        });
    }

    /**
     * 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(final Name name)
            throws ItemNotFoundException, RepositoryException {
        return perform(new SessionOperation<PropertyImpl>() {
            public PropertyImpl perform(SessionContext context)
                    throws RepositoryException {
                try {
                    return (PropertyImpl) context.getItemManager().getItem(
                            new PropertyId(getNodeId(), name));
                } catch (AccessDeniedException ade) {
                    String n = context.getJCRName(name);
                    throw new ItemNotFoundException(
                            "Property " + n + " not found");
                }
            }
            public String toString() {
                return "node.getProperty(" + name + ")";
            }
        });
    }

    /**
     * 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(final Name name) throws RepositoryException {
        return perform(new SessionOperation<Boolean>() {
            public Boolean perform(SessionContext context)
                    throws RepositoryException {
                return data.getNodeState().hasPropertyName(name)
                    && context.getItemManager().itemExists(
                            new PropertyId(getNodeId(), name));
            }
            public String toString() {
                return "node.hasProperty(" + name + ")";
            }
        });
    }

    /**
     * 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
     */
    // FIXME: This method should not be public
    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 = sessionContext.getNodeTypeManager().getNodeType(nodeTypeName);
            if (nt.isMixin()) {
                throw new ConstraintViolationException(
                        "Unable to add a node with a mixin node type: "
                        + sessionContext.getJCRName(nodeTypeName));
            } else if (nt.isAbstract()) {
                throw new ConstraintViolationException(
                        "Unable to add a node with an abstract node type: "
                        + sessionContext.getJCRName(nodeTypeName));
            } else {
                // adding a node with explicit specifying the node type name
                // requires the editing session to have nt_management privilege.
                sessionContext.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 "
                    + sessionContext.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;
        sessionContext.getItemValidator().checkModify(this, options, Permission.NONE);

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

    /**
     * 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>.
     */
    public PropertyImpl setProperty(Name name, Value value)
            throws RepositoryException {
        return sessionContext.getSessionState().perform(
                new SetPropertyOperation(name, value, false));
    }

    /**
     * @see ItemImpl#getQName()
     */
    @Override
    public Name getQName() throws RepositoryException {
        HierarchyManager hierMgr = sessionContext.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;
    }

    /**
     * Returns the name of the primary node type as exposed on the node state
     * without retrieving the node type.
     *
     * @return the name of the primary node type.
     */
    public Name getPrimaryNodeTypeName() {
        return data.getNodeState().getNodeTypeName();
    }

    /**
     * Test if this node is access controlled. The node is access controlled if
     * it is of node type
     * {@link org.apache.jackrabbit.core.security.authorization.AccessControlConstants#NT_REP_ACCESS_CONTROLLABLE "rep:AccessControllable"}
     * and if it has a child node named
     * {@link org.apache.jackrabbit.core.security.authorization.AccessControlConstants#N_POLICY}.
     *
     * @return <code>true</code> if this node is access controlled and has a
     * rep:policy child; <code>false</code> otherwise.
     * @throws RepositoryException if an error occurs
     */
    public boolean isAccessControllable() throws RepositoryException {
        return data.getNodeState().hasChildNodeEntry(NameConstants.REP_POLICY, 1)
                    && isNodeType(NameConstants.REP_ACCESS_CONTROLLABLE);
    }

    /**
     * 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 = sessionContext.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 = sessionContext.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;
        sessionContext.getItemValidator().checkModify(this, options, Permission.NONE);

        /*
        make sure the session is allowed to reorder child nodes.
        since there is no specific privilege for reordering child nodes,
        test if the the node to be reordered can be removed and added,
        i.e. treating reorder similar to a move.
        TODO: properly deal with sns in which case the index would change upon reorder.
        */
        AccessManager acMgr = sessionContext.getAccessManager();
        PathBuilder pb = new PathBuilder(getPrimaryPath());
        pb.addLast(srcName.getName(), srcName.getIndex());
        Path childPath = pb.getPath();
        if (!acMgr.isGranted(childPath, Permission.MODIFY_CHILD_NODE_COLLECTION)) {
            String msg = "Not allowed to reorder child node " + sessionContext.getJCRPath(childPath) + ".";
            log.debug(msg);
            throw new AccessDeniedException(msg);
        }

        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;
        sessionContext.getItemValidator().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}
     */
    @Override
    public boolean isNode() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getName() throws RepositoryException {
        return perform(new SessionOperation<String>() {
            public String perform(SessionContext context)
                    throws RepositoryException {
                NodeId parentId = data.getNodeState().getParentId();
                if (parentId == null) {
                    return ""; // this is the root node
                }

                Name name;
                if (!isShareable()) {
                    name = context.getHierarchyManager().getName(id);
                } else {
                    name = context.getHierarchyManager().getName(
                            getNodeId(), parentId);
                }
                return context.getJCRName(name);
            }
            public String toString() {
                return "node.getName()";
            }
        });
    }

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

        visitor.visit(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getParent() throws RepositoryException {
        return perform(new SessionOperation<Node>() {
            public Node perform(SessionContext context)
                    throws RepositoryException {
                NodeId parentId = getParentId();
                if (parentId != null) {
                    return (Node) context.getItemManager().getItem(parentId);
                } else {
                    throw new ItemNotFoundException(
                            "Root node doesn't have a parent");
                }
            }
            public String toString() {
                return "node.getParent()";
            }
        });
    }

    //----------------------------------------------------------------< 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 Node addNodeWithUuid(
            String relPath, String nodeTypeName, String uuid)
            throws RepositoryException {
        return perform(new AddNodeOperation(this, relPath, nodeTypeName, uuid));
    }

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

        Path.Element insertName;
        try {
            Path p = sessionContext.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 = sessionContext.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);
    }

    /** Wrapper around {@link #setProperty(Name, Value[], int boolean)} */
    public Property setProperty(String name, Value[] values)
            throws RepositoryException {
        return setProperty(getQName(name), values, getType(values), false);
    }

    /** Wrapper around {@link #setProperty(Name, Value[], int boolean)} */
    public Property setProperty(String name, Value[] values, int type)
            throws RepositoryException {
        return setProperty(getQName(name), values, type, true);
    }

    /** Wrapper around {@link #setProperty(Name, Value[], int boolean)} */
    public Property setProperty(String name, String[] strings)
            throws RepositoryException {
        Value[] values = getValues(strings, STRING);
        return setProperty(getQName(name), values, STRING, false);
    }

    /** Wrapper around {@link #setProperty(Name, Value[], int, boolean)} */
    public Property setProperty(String name, String[] values, int type)
            throws RepositoryException {
        Value[] converted = getValues(values, type);
        return setProperty(sessionContext.getQName(name), converted, type, true);
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, String value)
            throws RepositoryException {
        if (value != null) {
            return setProperty(name, getValueFactory().createValue(value));
        } else {
            return setProperty(name, (Value) null);
        }
    }

    /** Wrapper around {@link #setProperty(String, Value, int)} */
    public Property setProperty(String name, String value, int type)
            throws RepositoryException {
        if (value != null) {
            return setProperty(
                    name, getValueFactory().createValue(value, type), type);
        } else {
            return setProperty(name, (Value) null, type);
        }
    }

    /** Wrapper around {@link SetPropertyOperation} */
    public Property setProperty(String name, Value value, int type)
            throws RepositoryException {
        if (value != null && value.getType() != type) {
            value = ValueHelper.convert(value, type, getValueFactory());
        }
        return sessionContext.getSessionState().perform(
                new SetPropertyOperation(sessionContext.getQName(name), value, true));
    }

    /** Wrapper around {@link SetPropertyOperation} */
    public Property setProperty(String name, Value value)
            throws RepositoryException {
        return sessionContext.getSessionState().perform(
                new SetPropertyOperation(sessionContext.getQName(name), value, false));
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, InputStream value)
            throws RepositoryException {
        if (value != null) {
            Binary binary = getValueFactory().createBinary(value);
            try {
                return setProperty(name, getValueFactory().createValue(binary));
            } finally {
                binary.dispose();
            }
        } else {
            return setProperty(name, (Value) null);
        }
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, boolean value)
            throws RepositoryException {
        return setProperty(name, getValueFactory().createValue(value));
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, double value)
            throws RepositoryException {
        return setProperty(name, getValueFactory().createValue(value));
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, long value)
            throws RepositoryException {
        return setProperty(name, getValueFactory().createValue(value));
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, Calendar value)
            throws RepositoryException {
        if (value != null) {
            try {
                return setProperty(name, getValueFactory().createValue(value));
            } catch (IllegalArgumentException e) {
                throw new ValueFormatException(
                        "Value is not an ISO8601 date: " + value, e);
            }
        } else {
            return setProperty(name, (Value) null);
        }
    }

    /** Wrapper around {@link #setProperty(String, Value)} */
    public Property setProperty(String name, Node value)
            throws RepositoryException {
        if (value != null) {
            try {
                return setProperty(name, getValueFactory().createValue(value));
            } catch (UnsupportedRepositoryOperationException e) {
                throw new ValueFormatException(
                        "Node is not referenceable: " + value, e);
            }
        } else {
            return setProperty(name, (Value) null);
        }
    }

    /**
     * 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.
     */
    private class SetPropertyOperation implements SessionWriteOperation<PropertyImpl> {

        private final Name name;

        private final Value value;

        private final boolean enforceType;

        /**
         * @param name  property name
         * @param value new value of the property,
         *              or <code>null</code> to remove the property
         * @param enforceType <code>true</code> to enforce the value type
         */
        public SetPropertyOperation(
                Name name, Value value, boolean enforceType) {
            this.name = name;
            this.value = value;
            this.enforceType = enforceType;
        }

        /**
         * @return the <code>Property</code> object set,
         *         or <code>null</code> if this operation 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.
         */
        public PropertyImpl perform(SessionContext context)
                throws RepositoryException {
            itemSanityCheck();
            // check pre-conditions for setting property
            checkSetProperty();

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

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

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

        /**
         * Returns a string representation of this operation.
         */
        public String toString() {
            return "node.setProperty(" + name + ", " + value + ")";
        }

    }

    /**
     * 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(
            final Name name, final Value[] values, final int type,
            final boolean enforceType) throws RepositoryException {
        return perform(new SessionOperation<PropertyImpl>() {
            public PropertyImpl perform(SessionContext context)
                    throws RepositoryException {
                // 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;
            }
            public String toString() {
                return "node.setProperty(...)";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public Node getNode(final String relPath) throws RepositoryException {
        return perform(new SessionOperation<Node>() {
            public Node perform(SessionContext context)
                    throws RepositoryException {
                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 the node is shareable, it now returns the node
                    // with the right parent
                    if (parentId != null) {
                        return itemMgr.getNode(id, parentId);
                    } else {
                        return (NodeImpl) itemMgr.getItem(id);
                    }
                } catch (AccessDeniedException e) {
                    throw new PathNotFoundException(relPath);
                } catch (ItemNotFoundException e) {
                    throw new PathNotFoundException(relPath);
                }
            }
            public String toString() {
                return "node.getNode(" + relPath + ")";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public NodeIterator getNodes() throws RepositoryException {
        // 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!
        return perform(new SessionOperation<NodeIterator>() {
            public NodeIterator perform(SessionContext context)
                    throws RepositoryException {
                try {
                    return itemMgr.getChildNodes((NodeId) id);
                } catch (ItemNotFoundException e) {
                    throw new RepositoryException(
                            "Failed to list child nodes of " + NodeImpl.this, e);
                } catch (AccessDeniedException e) {
                    throw new RepositoryException(
                            "Failed to list child nodes of " + NodeImpl.this, e);
                }
            }
            public String toString() {
                return "node.getNodes()";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public PropertyIterator getProperties() throws RepositoryException {
        // 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!
        return perform(new SessionOperation<PropertyIterator>() {
            public PropertyIterator perform(SessionContext context)
                    throws RepositoryException {
                try {
                    return itemMgr.getChildProperties((NodeId) id);
                } catch (ItemNotFoundException e) {
                    throw new RepositoryException(
                            "Failed to list properties of " + NodeImpl.this, e);
                } catch (AccessDeniedException e) {
                    throw new RepositoryException(
                            "Failed to list properties of " + NodeImpl.this, e);
                }
            }
            public String toString() {
                return "node.getProperties()";
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    public Property getProperty(final String relPath)
            throws PathNotFoundException, RepositoryException {
        return perform(new SessionOperation<Property>() {
            public Property perform(SessionContext context)
                    throws RepositoryException {
                PropertyId id = resolveRelativePropertyPath(relPath);
                if (id != null) {
                    try {
                        return (Property) itemMgr.getItem(id);
                    } catch (ItemNotFoundException e) {
                        throw new PathNotFoundException(relPath);
                    } catch (AccessDeniedException e) {
                        throw new PathNotFoundException(relPath);
                    }
                } else {
                    throw new PathNotFoundException(relPath);
                }
            }
            public String toString() {
                return "node.getProperty(" + 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 {
        // check state of this instance
        sanityCheck();

        try {
            return isNodeType(sessionContext.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 sessionContext.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++] = sessionContext.getNodeTypeManager().getNodeType(iter.next());
        }
        return nta;
    }

    /** Wrapper around {@link #addMixin(Name)}. */
    public void addMixin(String mixinName) throws RepositoryException {
        try {
            addMixin(sessionContext.getQName(mixinName));
        } catch (NameException e) {
            throw new RepositoryException(
                    "Invalid mixin type name: " + mixinName, e);
        }
    }

    /** Wrapper around {@link #removeMixin(Name)}. */
    public void removeMixin(String mixinName) throws RepositoryException {
        try {
            removeMixin(sessionContext.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 = sessionContext.getQName(mixinName);
        NodeTypeManagerImpl ntMgr = sessionContext.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 (!sessionContext.getItemValidator().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)
            RepositoryImpl rep = (RepositoryImpl) getSession().getRepository();
            srcSession = rep.createSession(
                    sessionContext.getSessionImpl().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 = sessionContext.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
     */
    public 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 = sessionContext.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
     */
    @Override
    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)
                    sessionContext.getItemStateManager().getItemState(parentId);
            }
            PropertyId id = new PropertyId(state.getNodeId(), JCR_ISCHECKEDOUT);
            PropertyState ps =
                (PropertyState) sessionContext.getItemStateManager().getItemState(id);
            InternalValue[] values = ps.getValues();
            if (values == null || values.length != 1) {
                // the property is not fully set, or it is a multi-valued property
                // in which case it's probably not mix:versionable
                return true;
            }
            return values[0].getBoolean();
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * Returns the version manager of this workspace.
     */
    private VersionManagerImpl getVersionManagerImpl() {
        return sessionContext.getWorkspace().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 = getSession().getWorkspace().getLockManager();
        return lockMgr.lock(getPath(), isDeep, isSessionScoped,
                sessionContext.getWorkspace().getConfig().getDefaultLockTimeout(), null);
    }

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

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

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

    /**
     * {@inheritDoc}
     */
    public boolean isLocked() throws RepositoryException {
        // check state of this instance
        sanityCheck();
        LockManager lockMgr = getSession().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;
        }
        sessionContext.getWorkspace().getInternalLockManager().checkLock(this);
    }

    //--------------------------------------------------< new JSR 283 methods >
    /**
     * {@inheritDoc}
     */
    public String getIdentifier() throws RepositoryException {
        return 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 = sessionContext.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(sessionContext, 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) getSession().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 = getSession().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;
        sessionContext.getItemValidator().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 = sessionContext.getQName(nodeTypeName);
        if (ntName.equals(state.getNodeTypeName())) {
            log.debug("Node already has " + nodeTypeName + " as primary node type.");
            return;
        }

        NodeTypeManagerImpl ntMgr = sessionContext.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(),
                                                getSession().getValueFactory());
                                // redefine property
                                prop.onRedefine(pdi.unwrap());
                                // set converted values
                                prop.setValue(values);
                            } else {
                                // convert value
                                Value value =
                                        ValueHelper.convert(
                                                prop.getValue(),
                                                pdi.getRequiredType(),
                                                getSession().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.getId());
                            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.getId());
                    }
                }
            } 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 = getSession().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 = getSession().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));
    }

    //-------------------------------------------------------< JackrabbitNode >

    /**
     * {@inheritDoc}
     */
    public void rename(String newName) throws RepositoryException {
        // check if this is the root node
        if (getDepth() == 0) {
            throw new RepositoryException("Cannot rename the root node");
        }

        Name qName;
        try {
            qName = sessionContext.getQName(newName);
        } catch (NameException e) {
            throw new RepositoryException("invalid node name: " + newName, e);
        }

        NodeImpl parent = (NodeImpl) getParent();

        // check for name collisions
        NodeImpl existing = null;
        try {
            existing = parent.getNode(qName);
            // there's already a node with that name:
            // check same-name sibling setting of existing node
            if (!existing.getDefinition().allowsSameNameSiblings()) {
                throw new ItemExistsException(
                        "Same name siblings are not allowed: " + existing);
            }
        } catch (AccessDeniedException ade) {
            // FIXME by throwing ItemExistsException we're disclosing too much information
            throw new ItemExistsException();
        } catch (ItemNotFoundException infe) {
            // no name collision, fall through
        }

        // verify that parent node
        // - is checked-out
        // - is not protected neither by node type constraints nor by retention/hold
        int options = ItemValidator.CHECK_CHECKED_OUT | ItemValidator.CHECK_LOCK |
        ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD | ItemValidator.CHECK_RETENTION;
        sessionContext.getItemValidator().checkRemove(parent, options, Permission.NONE);
        sessionContext.getItemValidator().checkModify(parent, options, Permission.NONE);

        // check constraints
        // get applicable definition of renamed target node
        NodeTypeImpl nt = (NodeTypeImpl) getPrimaryNodeType();
        org.apache.jackrabbit.spi.commons.nodetype.NodeDefinitionImpl newTargetDef;
        try {
            newTargetDef = parent.getApplicableChildNodeDefinition(qName, nt.getQName());
        } catch (RepositoryException re) {
            String msg = safeGetJCRPath() + ": no definition found in parent node's node type for renamed node";
            log.debug(msg);
            throw new ConstraintViolationException(msg, re);
        }
        // if there's already a node with that name also check same-name sibling
        // setting of new node; just checking same-name sibling setting on
        // existing node is not sufficient since same-name sibling nodes don't
        // necessarily have identical definitions
        if (existing != null && !newTargetDef.allowsSameNameSiblings()) {
            throw new ItemExistsException(
                    "Same name siblings not allowed: " + existing);
        }

        // check permissions:
        // 1. on the parent node the session must have permission to manipulate the child-entries
        AccessManager acMgr = sessionContext.getAccessManager();
        if (!acMgr.isGranted(parent.getPrimaryPath(), qName, Permission.MODIFY_CHILD_NODE_COLLECTION)) {
            String msg = "Not allowed to rename node " + safeGetJCRPath() + " to " + newName;
            log.debug(msg);
            throw new AccessDeniedException(msg);
        }
        // 2. in case of nt-changes the session must have permission to change
        //    the primary node type on this node itself.
        if (!nt.getName().equals(newTargetDef.getName()) && !(acMgr.isGranted(getPrimaryPath(), Permission.NODE_TYPE_MNGMT))) {
            String msg = "Not allowed to rename node " + safeGetJCRPath() + " to " + newName;
            log.debug(msg);
            throw new AccessDeniedException(msg);
        }

        // change definition
        onRedefine(newTargetDef.unwrap());

        // delegate to parent
        parent.renameChildNode(getNodeId(), qName, true);
    }

    /**
     * {@inheritDoc}
     */
    public void setMixins(String[] mixinNames)
            throws NoSuchNodeTypeException, VersionException,
            ConstraintViolationException, LockException, RepositoryException {

        // check state of this instance
        sanityCheck();

        NodeTypeManagerImpl ntMgr = sessionContext.getNodeTypeManager();

        Set<Name> newMixins = new HashSet<Name>();
        for (String name : mixinNames) {
            Name qName = sessionContext.getQName(name);
            if (! ntMgr.getNodeType(qName).isMixin()) {
                throw new RepositoryException(
                        sessionContext.getJCRName(qName) + " is not a mixin node type");
            }
            newMixins.add(qName);
        }

        // make sure this node is checked-out, neither protected nor locked and
        // the editing session has sufficient permission to change the mixin types.

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

        final NodeState state = data.getNodeState();

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

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

        // added child item definitions
        Set<QItemDefinition> addedDefs = new HashSet<QItemDefinition>(Arrays.asList(entNew.getAllItemDefs()));
        addedDefs.removeAll(Arrays.asList(entOld.getAllItemDefs()));

        // referential integrity check
        boolean referenceableOld = getEffectiveNodeType().includesNodeType(NameConstants.MIX_REFERENCEABLE);
        boolean referenceableNew = entAll.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 mixin types 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");
            }
        }

        // gather currently assigned definitions *before* doing actual modifications
        Map<ItemId, ItemDefinition> oldDefs = new HashMap<ItemId, ItemDefinition>();
        for (Name name : getNodeState().getPropertyNames()) {
            PropertyId id = new PropertyId(getNodeId(), name);
            try {
                PropertyState propState = (PropertyState) stateMgr.getItemState(id);
                oldDefs.put(id, itemMgr.getDefinition(propState));
            } catch (ItemStateException ise) {
                String msg = name + ": failed to retrieve property state";
                log.error(msg, ise);
                throw new RepositoryException(msg, ise);
            }
        }
        for (ChildNodeEntry cne : getNodeState().getChildNodeEntries()) {
            try {
                NodeState nodeState = (NodeState) stateMgr.getItemState(cne.getId());
                oldDefs.put(cne.getId(), itemMgr.getDefinition(nodeState));
            } catch (ItemStateException ise) {
                String msg = cne + ": failed to retrieve node state";
                log.error(msg, ise);
                throw new RepositoryException(msg, ise);
            }
        }

        // now do the actual modifications in content as mandated by the new mixins

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

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

        // 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) {
            PropertyState propState = null;
            try {
                propState = (PropertyState) stateMgr.getItemState(
                                new PropertyId(thisState.getNodeId(), propName));
                // the following call triggers ConstraintViolationException
                // if there isn't any suitable definition anymore
                itemMgr.getDefinition(propState);
            } catch (ConstraintViolationException cve) {
                // no suitable definition found for this property
                // try to find new applicable definition first and
                // redefine property if possible
                try {
                    if (oldDefs.get(propState.getId()).isProtected()) {
                        // remove 'orphaned' protected properties immediately
                        removeChildProperty(propName);
                        continue;
                    }
                    PropertyDefinitionImpl pdi = getApplicablePropertyDefinition(
                            propName, propState.getType(),
                            propState.isMultiValued(), false);
                    PropertyImpl prop = (PropertyImpl) itemMgr.getItem(propState.getId());
                    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(),
                                            getSession().getValueFactory());
                            // redefine property
                            prop.onRedefine(pdi.unwrap());
                            // set converted values
                            prop.setValue(values);
                        } else {
                            // convert value
                            Value value =
                                    ValueHelper.convert(
                                            prop.getValue(),
                                            pdi.getRequiredType(),
                                            getSession().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 cve1) {
                    // 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);
            NodeState nodeState = null;
            try {
                nodeState = (NodeState) stateMgr.getItemState(entry.getId());
                // the following call triggers ConstraintViolationException
                // if there isn't any suitable definition anymore
                itemMgr.getDefinition(nodeState);
            } catch (ConstraintViolationException cve) {
                // no suitable definition found for this child node
                // try to find new applicable definition first and
                // redefine node if possible
                try {
                    if (oldDefs.get(nodeState.getId()).isProtected()) {
                        // remove 'orphaned' protected child node immediately
                        removeChildNode(entry.getId());
                        continue;
                    }
                    NodeDefinitionImpl ndi = getApplicableChildNodeDefinition(
                            entry.getName(),
                            nodeState.getNodeTypeName());
                    NodeImpl node = (NodeImpl) itemMgr.getItem(nodeState.getId());
                    // redefine node
                    node.onRedefine(ndi.unwrap());
                    // update collection of added definitions
                    addedDefs.remove(ndi.unwrap());
                } catch (ConstraintViolationException cve1) {
                    // no suitable definition found for this child node,
                    // remove it
                    removeChildNode(entry.getId());
                }
            } catch (ItemStateException ise) {
                String msg = entry + ": 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 mixins
        // and at the same time were not present with the old mixins
        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);
                }
            }
        }
    }

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

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

}
