org.apache.jackrabbit.core.ItemImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jackrabbit.core.ItemImpl.java

Source

/*
 * 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 org.apache.commons.collections.iterators.IteratorChain;
import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.core.nodetype.NodeDef;
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.nodetype.PropDef;
import org.apache.jackrabbit.core.nodetype.PropertyDefinitionImpl;
import org.apache.jackrabbit.core.security.AccessManager;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.PropertyState;
import org.apache.jackrabbit.core.state.SessionItemStateManager;
import org.apache.jackrabbit.core.state.StaleItemStateException;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.version.VersionHistoryInfo;
import org.apache.jackrabbit.core.version.VersionManager;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.util.Text;
import org.apache.jackrabbit.uuid.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.AccessDeniedException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ItemVisitor;
import javax.jcr.Node;
import javax.jcr.PathNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.lock.LockException;
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.version.VersionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

/**
 * <code>ItemImpl</code> implements the <code>Item</code> interface.
 */
public abstract class ItemImpl implements Item {

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

    protected static final int STATUS_NORMAL = 0;
    protected static final int STATUS_MODIFIED = 1;
    protected static final int STATUS_DESTROYED = 2;
    protected static final int STATUS_INVALIDATED = 3;

    protected final ItemId id;

    /**
     * <code>Session</code> through which this <code>Item</code> was acquired
     */
    protected final SessionImpl session;

    /**
     * the <code>Repository</code> object
     */
    protected final RepositoryImpl rep;

    /**
     * Item data associated with this item.
     */
    protected final ItemData data;

    /**
     * <code>ItemManager</code> that created this <code>Item</code>
     */
    protected final ItemManager itemMgr;

    /**
     * <code>SessionItemStateManager</code> associated with this <code>Item</code>
     */
    protected final SessionItemStateManager stateMgr;

    /**
     * Package private constructor.
     *
     * @param itemMgr   the <code>ItemManager</code> that created this <code>Item</code>
     * @param session   the <code>Session</code> through which this <code>Item</code> is acquired
     * @param data      ItemData of this <code>Item</code>
     */
    ItemImpl(ItemManager itemMgr, SessionImpl session, ItemData data) {
        this.session = session;
        rep = (RepositoryImpl) session.getRepository();
        stateMgr = session.getItemStateManager();
        this.id = data.getId();
        this.itemMgr = itemMgr;
        this.data = data;
    }

    /**
     * Performs a sanity check on this item and the associated session.
     *
     * @throws RepositoryException if this item has been rendered invalid for some reason
     */
    protected void sanityCheck() throws RepositoryException {
        // check session status
        session.sanityCheck();

        // check status of this item for read operation
        final int status = data.getStatus();
        if (status == STATUS_DESTROYED || status == STATUS_INVALIDATED) {
            throw new InvalidItemStateException(id + ": the item does not exist anymore");
        }
    }

    protected boolean isTransient() {
        return getItemState().isTransient();
    }

    protected abstract ItemState getOrCreateTransientItemState() throws RepositoryException;

    protected abstract void makePersistent() throws InvalidItemStateException;

    /**
     * Marks this instance as 'removed' and notifies its listeners.
     * The resulting state is either 'temporarily invalidated' or
     * 'permanently invalidated', depending on the initial state.
     *
     * @throws RepositoryException if an error occurs
     */
    protected void setRemoved() throws RepositoryException {
        final int status = data.getStatus();
        if (status == STATUS_INVALIDATED || status == STATUS_DESTROYED) {
            // this instance is already 'invalid', get outta here
            return;
        }

        ItemState transientState = getOrCreateTransientItemState();
        if (transientState.getStatus() == ItemState.STATUS_NEW) {
            // this is a 'new' item, simply dispose the transient state
            // (it is no longer used); this will indirectly (through
            // stateDiscarded listener method) invalidate this instance permanently
            stateMgr.disposeTransientItemState(transientState);
        } else {
            // this is an 'existing' item (i.e. it is backed by persistent
            // state), mark it as 'removed'
            transientState.setStatus(ItemState.STATUS_EXISTING_REMOVED);
            // transfer the transient state to the attic
            stateMgr.moveTransientItemStateToAttic(transientState);

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

    /**
     * Returns the item-state associated with this <code>Item</code>.
     *
     * @return state associated with this <code>Item</code>
     */
    ItemState getItemState() {
        return data.getState();
    }

    /**
     * Return the id of this <code>Item</code>.
     *
     * @return the id of this <code>Item</code>
     */
    public ItemId getId() {
        return id;
    }

    /**
     * Returns the primary path to this <code>Item</code>.
     *
     * @return the primary path to this <code>Item</code>
     */
    public Path getPrimaryPath() throws RepositoryException {
        return session.getHierarchyManager().getPath(id);
    }

    /**
     * Builds a list of transient (i.e. new or modified) item states that are
     * within the scope of <code>this.{@link #save()}</code>. The collection
     * returned is ordered depth-first, i.e. the item itself (if transient)
     * comes last.
     *
     * @return list of transient item states
     * @throws InvalidItemStateException
     * @throws RepositoryException
     */
    private Collection getTransientStates() throws InvalidItemStateException, RepositoryException {
        // list of transient states that should be persisted
        ArrayList dirty = new ArrayList();
        ItemState transientState;

        if (isNode()) {
            // build list of 'new' or 'modified' descendants
            Iterator iter = stateMgr.getDescendantTransientItemStates((NodeId) id);
            while (iter.hasNext()) {
                transientState = (ItemState) iter.next();
                // fail-fast test: check status of transient state
                switch (transientState.getStatus()) {
                case ItemState.STATUS_NEW:
                case ItemState.STATUS_EXISTING_MODIFIED:
                    // add modified state to the list
                    dirty.add(transientState);
                    break;

                case ItemState.STATUS_STALE_MODIFIED:
                    throw new InvalidItemStateException(
                            "Item cannot be saved because it has been " + "modified externally: " + this);

                case ItemState.STATUS_STALE_DESTROYED:
                    throw new InvalidItemStateException(
                            "Item cannot be saved because it has been " + "deleted externally: " + this);

                case ItemState.STATUS_UNDEFINED:
                    throw new InvalidItemStateException(
                            "Item cannot be saved; it seems to have been " + "removed externally: " + this);

                default:
                    log.warn("Unexpected item state status: " + transientState.getStatus() + " of " + this);
                    // ignore
                    break;
                }
            }
        }
        // fail-fast test: check status of this item's state
        if (isTransient()) {
            final ItemState state = getItemState();
            switch (state.getStatus()) {
            case ItemState.STATUS_EXISTING_MODIFIED:
                // add this item's state to the list
                dirty.add(state);
                break;

            case ItemState.STATUS_NEW:
                throw new RepositoryException("Cannot save a new item: " + this);

            case ItemState.STATUS_STALE_MODIFIED:
                throw new InvalidItemStateException(
                        "Item cannot be saved because it has been" + " modified externally: " + this);

            case ItemState.STATUS_STALE_DESTROYED:
                throw new InvalidItemStateException(
                        "Item cannot be saved because it has been" + " deleted externally:" + this);

            case ItemState.STATUS_UNDEFINED:
                throw new InvalidItemStateException(
                        "Item cannot be saved; it seems to have been" + " removed externally: " + this);

            default:
                log.warn("Unexpected item state status:" + state.getStatus() + " of " + this);
                // ignore
                break;
            }
        }

        return dirty;
    }

    /**
     * Builds a list of transient descendant item states in the attic
     * (i.e. those marked as 'removed') that are within the scope of
     * <code>this.{@link #save()}</code>.
     *
     * @return list of transient item states
     * @throws InvalidItemStateException
     * @throws RepositoryException
     */
    private Collection getRemovedStates() throws InvalidItemStateException, RepositoryException {
        ArrayList removed = new ArrayList();
        ItemState transientState;

        if (isNode()) {
            Iterator iter = stateMgr.getDescendantTransientItemStatesInAttic((NodeId) id);
            while (iter.hasNext()) {
                transientState = (ItemState) iter.next();
                // check if stale
                if (transientState.getStatus() == ItemState.STATUS_STALE_MODIFIED) {
                    String msg = transientState.getId()
                            + ": the item cannot be removed because it has been modified externally.";
                    log.debug(msg);
                    throw new InvalidItemStateException(msg);
                }
                if (transientState.getStatus() == ItemState.STATUS_STALE_DESTROYED) {
                    String msg = transientState.getId()
                            + ": the item cannot be removed because it has already been deleted externally.";
                    log.debug(msg);
                    throw new InvalidItemStateException(msg);
                }
                removed.add(transientState);
            }
        }
        return removed;
    }

    private void validateTransientItems(Iterator dirtyIter, Iterator removedIter)
            throws AccessDeniedException, ConstraintViolationException, RepositoryException {
        /**
         * the following validations/checks are performed on transient items:
         *
         * for every transient item:
         * - if it is 'modified' or 'new' check the corresponding write permission.
         * - if it is 'removed' check the REMOVE permission
         *
         * for every transient node:
         * - if it is 'new' check that its node type satisfies the
         *   'required node type' constraint specified in its definition
         * - check if 'mandatory' child items exist
         *
         * for every transient property:
         * - check if the property value satisfies the value constraints
         *   specified in the property's definition
         *
         * note that the protected flag is checked in Node.addNode/Node.remove
         * (for adding/removing child entries of a node), in
         * Node.addMixin/removeMixin/setPrimaryType (for type changes on nodes)
         * and in Property.setValue (for properties to be modified).
         */
        AccessManager accessMgr = session.getAccessManager();
        NodeTypeManagerImpl ntMgr = session.getNodeTypeManager();
        // walk through list of dirty transient items and validate each
        while (dirtyIter.hasNext()) {
            ItemState itemState = (ItemState) dirtyIter.next();
            ItemDefinition def;
            if (itemState.isNode()) {
                def = itemMgr.getDefinition((NodeState) itemState);
            } else {
                def = itemMgr.getDefinition((PropertyState) itemState);
            }
            /* check permissions for non-protected items. protected items are
               only added through API methods which need to assert that
               permissions are not violated.
             */
            if (!def.isProtected()) {
                /* detect the effective set of modification:
                   - new added node -> add_node perm on the child
                   - new property added -> set_property permission
                   - property modified -> set_property permission
                   - modified nodes can be ignored for changes only included
                 child-item addition or removal or changes of protected
                 properties such as mixin-types which are covered separately
                   note: removed items are checked later on.
                */
                Path path = stateMgr.getHierarchyMgr().getPath(itemState.getId());
                boolean isGranted = true;
                if (itemState.isNode()) {
                    if (itemState.getStatus() == ItemState.STATUS_NEW) {
                        isGranted = accessMgr.isGranted(path, Permission.ADD_NODE);
                    } // else: modified node (see comment above)
                } else {
                    // modified or new property: set_property permission
                    isGranted = accessMgr.isGranted(path, Permission.SET_PROPERTY);
                }

                if (!isGranted) {
                    String msg = itemMgr.safeGetJCRPath(path) + ": not allowed to add or modify item";
                    log.debug(msg);
                    throw new AccessDeniedException(msg);
                }
            }

            if (itemState.isNode()) {
                // the transient item is a node
                NodeState nodeState = (NodeState) itemState;
                ItemId id = nodeState.getNodeId();
                NodeDefinition nodeDef = (NodeDefinition) def;
                // primary type
                NodeTypeImpl pnt = ntMgr.getNodeType(nodeState.getNodeTypeName());
                // effective node type (primary type incl. mixins)
                EffectiveNodeType ent = getEffectiveNodeType(nodeState);
                /**
                 * if the transient node was added (i.e. if it is 'new') or if
                 * its primary type has changed, check its node type against the
                 * required node type in its definition
                 */
                if (nodeState.getStatus() == ItemState.STATUS_NEW || !nodeState.getNodeTypeName()
                        .equals(((NodeState) nodeState.getOverlayedState()).getNodeTypeName())) {
                    NodeType[] nta = nodeDef.getRequiredPrimaryTypes();
                    for (int i = 0; i < nta.length; i++) {
                        NodeTypeImpl ntReq = (NodeTypeImpl) nta[i];
                        if (!(pnt.getQName().equals(ntReq.getQName()) || pnt.isDerivedFrom(ntReq.getQName()))) {
                            /**
                             * the transient node's primary node type does not
                             * satisfy the 'required primary types' constraint
                             */
                            String msg = itemMgr.safeGetJCRPath(id) + " must be of node type " + ntReq.getName();
                            log.debug(msg);
                            throw new ConstraintViolationException(msg);
                        }
                    }
                }

                // mandatory child properties
                PropDef[] pda = ent.getMandatoryPropDefs();
                for (int i = 0; i < pda.length; i++) {
                    PropDef pd = pda[i];
                    if (pd.getDeclaringNodeType().equals(NameConstants.MIX_VERSIONABLE)
                            || pd.getDeclaringNodeType().equals(NameConstants.MIX_SIMPLE_VERSIONABLE)) {
                        /**
                         * todo FIXME workaround for mix:versionable:
                         * the mandatory properties are initialized at a
                         * later stage and might not exist yet
                         */
                        continue;
                    }
                    if (!nodeState.hasPropertyName(pd.getName())) {
                        String msg = itemMgr.safeGetJCRPath(id) + ": mandatory property " + pd.getName()
                                + " does not exist";
                        log.debug(msg);
                        throw new ConstraintViolationException(msg);
                    }
                }
                // mandatory child nodes
                NodeDef[] cnda = ent.getMandatoryNodeDefs();
                for (int i = 0; i < cnda.length; i++) {
                    NodeDef cnd = cnda[i];
                    if (!nodeState.hasChildNodeEntry(cnd.getName())) {
                        String msg = itemMgr.safeGetJCRPath(id) + ": mandatory child node " + cnd.getName()
                                + " does not exist";
                        log.debug(msg);
                        throw new ConstraintViolationException(msg);
                    }
                }
            } else {
                // the transient item is a property
                PropertyState propState = (PropertyState) itemState;
                ItemId propId = propState.getPropertyId();
                PropertyDefinitionImpl propDef = (PropertyDefinitionImpl) def;

                /**
                 * check value constraints
                 * (no need to check value constraints of protected properties
                 * as those are set by the implementation only, i.e. they
                 * cannot be set by the user through the api)
                 */
                if (!def.isProtected()) {
                    String[] constraints = propDef.getValueConstraints();
                    if (constraints != null) {
                        InternalValue[] values = propState.getValues();
                        try {
                            EffectiveNodeType.checkSetPropertyValueConstraints(propDef.unwrap(), values);
                        } catch (RepositoryException e) {
                            // repack exception for providing more verbose error message
                            String msg = itemMgr.safeGetJCRPath(propId) + ": " + e.getMessage();
                            log.debug(msg);
                            throw new ConstraintViolationException(msg);
                        }

                        /**
                         * need to manually check REFERENCE value constraints
                         * as this requires a session (target node needs to
                         * be checked)
                         */
                        if (constraints.length > 0 && propDef.getRequiredType() == PropertyType.REFERENCE) {
                            for (int i = 0; i < values.length; i++) {
                                boolean satisfied = false;
                                String constraintViolationMsg = null;
                                try {
                                    UUID targetUUID = values[i].getUUID();
                                    Node targetNode = session.getNodeByUUID(targetUUID);
                                    /**
                                     * constraints are OR-ed, i.e. at least one
                                     * has to be satisfied
                                     */
                                    for (int j = 0; j < constraints.length; j++) {
                                        /**
                                         * a REFERENCE value constraint specifies
                                         * the name of the required node type of
                                         * the target node
                                         */
                                        String ntName = constraints[j];
                                        if (targetNode.isNodeType(ntName)) {
                                            satisfied = true;
                                            break;
                                        }
                                    }
                                    if (!satisfied) {
                                        NodeType[] mixinNodeTypes = targetNode.getMixinNodeTypes();
                                        String[] targetMixins = new String[mixinNodeTypes.length];
                                        for (int j = 0; j < mixinNodeTypes.length; j++) {
                                            targetMixins[j] = mixinNodeTypes[j].getName();
                                        }
                                        String targetMixinsString = Text.implode(targetMixins, ", ");
                                        String constraintsString = Text.implode(constraints, ", ");
                                        constraintViolationMsg = itemMgr.safeGetJCRPath(propId)
                                                + ": is constraint to [" + constraintsString
                                                + "] but references [primaryType="
                                                + targetNode.getPrimaryNodeType().getName() + ", mixins="
                                                + targetMixinsString + "]";
                                    }
                                } catch (RepositoryException re) {
                                    String msg = itemMgr.safeGetJCRPath(propId)
                                            + ": failed to check REFERENCE value constraint";
                                    log.debug(msg);
                                    throw new ConstraintViolationException(msg, re);
                                }
                                if (!satisfied) {
                                    log.debug(constraintViolationMsg);
                                    throw new ConstraintViolationException(constraintViolationMsg);
                                }
                            }
                        }
                    }
                }

                /**
                 * no need to check the protected flag as this is checked
                 * in PropertyImpl.setValue(Value)
                 */
            }
        }

        // walk through list of removed transient items and check REMOVE permission
        while (removedIter.hasNext()) {
            ItemState itemState = (ItemState) removedIter.next();
            ItemDefinition def;
            try {
                if (itemState.isNode()) {
                    def = itemMgr.getDefinition((NodeState) itemState);
                } else {
                    def = itemMgr.getDefinition((PropertyState) itemState);
                }
            } catch (ConstraintViolationException e) {
                // since identifier of assigned definition is not stored anymore
                // with item state (see JCR-2170), correct definition cannot be
                // determined for items which have been removed due to removal
                // of a mixin (see also JCR-2130 & JCR-2408)
                continue;
            }
            if (!def.isProtected()) {
                Path path = stateMgr.getAtticAwareHierarchyMgr().getPath(itemState.getId());
                // check REMOVE permission
                int permission = (itemState.isNode()) ? Permission.REMOVE_NODE : Permission.REMOVE_PROPERTY;
                if (!accessMgr.isGranted(path, permission)) {
                    String msg = itemMgr.safeGetJCRPath(path) + ": not allowed to remove item";
                    log.debug(msg);
                    throw new AccessDeniedException(msg);
                }
            }
        }
    }

    private void removeTransientItems(Iterator iter) {

        /**
         * walk through list of transient items marked 'removed' and
         * definitively remove each one
         */
        while (iter.hasNext()) {
            ItemState transientState = (ItemState) iter.next();
            ItemState persistentState = transientState.getOverlayedState();
            /**
             * remove persistent state
             *
             * this will indirectly (through stateDestroyed listener method)
             * permanently invalidate all Item instances wrapping it
             */
            stateMgr.destroy(persistentState);
        }
    }

    private void persistTransientItems(Iterator iter) throws RepositoryException {

        // walk through list of transient items and persist each one
        while (iter.hasNext()) {
            ItemState state = (ItemState) iter.next();
            ItemImpl item = itemMgr.getItem(state.getId());
            // persist state of transient item
            item.makePersistent();
        }
    }

    private void restoreTransientItems(Iterator iter) {
        // walk through list of transient states and re-apply transient changes
        while (iter.hasNext()) {
            ItemState itemState = (ItemState) iter.next();
            ItemId id = itemState.getId();
            ItemImpl item;

            try {
                if (stateMgr.isItemStateInAttic(id)) {
                    // If an item has been removed and then again created, the
                    // item is lost after persistTransientItems() and the
                    // TransientItemStateManager will bark because of a deleted
                    // state in its attic. We therefore have to forge a new item
                    // instance ourself.
                    item = itemMgr.createItemInstance(itemState);
                    itemState.setStatus(ItemState.STATUS_NEW);
                } else {
                    try {
                        item = itemMgr.getItem(id);
                    } catch (ItemNotFoundException infe) {
                        // itemState probably represents a 'new' item and the
                        // ItemImpl instance wrapping it has already been gc'ed;
                        // we have to re-create the ItemImpl instance
                        item = itemMgr.createItemInstance(itemState);
                        itemState.setStatus(ItemState.STATUS_NEW);
                    }
                }
                // re-apply transient changes
                // for persistent nodes undo effect of item.makePersistent()
                if (item.isNode()) {
                    NodeImpl node = (NodeImpl) item;
                    node.restoreTransient((NodeState) itemState);
                } else {
                    PropertyImpl prop = (PropertyImpl) item;
                    prop.restoreTransient((PropertyState) itemState);
                }
            } catch (RepositoryException re) {
                // something went wrong, log exception and carry on
                String msg = itemMgr.safeGetJCRPath(id) + ": failed to restore transient state";
                log.warn(msg, re);
            }
        }
    }

    /**
     * Process all items given in iterator and check whether <code>mix:shareable</code>
     * or (some derived node type) has been added or removed:
     * <ul>
     * <li>If the mixin <code>mix:shareable</code> (or some derived node type),
     * then initialize the shared set inside the state.</li>
     * <li>If the mixin <code>mix:shareable</code> (or some derived node type)
     * has been removed, throw.</li>
     * </ul>
     */
    private void processShareableNodes(Iterator iter) throws RepositoryException {
        while (iter.hasNext()) {
            ItemState is = (ItemState) iter.next();
            if (is.isNode()) {
                NodeState ns = (NodeState) is;
                boolean wasShareable = false;
                if (ns.hasOverlayedState()) {
                    NodeState old = (NodeState) ns.getOverlayedState();
                    EffectiveNodeType ntOld = getEffectiveNodeType(old);
                    wasShareable = ntOld.includesNodeType(NameConstants.MIX_SHAREABLE);
                }
                EffectiveNodeType ntNew = getEffectiveNodeType(ns);
                boolean isShareable = ntNew.includesNodeType(NameConstants.MIX_SHAREABLE);

                if (!wasShareable && isShareable) {
                    // mix:shareable has been added
                    ns.addShare(ns.getParentId());

                } else if (wasShareable && !isShareable) {
                    // mix:shareable has been removed: not supported
                    String msg = "Removing mix:shareable is not supported.";
                    log.debug(msg);
                    throw new UnsupportedRepositoryOperationException(msg);
                }
            }
        }
    }

    /**
     * Initializes the version history of all new nodes of node type
     * <code>mix:versionable</code>.
     * <p/>
     * Called by {@link #save()}.
     *
     * @param iter
     * @return true if this call generated new transient state; otherwise false
     * @throws RepositoryException
     */
    private boolean initVersionHistories(Iterator iter) throws RepositoryException {
        // walk through list of transient items and search for new versionable nodes
        boolean createdTransientState = false;
        while (iter.hasNext()) {
            ItemState itemState = (ItemState) iter.next();
            if (itemState.isNode()) {
                NodeState nodeState = (NodeState) itemState;
                EffectiveNodeType nt = getEffectiveNodeType(nodeState);
                if (nt.includesNodeType(NameConstants.MIX_VERSIONABLE)) {
                    if (!nodeState.hasPropertyName(NameConstants.JCR_VERSIONHISTORY)) {
                        NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId());
                        VersionManager vMgr = session.getVersionManager();
                        /**
                         * check if there's already a version history for that
                         * node; this would e.g. be the case if a versionable
                         * node had been exported, removed and re-imported with
                         * either IMPORT_UUID_COLLISION_REMOVE_EXISTING or
                         * IMPORT_UUID_COLLISION_REPLACE_EXISTING;
                         * otherwise create a new version history
                         */
                        VersionHistoryInfo history = vMgr.getVersionHistory(session, nodeState);
                        InternalValue historyId = InternalValue.create(history.getVersionHistoryId().getUUID());
                        InternalValue versionId = InternalValue.create(history.getRootVersionId().getUUID());
                        node.internalSetProperty(NameConstants.JCR_VERSIONHISTORY, historyId);
                        node.internalSetProperty(NameConstants.JCR_BASEVERSION, versionId);
                        node.internalSetProperty(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true));
                        node.internalSetProperty(NameConstants.JCR_PREDECESSORS, new InternalValue[] { versionId });
                        createdTransientState = true;
                    }
                } else if (nt.includesNodeType(NameConstants.MIX_SIMPLE_VERSIONABLE)) {
                    // we need to check the version manager for an existing
                    // version history, since simple versioning does not
                    // expose it's reference in a property
                    VersionManager vMgr = session.getVersionManager();
                    vMgr.getVersionHistory(session, nodeState);

                    // create isCheckedOutProperty if not already exists
                    NodeImpl node = (NodeImpl) itemMgr.getItem(itemState.getId());
                    if (!nodeState.hasPropertyName(NameConstants.JCR_ISCHECKEDOUT)) {
                        node.internalSetProperty(NameConstants.JCR_ISCHECKEDOUT, InternalValue.create(true));
                        createdTransientState = true;
                    }
                }
            }
        }
        return createdTransientState;
    }

    /**
     * Helper method that builds the effective (i.e. merged and resolved)
     * node type representation of the specified node's primary and mixin
     * node types.
     *
     * @param state
     * @return the effective node type
     * @throws RepositoryException
     */
    private EffectiveNodeType getEffectiveNodeType(NodeState state) throws RepositoryException {
        try {
            NodeTypeRegistry registry = session.getNodeTypeManager().getNodeTypeRegistry();
            return registry.getEffectiveNodeType(state.getNodeTypeName(), state.getMixinTypeNames());
        } catch (NodeTypeConflictException e) {
            throw new RepositoryException("Failed to build effective node type of node state " + state.getId(), e);
        }
    }

    /**
     * Failsafe mapping of internal <code>id</code> to JCR path for use in
     * diagnostic output, error messages etc.
     *
     * @return JCR path or some fallback value
     */
    public String safeGetJCRPath() {
        return itemMgr.safeGetJCRPath(id);
    }

    /**
     * Same as <code>{@link Item#remove()}</code> except for the
     * <code>noChecks</code> parameter.
     *
     * @param noChecks
     * @throws VersionException
     * @throws LockException
     * @throws RepositoryException
     */
    protected void internalRemove(boolean noChecks)
            throws VersionException, LockException, ConstraintViolationException, RepositoryException {

        // check state of this instance
        sanityCheck();

        // check if this is the root node
        if (getDepth() == 0) {
            throw new RepositoryException("Cannot remove the root node");
        }

        NodeImpl parentNode = (NodeImpl) getParent();
        if (!noChecks) {
            // check if protected and not under retention/hold
            int options = ItemValidator.CHECK_CONSTRAINTS | ItemValidator.CHECK_HOLD
                    | ItemValidator.CHECK_RETENTION;
            session.getValidator().checkRemove(this, options, Permission.NONE);

            // parent node: make sure it is checked-out and not protected nor locked.
            options = ItemValidator.CHECK_LOCK | ItemValidator.CHECK_VERSIONING | ItemValidator.CHECK_CONSTRAINTS;
            session.getValidator().checkModify(parentNode, options, Permission.NONE);
        }

        // delegate the removal of the child item to the parent node
        if (isNode()) {
            parentNode.removeChildNode((NodeId) getId());
        } else {
            Path.Element thisName = getPrimaryPath().getNameElement();
            parentNode.removeChildProperty(thisName.getName());
        }
    }

    /**
     * Same as <code>{@link Item#getName()}</code> except that
     * this method returns a <code>Name</code> instead of a
     * <code>String</code>.
     *
     * @return the name of this item as <code>Name</code>
     * @throws RepositoryException if an error occurs.
     */
    public abstract Name getQName() throws RepositoryException;

    //-----------------------------------------------------------------< Item >

    /**
     * {@inheritDoc}
     */
    public abstract void accept(ItemVisitor visitor) throws RepositoryException;

    /**
     * {@inheritDoc}
     */
    public abstract boolean isNode();

    /**
     * {@inheritDoc}
     */
    public abstract String getName() throws RepositoryException;

    /**
     * {@inheritDoc}
     */
    public abstract Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException;

    /**
     * {@inheritDoc}
     */
    public boolean isNew() {
        final ItemState state = getItemState();
        return state.isTransient() && state.getOverlayedState() == null;
    }

    /**
     * checks if this item is new. running outside of transactions, this
     * is the same as {@link #isNew()} but within a transaction an item can
     * be saved but not yet persisted.
     */
    protected boolean isTransactionalNew() {
        final ItemState state = getItemState();
        return state.getStatus() == ItemState.STATUS_NEW;
    }

    /**
     * {@inheritDoc}
     */
    public boolean isModified() {
        final ItemState state = getItemState();
        return state.isTransient() && state.getOverlayedState() != null;
    }

    /**
     * {@inheritDoc}
     */
    public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
        internalRemove(false);
    }

    /**
     * {@inheritDoc}
     */
    public void save() throws AccessDeniedException, ItemExistsException, ConstraintViolationException,
            InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException,
            NoSuchNodeTypeException, RepositoryException {
        // check state of this instance
        sanityCheck();

        // synchronize on this session
        synchronized (session) {
            /**
             * build list of transient (i.e. new & modified) states that
             * should be persisted
             */

            Collection dirty;
            try {
                dirty = getTransientStates();
            } catch (ConcurrentModificationException e) {
                String msg = "Concurrent modification; session is closed";
                log.error(msg, e);
                session.logout();
                throw e;
            }

            if (dirty.size() == 0) {
                // no transient items, nothing to do here
                return;
            }
            /**
             * build list of transient descendants in the attic
             * (i.e. those marked as 'removed')
             */
            Collection removed = getRemovedStates();

            /**
             * build set of item id's which are within the scope of
             * (i.e. affected by) this save operation
             */
            Set affectedIds = new HashSet(dirty.size() + removed.size());
            for (Iterator it = new IteratorChain(dirty.iterator(), removed.iterator()); it.hasNext();) {
                affectedIds.add(((ItemState) it.next()).getId());
            }

            /**
             * make sure that this save operation is totally 'self-contained'
             * and independent; items within the scope of this save operation
             * must not have 'external' dependencies;
             * (e.g. moving a node requires that the target node including both
             * old and new parents are saved)
             */
            for (Iterator it = new IteratorChain(dirty.iterator(), removed.iterator()); it.hasNext();) {
                ItemState transientState = (ItemState) it.next();
                if (transientState.isNode()) {
                    NodeState nodeState = (NodeState) transientState;
                    Set dependentIDs = new HashSet();
                    if (nodeState.hasOverlayedState()) {
                        NodeState overlayedState = (NodeState) nodeState.getOverlayedState();
                        NodeId oldParentId = overlayedState.getParentId();
                        NodeId newParentId = nodeState.getParentId();
                        if (oldParentId != null) {
                            if (newParentId == null) {
                                // node has been removed, add old parents
                                // to dependencies
                                if (overlayedState.isShareable()) {
                                    dependentIDs.addAll(overlayedState.getSharedSet());
                                } else {
                                    dependentIDs.add(oldParentId);
                                }
                            } else {
                                if (!oldParentId.equals(newParentId)) {
                                    // node has been moved to a new location,
                                    // add old and new parent to dependencies
                                    dependentIDs.add(oldParentId);
                                    dependentIDs.add(newParentId);
                                } else {
                                    // parent id hasn't changed, check whether
                                    // the node has been renamed (JCR-1034)
                                    if (!affectedIds.contains(newParentId)
                                            && stateMgr.hasTransientItemState(newParentId)) {
                                        try {
                                            NodeState parent = (NodeState) stateMgr
                                                    .getTransientItemState(newParentId);
                                            // check parent's renamed child node entries
                                            for (Iterator cneIt = parent.getRenamedChildNodeEntries()
                                                    .iterator(); cneIt.hasNext();) {
                                                ChildNodeEntry cne = (ChildNodeEntry) cneIt.next();
                                                if (cne.getId().equals(nodeState.getId())) {
                                                    // node has been renamed,
                                                    // add parent to dependencies
                                                    dependentIDs.add(newParentId);
                                                }
                                            }
                                        } catch (ItemStateException ise) {
                                            // should never get here
                                            log.warn("failed to retrieve transient state: " + newParentId, ise);
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // removed child node entries
                    for (Iterator cneIt = nodeState.getRemovedChildNodeEntries().iterator(); cneIt.hasNext();) {
                        ChildNodeEntry cne = (ChildNodeEntry) cneIt.next();
                        dependentIDs.add(cne.getId());
                    }
                    // added child node entries
                    for (Iterator cneIt = nodeState.getAddedChildNodeEntries().iterator(); cneIt.hasNext();) {
                        ChildNodeEntry cne = (ChildNodeEntry) cneIt.next();
                        dependentIDs.add(cne.getId());
                    }

                    // now walk through dependencies and check whether they
                    // are within the scope of this save operation
                    Iterator depIt = dependentIDs.iterator();
                    while (depIt.hasNext()) {
                        NodeId id = (NodeId) depIt.next();
                        if (!affectedIds.contains(id)) {
                            // JCR-1359 workaround: check whether unresolved
                            // dependencies originate from 'this' session;
                            // otherwise ignore them
                            if (stateMgr.hasTransientItemState(id) || stateMgr.hasTransientItemStateInAttic(id)) {
                                // need to save dependency as well
                                String msg = itemMgr.safeGetJCRPath(id) + " needs to be saved as well.";
                                log.debug(msg);
                                throw new ConstraintViolationException(msg);
                            }
                        }
                    }
                }
            }

            /**
             * validate access and node type constraints
             * (this will also validate child removals)
             */
            validateTransientItems(dirty.iterator(), removed.iterator());

            // start the update operation
            try {
                stateMgr.edit();
            } catch (IllegalStateException e) {
                String msg = "Unable to start edit operation";
                log.debug(msg);
                throw new RepositoryException(msg, e);
            }

            boolean succeeded = false;

            try {

                // process transient items marked as 'removed'
                removeTransientItems(removed.iterator());

                // process transient items that have change in mixins
                processShareableNodes(dirty.iterator());

                // initialize version histories for new nodes (might generate new transient state)
                if (initVersionHistories(dirty.iterator())) {
                    // re-build the list of transient states because the previous call
                    // generated new transient state
                    dirty = getTransientStates();
                }

                // process 'new' or 'modified' transient states
                persistTransientItems(dirty.iterator());

                // dispose the transient states marked 'new' or 'modified'
                // at this point item state data is pushed down one level,
                // node instances are disconnected from the transient
                // item state and connected to the 'overlayed' item state.
                // transient item states must be removed now. otherwise
                // the session item state provider will return an orphaned
                // item state which is not referenced by any node instance.
                for (Iterator it = dirty.iterator(); it.hasNext();) {
                    ItemState transientState = (ItemState) it.next();
                    // dispose the transient state, it is no longer used
                    stateMgr.disposeTransientItemState(transientState);
                }

                // end update operation
                stateMgr.update();
                // update operation succeeded
                succeeded = true;
            } catch (StaleItemStateException e) {
                throw new InvalidItemStateException(e.getMessage());
            } catch (ItemStateException e) {
                throw new RepositoryException("Unable to update item: " + this, e);
            } finally {
                if (!succeeded) {
                    // update operation failed, cancel all modifications
                    stateMgr.cancel();

                    // JCR-288: if an exception has been thrown during
                    // update() the transient changes have already been
                    // applied by persistTransientItems() and we need to
                    // restore transient state, i.e. undo the effect of
                    // persistTransientItems()
                    restoreTransientItems(dirty.iterator());
                }
            }

            // now it is safe to dispose the transient states:
            // dispose the transient states marked 'removed'.
            // item states in attic are removed after store, because
            // the observation mechanism needs to build paths of removed
            // items in store().
            for (Iterator it = removed.iterator(); it.hasNext();) {
                ItemState transientState = (ItemState) it.next();
                // dispose the transient state, it is no longer used
                stateMgr.disposeTransientItemStateInAttic(transientState);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public synchronized void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException {
        // check state of this instance
        sanityCheck();

        if (keepChanges) {
            /** todo FIXME should reset Item#status field to STATUS_NORMAL
             * of all descendent non-transient instances; maybe also
             * have to reset stale ItemState instances */
            return;
        }

        if (isNode()) {
            // check if this is the root node
            if (getDepth() == 0) {
                // optimization
                stateMgr.disposeAllTransientItemStates();
                return;
            }
        }

        // list of transient items that should be discarded
        ArrayList list = new ArrayList();
        ItemState transientState;

        // check status of this item's state
        if (isTransient()) {
            transientState = getItemState();
            switch (transientState.getStatus()) {
            case ItemState.STATUS_STALE_MODIFIED:
            case ItemState.STATUS_STALE_DESTROYED:
                // add this item's state to the list
                list.add(transientState);
                break;

            case ItemState.STATUS_EXISTING_MODIFIED:
                if (!transientState.getParentId().equals(transientState.getOverlayedState().getParentId())) {
                    throw new RepositoryException(
                            "Cannot refresh a moved item: " + this + " - possible solution: refresh the parent");
                }
                list.add(transientState);
                break;

            case ItemState.STATUS_NEW:
                throw new RepositoryException("Cannot refresh a new item: " + this);

            default:
                log.warn("Unexpected item state status:" + transientState.getStatus() + " of " + this);
                // ignore
                break;
            }
        }

        if (isNode()) {
            // build list of 'new', 'modified' or 'stale' descendants
            Iterator iter = stateMgr.getDescendantTransientItemStates((NodeId) id);
            while (iter.hasNext()) {
                transientState = (ItemState) iter.next();
                switch (transientState.getStatus()) {
                case ItemState.STATUS_STALE_MODIFIED:
                case ItemState.STATUS_STALE_DESTROYED:
                case ItemState.STATUS_NEW:
                case ItemState.STATUS_EXISTING_MODIFIED:
                    // add new or modified state to the list
                    list.add(transientState);
                    break;

                default:
                    log.debug("unexpected state status (" + transientState.getStatus() + ")");
                    // ignore
                    break;
                }
            }
        }

        // process list of 'new', 'modified' or 'stale' transient states
        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            transientState = (ItemState) iter.next();
            // dispose the transient state, it is no longer used;
            // this will indirectly (through stateDiscarded listener method)
            // either restore or permanently invalidate the wrapping Item instances
            stateMgr.disposeTransientItemState(transientState);
        }

        if (isNode()) {
            // discard all transient descendants in the attic (i.e. those marked
            // as 'removed'); this will resurrect the removed items
            iter = stateMgr.getDescendantTransientItemStatesInAttic((NodeId) id);
            while (iter.hasNext()) {
                transientState = (ItemState) iter.next();
                // dispose the transient state; this will indirectly (through
                // stateDiscarded listener method) resurrect the wrapping Item instances
                stateMgr.disposeTransientItemStateInAttic(transientState);
            }
        }
    }

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

        if (degree == 0) {
            return itemMgr.getRootNode();
        }

        try {
            // Path.getAncestor requires relative degree, i.e. we need
            // to convert absolute to relative ancestor degree
            Path path = getPrimaryPath();
            int relDegree = path.getAncestorCount() - degree;
            if (relDegree < 0) {
                throw new ItemNotFoundException();
            }
            // shortcut
            if (relDegree == 0) {
                return this;
            }
            Path ancestorPath = path.getAncestor(relDegree);
            return itemMgr.getNode(ancestorPath);
        } catch (PathNotFoundException pnfe) {
            throw new ItemNotFoundException();
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getPath() throws RepositoryException {
        // check state of this instance
        sanityCheck();
        return session.getJCRPath(getPrimaryPath());
    }

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

        final ItemState state = getItemState();
        if (state.getParentId() == null) {
            // shortcut
            return 0;
        }
        return session.getHierarchyManager().getDepth(id);
    }

    /**
     * Returns the session associated with this item.
     * <p>
     * Since Jackrabbit 1.4 it is safe to use this method regardless
     * of item state.
     *
     * @see <a href="http://issues.apache.org/jira/browse/JCR-911">Issue JCR-911</a>
     * @return current session
     */
    public Session getSession() {
        return session;
    }

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

        if (this == otherItem) {
            return true;
        }
        if (otherItem instanceof ItemImpl) {
            ItemImpl other = (ItemImpl) otherItem;
            return id.equals(other.id)
                    && session.getWorkspace().getName().equals(other.getSession().getWorkspace().getName());
        }
        return false;
    }

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

    /**
     * Returns the({@link #safeGetJCRPath() safe}) path of this item for use
     * in diagnostic output.
     *
     * @return "/path/to/item"
     */
    public String toString() {
        return safeGetJCRPath();
    }

}