org.apache.jackrabbit.core.state.SessionItemStateManager.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.jackrabbit.core.state.SessionItemStateManager.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.state;

import org.apache.commons.collections.iterators.IteratorChain;
import org.apache.jackrabbit.core.CachingHierarchyManager;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.ItemId;
import org.apache.jackrabbit.core.NodeId;
import org.apache.jackrabbit.core.PropertyId;
import org.apache.jackrabbit.core.ZombieHierarchyManager;
import org.apache.jackrabbit.core.nodetype.NodeDef;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.util.Dumpable;
import org.apache.jackrabbit.spi.Name;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

/**
 * Item state manager that handles both transient and persistent items.
 */
public class SessionItemStateManager implements UpdatableItemStateManager, Dumpable, NodeStateListener {

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

    /**
     * State manager that allows updates
     */
    private final UpdatableItemStateManager stateMgr;

    /**
     * Hierarchy manager
     */
    private CachingHierarchyManager hierMgr;

    /**
     * map of those states that have been removed transiently
     */
    private final ItemStateStore atticStore;

    /**
     * map of new or modified transient states
     */
    private final ItemStateStore transientStore;

    /**
     * ItemStateManager view of the states in the attic; lazily instantiated
     * in {@link #getAttic()}
     */
    private AtticItemStateManager attic;

    /**
     * Node Type Registry
     */
    private final NodeTypeRegistry ntReg;

    /**
     * State change dispatcher.
     */
    private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher();

    /**
     * Creates a new <code>SessionItemStateManager</code> instance.
     *
     * @param rootNodeId the root node id
     * @param stateMgr the local item state manager
     * @param ntReg node type registry
     */
    public SessionItemStateManager(NodeId rootNodeId, LocalItemStateManager stateMgr, NodeTypeRegistry ntReg) {
        transientStore = new ItemStateMap();
        atticStore = new ItemStateMap();

        this.stateMgr = stateMgr;
        stateMgr.addListener(this);

        // create hierarchy manager that uses both transient and persistent state
        hierMgr = new CachingHierarchyManager(rootNodeId, this);
        addListener(hierMgr);

        this.ntReg = ntReg;
    }

    /**
     * Returns the hierarchy manager
     *
     * @return the hierarchy manager
     */
    public HierarchyManager getHierarchyMgr() {
        return hierMgr;
    }

    /**
     * Returns an attic-aware hierarchy manager, i.e. an hierarchy manager that
     * is also able to build/resolve paths of those items that have been moved
     * or removed (i.e. moved to the attic).
     *
     * @return an attic-aware hierarchy manager
     */
    public HierarchyManager getAtticAwareHierarchyMgr() {
        return new ZombieHierarchyManager(hierMgr, this, getAttic());
    }

    //-------------------------------------------------------------< Dumpable >

    /**
     * {@inheritDoc}
     */
    public void dump(PrintStream ps) {
        ps.println("SessionItemStateManager (" + this + ")");
        ps.println();
        ps.print("[transient] ");
        if (transientStore instanceof Dumpable) {
            ((Dumpable) transientStore).dump(ps);
        } else {
            ps.println(transientStore.toString());
        }
        ps.println();
        ps.print("[attic]     ");
        if (atticStore instanceof Dumpable) {
            ((Dumpable) atticStore).dump(ps);
        } else {
            ps.println(atticStore.toString());
        }
        ps.println();
    }

    //-----------------------------------------------------< ItemStateManager >

    /**
     * {@inheritDoc}
     */
    public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException {

        // first check if the specified item has been transiently removed
        if (atticStore.contains(id)) {
            /**
             * check if there's new transient state for the specified item
             * (e.g. if a property with name 'x' has been removed and a new
             * property with same name has been created);
             * this will throw a NoSuchItemStateException if there's no new
             * transient state
             */
            return getTransientItemState(id);
        }

        // check if there's transient state for the specified item
        if (transientStore.contains(id)) {
            return getTransientItemState(id);
        }

        // check if there's persistent state for the specified item
        if (stateMgr.hasItemState(id)) {
            return stateMgr.getItemState(id);
        }

        throw new NoSuchItemStateException(id.toString());
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasItemState(ItemId id) {
        // first check if the specified item has been transiently removed
        if (atticStore.contains(id)) {
            /**
             * check if there's new transient state for the specified item
             * (e.g. if a property with name 'x' has been removed and a new
             * property with same name has been created);
             */
            return transientStore.contains(id);
        }
        // check if there's transient state for the specified item
        if (transientStore.contains(id)) {
            return true;
        }
        // check if there's persistent state for the specified item
        return stateMgr.hasItemState(id);
    }

    /**
     * {@inheritDoc}
     */
    public NodeReferences getNodeReferences(NodeReferencesId id)
            throws NoSuchItemStateException, ItemStateException {

        return stateMgr.getNodeReferences(id);
    }

    /**
     * {@inheritDoc}
     */
    public boolean hasNodeReferences(NodeReferencesId id) {
        return stateMgr.hasNodeReferences(id);
    }

    //--------------------------------------------< UpdatableItemStateManager >

    /**
     * {@inheritDoc}
     */
    public void edit() throws IllegalStateException {
        stateMgr.edit();
    }

    /**
     * {@inheritDoc}
     */
    public boolean inEditMode() {
        return stateMgr.inEditMode();
    }

    /**
     * {@inheritDoc}
     */
    public NodeState createNew(NodeId id, Name nodeTypeName, NodeId parentId) throws IllegalStateException {
        return stateMgr.createNew(id, nodeTypeName, parentId);
    }

    /**
     * Customized variant of {@link #createNew(NodeId, Name, NodeId)} that
     * connects the newly created persistent state with the transient state.
     */
    public NodeState createNew(NodeState transientState) throws IllegalStateException {

        NodeState persistentState = createNew(transientState.getNodeId(), transientState.getNodeTypeName(),
                transientState.getParentId());
        transientState.connect(persistentState);
        return persistentState;
    }

    /**
     * {@inheritDoc}
     */
    public PropertyState createNew(Name propName, NodeId parentId) throws IllegalStateException {
        return stateMgr.createNew(propName, parentId);
    }

    /**
     * Customized variant of {@link #createNew(Name, NodeId)} that
     * connects the newly created persistent state with the transient state.
     */
    public PropertyState createNew(PropertyState transientState) throws IllegalStateException {

        PropertyState persistentState = createNew(transientState.getName(), transientState.getParentId());
        transientState.connect(persistentState);
        return persistentState;
    }

    /**
     * {@inheritDoc}
     */
    public void store(ItemState state) throws IllegalStateException {
        stateMgr.store(state);
    }

    /**
     * {@inheritDoc}
     */
    public void destroy(ItemState state) throws IllegalStateException {
        stateMgr.destroy(state);
    }

    /**
     * {@inheritDoc}
     */
    public void cancel() throws IllegalStateException {
        stateMgr.cancel();
    }

    /**
     * {@inheritDoc}
     */
    public void update() throws ReferentialIntegrityException, StaleItemStateException, ItemStateException,
            IllegalStateException {
        stateMgr.update();
    }

    /**
     * {@inheritDoc}
     */
    public void dispose() {
        // discard all transient changes
        disposeAllTransientItemStates();
        // dispose our (i.e. 'local') state manager
        stateMgr.dispose();
    }

    //< more methods for listing and retrieving transient ItemState instances >

    /**
     * @param id
     * @return
     * @throws NoSuchItemStateException
     * @throws ItemStateException
     */
    public ItemState getTransientItemState(ItemId id) throws NoSuchItemStateException, ItemStateException {

        ItemState state = transientStore.get(id);
        if (state != null) {
            return state;
        } else {
            throw new NoSuchItemStateException(id.toString());
        }
    }

    /**
     *
     * @param id
     * @return
     */
    public boolean hasTransientItemState(ItemId id) {
        return transientStore.contains(id);
    }

    /**
     *
     * @param id
     * @return
     */
    public boolean hasTransientItemStateInAttic(ItemId id) {
        return atticStore.contains(id);
    }

    /**
     * @return <code>true</code> if this manager has any transient state;
     *         <code>false</code> otherwise.
     */
    public boolean hasAnyTransientItemStates() {
        return !transientStore.isEmpty();
    }

    /**
     * Returns an iterator over those transient item state instances that are
     * direct or indirect descendants of the item state with the given
     * <code>parentId</code>. The transient item state instance with the given
     * <code>parentId</code> itself (if there is such)                                                                            not be included.
     * <p/>
     * The instances are returned in depth-first tree traversal order.
     *
     * @param parentId the id of the common parent of the transient item state
     *                 instances to be returned.
     * @return an iterator over descendant transient item state instances
     * @throws InvalidItemStateException if any descendant item state has been
     *                                   deleted externally
     * @throws RepositoryException       if another error occurs
     */
    public Iterator<ItemState> getDescendantTransientItemStates(NodeId parentId)
            throws InvalidItemStateException, RepositoryException {
        if (transientStore.isEmpty()) {
            List<ItemState> empty = Collections.emptyList();
            return empty.iterator();
        }

        // build ordered collection of descendant transient states
        // sorted by decreasing relative depth

        // use an array of lists to group the descendants by relative depth;
        // the depth is used as array index
        List[] la = new List[10];
        try {
            Iterator iter = transientStore.values().iterator();
            while (iter.hasNext()) {
                ItemState state = (ItemState) iter.next();
                // determine relative depth: > 0 means it's a descendant
                int depth;
                try {
                    depth = hierMgr.getShareRelativeDepth(parentId, state.getId());
                } catch (ItemNotFoundException infe) {
                    /**
                     * one of the parents of the specified item has been
                     * removed externally; as we don't know its path,
                     * we can't determine if it is a descendant;
                     * InvalidItemStateException should only be thrown if
                     * a descendant is affected;
                     * => throw InvalidItemStateException for now
                     * todo FIXME
                     */
                    // unable to determine relative depth, assume that the item
                    // (or any of its ancestors) has been removed externally
                    String msg = state.getId() + ": the item seems to have been removed externally.";
                    log.debug(msg);
                    throw new InvalidItemStateException(msg);
                }

                if (depth < 1) {
                    // not a descendant
                    continue;
                }

                // ensure capacity
                if (depth > la.length) {
                    List[] old = la;
                    la = new List[depth + 10];
                    System.arraycopy(old, 0, la, 0, old.length);
                }

                List list = la[depth - 1];
                if (list == null) {
                    list = new ArrayList();
                    la[depth - 1] = list;
                }
                list.add(state);
            }
        } catch (RepositoryException re) {
            log.warn("inconsistent hierarchy state", re);
        }
        // create an iterator over the collected descendants
        // in decreasing depth order
        IteratorChain resultIter = new IteratorChain();
        for (int i = la.length - 1; i >= 0; i--) {
            List list = la[i];
            if (list != null) {
                resultIter.addIterator(list.iterator());
            }
        }
        /**
         * if the resulting iterator chain is empty return
         * EMPTY_LIST.iterator() instead because older versions
         * of IteratorChain (pre Commons Collections 3.1)
         * would throw UnsupportedOperationException in this
         * situation
         */
        if (resultIter.getIterators().isEmpty()) {
            List<ItemState> empty = Collections.emptyList();
            return empty.iterator();
        }
        return resultIter;
    }

    /**
     * Same as <code>{@link #getDescendantTransientItemStates(NodeId)}</code>
     * except that item state instances in the attic are returned.
     *
     * @param parentId the id of the common parent of the transient item state
     *                 instances to be returned.
     * @return an iterator over descendant transient item state instances in the attic
     */
    public Iterator<ItemState> getDescendantTransientItemStatesInAttic(NodeId parentId) {
        if (atticStore.isEmpty()) {
            List<ItemState> empty = Collections.emptyList();
            return empty.iterator();
        }

        // build ordered collection of descendant transient states in attic
        // sorted by decreasing relative depth

        // use a special attic-aware hierarchy manager
        ZombieHierarchyManager zombieHierMgr = new ZombieHierarchyManager(hierMgr, this, getAttic());

        // use an array of lists to group the descendants by relative depth;
        // the depth is used as array index
        List[] la = new List[10];
        try {
            for (ItemState state : atticStore.values()) {
                // determine relative depth: > 0 means it's a descendant
                //int depth = zombieHierMgr.getRelativeDepth(parentId, state.getId());
                int depth = zombieHierMgr.getShareRelativeDepth(parentId, state.getId());
                if (depth < 1) {
                    // not a descendant
                    continue;
                }

                // ensure capacity
                if (depth > la.length) {
                    List[] old = la;
                    la = new List[depth + 10];
                    System.arraycopy(old, 0, la, 0, old.length);
                }

                List list = la[depth - 1];
                if (list == null) {
                    list = new ArrayList();
                    la[depth - 1] = list;
                }
                list.add(state);
            }
        } catch (RepositoryException re) {
            log.warn("inconsistent hierarchy state", re);
        }
        // create an iterator over the collected descendants
        // in decreasing depth order
        IteratorChain resultIter = new IteratorChain();
        for (int i = la.length - 1; i >= 0; i--) {
            List list = la[i];
            if (list != null) {
                resultIter.addIterator(list.iterator());
            }
        }
        /**
         * if the resulting iterator chain is empty return
         * EMPTY_LIST.iterator() instead because older versions
         * of IteratorChain (pre Commons Collections 3.1)
         * would throw UnsupportedOperationException in this
         * situation
         */
        if (resultIter.getIterators().isEmpty()) {
            List<ItemState> empty = Collections.emptyList();
            return empty.iterator();
        }
        return resultIter;
    }

    /**
     * Return a flag indicating whether the specified item is in the transient
     * item state manager's attic space.
     *
     * @param id item id
     * @return <code>true</code> if the item state is in the attic space;
     *         <code>false</code> otherwise
     */
    public boolean isItemStateInAttic(ItemId id) {
        return atticStore.contains(id);
    }

    //------< methods for creating & discarding transient ItemState instances >

    /**
     * @param id
     * @param nodeTypeName
     * @param parentId
     * @param initialStatus
     * @return
     * @throws ItemStateException
     */
    public NodeState createTransientNodeState(NodeId id, Name nodeTypeName, NodeId parentId, int initialStatus)
            throws ItemStateException {

        // check map; synchronized to ensure an entry is not created twice.
        synchronized (transientStore) {
            if (transientStore.contains(id)) {
                String msg = "there's already a node state instance with id " + id;
                log.debug(msg);
                throw new ItemStateException(msg);
            }

            NodeState state = new NodeState(id, nodeTypeName, parentId, initialStatus, true);
            // put transient state in the map
            transientStore.put(state);
            state.setContainer(this);
            return state;
        }
    }

    /**
     * @param overlayedState
     * @param initialStatus
     * @return
     * @throws ItemStateException
     */
    public NodeState createTransientNodeState(NodeState overlayedState, int initialStatus)
            throws ItemStateException {

        ItemId id = overlayedState.getNodeId();

        // check map; synchronized to ensure an entry is not created twice.
        synchronized (transientStore) {
            if (transientStore.contains(id)) {
                String msg = "there's already a node state instance with id " + id;
                log.debug(msg);
                throw new ItemStateException(msg);
            }

            NodeState state = new NodeState(overlayedState, initialStatus, true);
            // put transient state in the map
            transientStore.put(state);
            state.setContainer(this);
            return state;
        }
    }

    /**
     * @param parentId
     * @param propName
     * @param initialStatus
     * @return
     * @throws ItemStateException
     */
    public PropertyState createTransientPropertyState(NodeId parentId, Name propName, int initialStatus)
            throws ItemStateException {

        PropertyId id = new PropertyId(parentId, propName);

        // check map; synchronized to ensure an entry is not created twice.
        synchronized (transientStore) {
            if (transientStore.contains(id)) {
                String msg = "there's already a property state instance with id " + id;
                log.debug(msg);
                throw new ItemStateException(msg);
            }

            PropertyState state = new PropertyState(id, initialStatus, true);
            // put transient state in the map
            transientStore.put(state);
            state.setContainer(this);
            return state;
        }
    }

    /**
     * @param overlayedState
     * @param initialStatus
     * @return
     * @throws ItemStateException
     */
    public PropertyState createTransientPropertyState(PropertyState overlayedState, int initialStatus)
            throws ItemStateException {

        PropertyId id = overlayedState.getPropertyId();

        // check map; synchronized to ensure an entry is not created twice.
        synchronized (transientStore) {
            if (transientStore.contains(id)) {
                String msg = "there's already a property state instance with id " + id;
                log.debug(msg);
                throw new ItemStateException(msg);
            }

            PropertyState state = new PropertyState(overlayedState, initialStatus, true);
            // put transient state in the map
            transientStore.put(state);
            state.setContainer(this);
            return state;
        }
    }

    /**
     * Disconnect a transient item state from its underlying persistent state.
     * Notifies the <code>HierarchyManager</code> about the changed identity.
     *
     * @param state the transient <code>ItemState</code> instance that should
     *              be disconnected
     */
    public void disconnectTransientItemState(ItemState state) {
        state.disconnect();
    }

    /**
     * Disposes the specified transient item state instance, i.e. discards it
     * and clears it from cache.
     *
     * @param state the transient <code>ItemState</code> instance that should
     *              be disposed
     * @see ItemState#discard()
     */
    public void disposeTransientItemState(ItemState state) {
        // discard item state, this will invalidate the wrapping Item
        // instance of the transient state
        state.discard();
        // remove from map
        transientStore.remove(state.getId());
        // give the instance a chance to prepare to get gc'ed
        state.onDisposed();
    }

    /**
     * Transfers the specified transient item state instance from the 'active'
     * cache to the attic.
     *
     * @param state the transient <code>ItemState</code> instance that should
     *              be moved to the attic
     */
    public void moveTransientItemStateToAttic(ItemState state) {
        // remove from map
        transientStore.remove(state.getId());
        // add to attic
        atticStore.put(state);
    }

    /**
     * Disposes the specified transient item state instance in the attic, i.e.
     * discards it and removes it from the attic.
     *
     * @param state the transient <code>ItemState</code> instance that should
     *              be disposed @see ItemState#discard()
     */
    public void disposeTransientItemStateInAttic(ItemState state) {
        // discard item state, this will invalidate the wrapping Item
        // instance of the transient state
        state.discard();
        // remove from attic
        atticStore.remove(state.getId());
        // give the instance a chance to prepare to get gc'ed
        state.onDisposed();
    }

    /**
     * Disposes all transient item states in the cache and in the attic.
     */
    public void disposeAllTransientItemStates() {
        // dispose item states in transient map & attic
        // (use temp collection to avoid ConcurrentModificationException)
        Collection<ItemState> tmp = new ArrayList<ItemState>(transientStore.values());
        for (ItemState state : tmp) {
            disposeTransientItemState(state);
        }
        tmp = new ArrayList<ItemState>(atticStore.values());
        for (ItemState state : tmp) {
            disposeTransientItemStateInAttic(state);
        }
    }

    /**
     * Add an <code>ItemStateListener</code>
     *
     * @param listener the new listener to be informed on modifications
     */
    public void addListener(ItemStateListener listener) {
        dispatcher.addListener(listener);
    }

    /**
     * Remove an <code>ItemStateListener</code>
     *
     * @param listener an existing listener
     */
    public void removeListener(ItemStateListener listener) {
        dispatcher.removeListener(listener);
    }

    /**
     * Return the attic item state provider that holds all items
     * moved into the attic.
     *
     * @return attic
     */
    ItemStateManager getAttic() {
        if (attic == null) {
            attic = new AtticItemStateManager();
        }
        return attic;
    }

    //----------------------------------------------------< ItemStateListener >

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both transient states that this state manager
     * has created, as well as states that were created by the local state manager
     * we're listening to.
     */
    public void stateCreated(ItemState created) {
        ItemState visibleState = created;
        if (created.getContainer() != this) {
            // local state was created
            ItemState transientState = transientStore.get(created.getId());
            if (transientState != null) {
                if (transientState.hasOverlayedState()) {
                    // underlying state has been permanently created
                    transientState.pull();
                    transientState.setStatus(ItemState.STATUS_EXISTING);
                } else {
                    // this is a notification from another session
                    try {
                        ItemState local = stateMgr.getItemState(created.getId());
                        transientState.connect(local);
                        // update mod count
                        transientState.setModCount(local.getModCount());
                        transientState.setStatus(ItemState.STATUS_EXISTING_MODIFIED);
                    } catch (ItemStateException e) {
                        // something went wrong, mark as stale
                        transientState.setStatus(ItemState.STATUS_STALE_MODIFIED);
                    }
                }
                visibleState = transientState;
            }
        }
        dispatcher.notifyStateCreated(visibleState);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both transient states that this state manager
     * has created, as well as states that were created by the local state manager
     * we're listening to.
     */
    public void stateModified(ItemState modified) {
        ItemState visibleState = modified;
        if (modified.getContainer() != this) {
            // local state was modified
            ItemState transientState = transientStore.get(modified.getId());
            if (transientState != null) {
                if (transientState.isNode() && !transientState.isStale()) {
                    // try to silently merge non-conflicting changes (JCR-584)
                    NodeStateMerger.MergeContext context = new NodeStateMerger.MergeContext() {
                        public boolean isAdded(ItemId id) {
                            ItemState is = transientStore.get(id);
                            return is != null && is.getStatus() == ItemState.STATUS_NEW;
                        }

                        public boolean isDeleted(ItemId id) {
                            return atticStore.contains(id);
                        }

                        public boolean isModified(ItemId id) {
                            ItemState is = transientStore.get(id);
                            return is != null && is.getStatus() == ItemState.STATUS_EXISTING_MODIFIED;
                        }

                        public boolean allowsSameNameSiblings(NodeId id) {
                            NodeState ns;
                            try {
                                ns = (NodeState) getItemState(id);
                            } catch (ItemStateException e) {
                                return false;
                            }
                            NodeDef def = ntReg.getNodeDef(ns.getDefinitionId());
                            return def != null ? def.allowsSameNameSiblings() : false;
                        }
                    };
                    if (NodeStateMerger.merge((NodeState) transientState, context)) {
                        // merge succeeded
                        return;
                    }
                }
                transientState.setStatus(ItemState.STATUS_STALE_MODIFIED);
                visibleState = transientState;
            }
            // check attic as well (JCR-1432)
            transientState = atticStore.get(modified.getId());
            if (transientState != null) {
                transientState.setStatus(ItemState.STATUS_STALE_MODIFIED);
                visibleState = transientState;
            }
        }
        dispatcher.notifyStateModified(visibleState);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both transient states that this state manager
     * has created, as well as states that were created by the local state manager
     * we're listening to.
     */
    public void stateDestroyed(ItemState destroyed) {
        ItemState visibleState = destroyed;
        if (destroyed.getContainer() != this) {
            // local state was destroyed
            ItemState transientState = transientStore.get(destroyed.getId());
            if (transientState != null) {
                transientState.setStatus(ItemState.STATUS_STALE_DESTROYED);
                visibleState = transientState;
            } else {
                // check attic
                transientState = atticStore.get(destroyed.getId());
                if (transientState != null) {
                    atticStore.remove(destroyed.getId());
                    transientState.onDisposed();
                }
            }
        }
        dispatcher.notifyStateDestroyed(visibleState);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Notification handler gets called for both transient states that this state manager
     * has created, as well as states that were created by the local state manager
     * we're listening to.
     */
    public void stateDiscarded(ItemState discarded) {
        ItemState visibleState = discarded;
        if (discarded.getContainer() != this) {
            // local state was discarded
            ItemState transientState = transientStore.get(discarded.getId());
            if (transientState != null) {
                transientState.setStatus(ItemState.STATUS_UNDEFINED);
                visibleState = transientState;
            }
        }
        dispatcher.notifyStateDiscarded(visibleState);
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Pass notification to listeners if a transient state was modified
     * or if the local state is not overlayed.
     */
    public void nodeAdded(NodeState state, Name name, int index, NodeId id) {
        if (state.getContainer() == this || !transientStore.contains(state.getId())) {
            dispatcher.notifyNodeAdded(state, name, index, id);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Pass notification to listeners if a transient state was modified
     * or if the local state is not overlayed.
     */
    public void nodesReplaced(NodeState state) {
        if (state.getContainer() == this || !transientStore.contains(state.getId())) {
            dispatcher.notifyNodesReplaced(state);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Pass notification to listeners if a transient state was modified
     * or if the local state is not overlayed.
     */
    public void nodeModified(NodeState state) {
        if (state.getContainer() == this || !transientStore.contains(state.getId())) {
            dispatcher.notifyNodeModified(state);
        }
    }

    /**
     * {@inheritDoc}
     * <p/>
     * Pass notification to listeners if a transient state was modified
     * or if the local state is not overlayed.
     */
    public void nodeRemoved(NodeState state, Name name, int index, NodeId id) {
        if (state.getContainer() == this || !transientStore.contains(state.getId())) {
            dispatcher.notifyNodeRemoved(state, name, index, id);
        }
    }

    //--------------------------------------------------------< inner classes >

    /**
     * ItemStateManager view of the states in the attic
     *
     * @see SessionItemStateManager#getAttic
     */
    private class AtticItemStateManager implements ItemStateManager {

        /**
         * {@inheritDoc}
         */
        public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException {

            ItemState state = atticStore.get(id);
            if (state != null) {
                return state;
            } else {
                throw new NoSuchItemStateException(id.toString());
            }
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasItemState(ItemId id) {
            return atticStore.contains(id);
        }

        /**
         * {@inheritDoc}
         */
        public NodeReferences getNodeReferences(NodeReferencesId id)
                throws NoSuchItemStateException, ItemStateException {
            // n/a
            throw new ItemStateException("getNodeReferences() not implemented");
        }

        /**
         * {@inheritDoc}
         */
        public boolean hasNodeReferences(NodeReferencesId id) {
            // n/a
            return false;
        }
    }
}