/*
 * 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.version;

import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ACTIVITY;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_ROOTVERSION;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.JCR_VERSIONHISTORY;
import static org.apache.jackrabbit.spi.commons.name.NameConstants.MIX_VERSIONABLE;

import java.util.Calendar;

import javax.jcr.ItemNotFoundException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.version.VersionException;

import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.NodeIdFactory;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.LocalItemStateManager;
import org.apache.jackrabbit.core.state.NodeReferences;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Base implementation of the {@link InternalVersionManager} interface.
 * <p>
 * All read operations must acquire the read lock before reading, all write
 * operations must acquire the write lock.
 */
abstract class InternalVersionManagerBase implements InternalVersionManager {

    /**
     * Logger instance.
     */
    private static Logger log = LoggerFactory.getLogger(InternalVersionManagerBase.class);

    /**
     * State manager for the version storage.
     */
    protected LocalItemStateManager stateMgr;

    /**
     * Node type registry.
     */
    protected final NodeTypeRegistry ntReg;

    protected final NodeId historiesId;

    protected final NodeId activitiesId;

    /**
     * the lock on this version manager
     */
    private final VersioningLock rwLock = new VersioningLock();

    private final NodeIdFactory nodeIdFactory;

    protected InternalVersionManagerBase(NodeTypeRegistry ntReg,
                                         NodeId historiesId,
                                         NodeId activitiesId,
                                         NodeIdFactory nodeIdFactory) {
        this.ntReg = ntReg;
        this.historiesId = historiesId;
        this.activitiesId = activitiesId;
        this.nodeIdFactory = nodeIdFactory;
    }

//-------------------------------------------------------< InternalVersionManager >

    /**
     * {@inheritDoc}
     */
    public InternalVersion getVersion(NodeId id) throws RepositoryException {
        // lock handling via getItem()
        InternalVersion v = (InternalVersion) getItem(id);
        if (v == null) {
            log.warn("Versioning item not found: " + id);
        }
        return v;
    }

    /**
     * {@inheritDoc}
     */
    public InternalBaseline getBaseline(NodeId id) throws RepositoryException {
        // lock handling via getItem()
        InternalVersionItem item = getItem(id);
        if (item == null) {
            log.warn("Versioning item not found: " + id);
        } else if (!(item instanceof InternalBaseline)) {
            log.warn("Versioning item is not a baseline: " + id);
            item = null;
        }
        return (InternalBaseline) item;
    }

    /**
     * {@inheritDoc}
     */
    public InternalActivity getActivity(NodeId id) throws RepositoryException {
        // lock handling via getItem()
        InternalActivity v = (InternalActivity) getItem(id);
        if (v == null) {
            log.warn("Versioning item not found: " + id);
        }
        return v;
    }

    /**
     * {@inheritDoc}
     */
    public InternalVersionHistory getVersionHistory(NodeId id)
            throws RepositoryException {
        // lock handling via getItem()
        return (InternalVersionHistory) getItem(id);
    }

    /**
     * {@inheritDoc}
     */
    public InternalVersionHistory getVersionHistoryOfNode(NodeId id)
            throws RepositoryException {
        VersioningLock.ReadLock lock = acquireReadLock();
        try {
            String uuid = id.toString();
            Name name = getName(uuid);

            NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, null);
            if (parent != null && parent.hasNode(name)) {
                NodeStateEx history = parent.getNode(name, 1);
                if (history == null) {
                    throw new InconsistentVersioningState("Unexpected failure to get child node " + name + " on parent node" + parent.getNodeId());
                }
                return getVersionHistory(history.getNodeId());
            } else {
                throw new ItemNotFoundException("Version history of node " + id + " not found.");
            }
        } finally {
            lock.release();
        }
    }

    /**
     * {@inheritDoc}
     *
     * Assumes that all versions are stored chronologically below the version
     * history and just returns the last one. i.e. currently only works for
     * simple versioning.
     */
    public InternalVersion getHeadVersionOfNode(NodeId id) throws RepositoryException {
        InternalVersionHistory vh = getVersionHistoryOfNode(id);
        Name[] names = vh.getVersionNames();
        InternalVersion last = vh.getVersion(names[names.length - 1]);
        return getVersion(last.getId());
    }

    //-------------------------------------------------------< implementation >

    /**
     * Acquires the write lock on this version manager.
     * @return returns the write lock
     */
    protected VersioningLock.WriteLock acquireWriteLock() {
        while (true) {
            try {
                return rwLock.acquireWriteLock();
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * acquires the read lock on this version manager.
     * @return returns the read lock
     */
    public VersioningLock.ReadLock acquireReadLock() {
        while (true) {
            try {
                return rwLock.acquireReadLock();
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * returns the id of the version history root node
     *
     * @return the id of the version history root node
     */
    abstract protected NodeStateEx getHistoryRoot();

    /**
     * returns the id of the activities root node
     *
     * @return the id of the activities root node
     */
    abstract protected NodeStateEx getActivitiesRoot();


    /**
     * Helper for managing write operations.
     */
    private class WriteOperation {

        /**
         * Flag for successful completion of the write operation.
         */
        private boolean success = false;

        private final VersioningLock.WriteLock lock;

        public WriteOperation(VersioningLock.WriteLock lock) {
            this.lock = lock;
        }

        /**
         * Saves the pending operations in the {@link LocalItemStateManager}.
         *
         * @throws ItemStateException if the pending state is invalid
         * @throws RepositoryException if the pending state could not be persisted
         */
        public void save() throws ItemStateException, RepositoryException {
            stateMgr.update();
            success = true;
        }

        /**
         * Closes the write operation. The pending operations are canceled
         * if they could not be properly saved. Finally the write lock is
         * released.
         */
        public void close() {
            try {
                if (!success) {
                    // update operation failed, cancel all modifications
                    stateMgr.cancel();
                }
            } finally {
                lock.release();
            }
        }
    }

    /**
     * Starts a write operation by acquiring the write lock and setting the
     * item state manager to the "edit" state. If something goes wrong, the
     * write lock is released and an exception is thrown.
     * <p>
     * The pattern for using this method and the returned helper instance is:
     * <pre>
     *     WriteOperation operation = startWriteOperation();
     *     try {
     *         ...
     *         operation.save(); // if everything is OK
     *         ...
     *     } catch (...) {
     *         ...
     *     } finally {
     *         operation.close();
     *     }
     * </pre>
     *
     * @return write operation helper
     * @throws RepositoryException if the write operation could not be started
     */
    private WriteOperation startWriteOperation() throws RepositoryException {
        boolean success = false;
        VersioningLock.WriteLock lock = acquireWriteLock();
        try {
            stateMgr.edit();
            success = true;
            return new WriteOperation(lock);
        } catch (IllegalStateException e) {
            throw new RepositoryException("Unable to start edit operation.", e);
        } finally {
            if (!success) {
                lock.release();
            }
        }
    }

    /**
     * Returns information about the version history of the specified node
     * or <code>null</code> when unavailable.
     *
     * @param node node whose version history should be returned
     * @return identifiers of the version history and root version nodes
     * @throws RepositoryException if an error occurs
     */
    public VersionHistoryInfo getVersionHistoryInfoForNode(NodeState node) throws RepositoryException {
        VersionHistoryInfo info = null;

        VersioningLock.ReadLock lock = acquireReadLock();
        try {
            String uuid = node.getNodeId().toString();
            Name name = getName(uuid);

            NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, null);
            if (parent != null && parent.hasNode(name)) {
                NodeStateEx history = parent.getNode(name, 1);
                if (history == null) {
                    throw new InconsistentVersioningState("Unexpected failure to get child node " + name + " on parent node " + parent.getNodeId());
                }
                ChildNodeEntry rootv = history.getState().getChildNodeEntry(JCR_ROOTVERSION, 1);
                if (rootv == null) {
                    throw new InconsistentVersioningState("missing child node entry for " + JCR_ROOTVERSION + " on version history node " + history.getNodeId(),
                            history.getNodeId(), null);
                }
                info = new VersionHistoryInfo(history.getNodeId(),
                        rootv.getId());
            }
        } finally {
            lock.release();
        }

        return info;
    }

    /**
     * {@inheritDoc}
     */
    public VersionHistoryInfo getVersionHistory(Session session, NodeState node,
                                                NodeId copiedFrom)
            throws RepositoryException {
        VersionHistoryInfo info = getVersionHistoryInfoForNode(node);

        if (info == null) {
            info = createVersionHistory(session, node, copiedFrom);
        }

        return info;
    }

    /**
     * Creates a new version history. This action is needed either when creating
     * a new 'mix:versionable' node or when adding the 'mix:versionable' mixin
     * to a node.
     *
     * @param session repository session
     * @param node versionable node state
     * @param copiedFrom node id for the jcr:copiedFrom property
     * @return identifier of the new version history node
     * @throws RepositoryException if an error occurs
     * @see #getVersionHistory(Session, NodeState, NodeId)
     */
    protected abstract VersionHistoryInfo createVersionHistory(Session session,
                                                               NodeState node,
                                                               NodeId copiedFrom)
            throws RepositoryException;

    /**
     * Returns the item with the given persistent id. Subclass responsibility.
     * <p>
     * Please note, that the overridden method must acquire the readlock before
     * reading the state manager.
     *
     * @param id the id of the item
     * @return version item
     * @throws RepositoryException if an error occurs
     */
    protected abstract InternalVersionItem getItem(NodeId id)
            throws RepositoryException;

    /**
     * Return a flag indicating if the item specified exists.
     * Subclass responsibility.
     * @param id the id of the item
     * @return <code>true</code> if the item exists;
     *         <code>false</code> otherwise
     */
    protected abstract boolean hasItem(NodeId id);

    /**
     * Checks if there are item references (from outside the version storage)
     * that reference the given node. Subclass responsibility.
     * <p>
     * Please note, that the overridden method must acquire the readlock before
     * reading the state manager.
     *
     * @param id the id of the node
     * @return <code>true</code> if there are item references from outside the
     *         version storage; <code>false</code> otherwise.
     * @throws RepositoryException if an error occurs while reading from the
     *                             repository.
     */
    protected abstract boolean hasItemReferences(NodeId id)
            throws RepositoryException;

    /**
     * Returns the node with the given persistent id. Subclass responsibility.
     * <p>
     * Please note, that the overridden method must acquire the readlock before
     * reading the state manager.
     *
     * @param id the id of the node
     * @throws RepositoryException if an error occurs while reading from the
     *                             repository.
     * @return the nodestate for the given id.
     */
    protected abstract NodeStateEx getNodeStateEx(NodeId id)
            throws RepositoryException;

    /**
     * Creates a new Version History.
     *
     * @param node the node for which the version history is to be initialized
     * @param copiedFrom node id for the jcr:copiedFrom parameter
     * @return the identifiers of the newly created version history and root version
     * @throws RepositoryException if an error occurs
     */
    NodeStateEx internalCreateVersionHistory(NodeState node, NodeId copiedFrom)
            throws RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            // create deep path
            String uuid = node.getNodeId().toString();
            NodeStateEx parent = getParentNode(getHistoryRoot(), uuid, NameConstants.REP_VERSIONSTORAGE);
            Name name = getName(uuid);
            if (parent.hasNode(name)) {
                // already exists
                return null;
            }

            // create new history node in the persistent state
            NodeStateEx history =
                InternalVersionHistoryImpl.create(this, parent, name, node, copiedFrom);

            // end update
            operation.save();

            log.debug(
                    "Created new version history " + history.getNodeId()
                    + " for " + node + ".");
            return history;
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            operation.close();
        }
    }

    /**
     * Creates a new activity.
     *
     * @param title title of the new activity
     * @return the id of the newly created activity
     * @throws RepositoryException if an error occurs
     */
    NodeStateEx internalCreateActivity(String title)
            throws RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            // create deep path
            NodeId activityId = nodeIdFactory.newNodeId();
            NodeStateEx parent = getParentNode(getActivitiesRoot(), activityId.toString(), NameConstants.REP_ACTIVITIES);
            Name name = getName(activityId.toString());

            // create new activity node in the persistent state
            NodeStateEx pNode = InternalActivityImpl.create(parent, name, activityId, title);

            // end update
            operation.save();

            log.debug("Created new activity " + activityId
                    + " with title " + title + ".");
            return pNode;
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            operation.close();
        }
    }

    /**
     * Removes the specified activity
     *
     * @param activity the activity to remove
     * @throws javax.jcr.RepositoryException if any other error occurs.
     */
    protected void internalRemoveActivity(InternalActivityImpl activity)
            throws RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            // check if the activity has any references in the workspaces
            NodeId nodeId = activity.getId();
            if (stateMgr.hasNodeReferences(nodeId)) {
                NodeReferences refs = stateMgr.getNodeReferences(nodeId);
                if (refs.hasReferences()) {
                    throw new ReferentialIntegrityException("Unable to delete activity. still referenced.");
                }
            }
            // TODO:
            // check if the activity is used in anywhere in the version storage
            // and reject removal

            // remove activity and possible empty parent directories
            NodeStateEx act = getNodeStateEx(nodeId);
            NodeId parentId = act.getParentId();
            Name name = act.getName();
            while (parentId != null) {
                NodeStateEx parent = getNodeStateEx(parentId);
                parent.removeNode(name);
                parent.store();
                if (parent.getChildNodes().length == 0 && !parentId.equals(activitiesId)) {
                    name = parent.getName();
                    parentId = parent.getParentId();
                } else {
                    parentId = null;
                }
            }
            operation.save();
        } catch (ItemStateException e) {
            log.error("Error while storing: " + e.toString());
        } finally {
            operation.close();
        }
    }

    /**
     * Utility method that returns the given string as a name in the default
     * namespace.
     *
     * @param name string name
     * @return A <code>Name</code> object.
     */
    protected static Name getName(String name) {
        return NameFactoryImpl.getInstance().create(Name.NS_DEFAULT_URI, name);
    }

    /**
     * Utility method that returns the parent node under which the version
     * history of the identified versionable node is or will be stored. If
     * the <code>interNT</code> is not <code>null</code> then the returned
     * parent node and any ancestor nodes are automatically created if they do
     * not already exist. Otherwise
     * <code>null</code> is returned if the parent node does not exist.
     *
     * @param parent the parent node
     * @param uuid UUID of a versionable node
     * @param interNT intermediate nodetype.
     * @return parent node of the version history, or <code>null</code>
     * @throws RepositoryException if an error occurs
     */
    protected static NodeStateEx getParentNode(NodeStateEx parent, String uuid, Name interNT)
            throws RepositoryException {
        NodeStateEx n = parent;
        for (int i = 0; i < 3; i++) {
            Name name = getName(uuid.substring(i * 2, i * 2 + 2));
            NodeStateEx childn = n.getNode(name, 1);
            if (childn != null) {
                n = childn;
            } else if (interNT != null) {
                childn = n.addNode(name, interNT, null, false);
                n.store(false);
                childn.store(true);
                n = childn;
            } else {
                return null;
            }
        }
        return n;
    }

    /**
     * Creates a new version of the given node using the given version
     * creation time.
     *
     * @param node the node to be checked in
     * @param created version creation time
     * @return the new version
     * @throws RepositoryException if an error occurs
     */
    protected InternalVersion checkin(NodeStateEx node, Calendar created)
            throws RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            boolean simple =
                !node.getEffectiveNodeType().includesNodeType(MIX_VERSIONABLE);
            InternalVersionHistoryImpl vh;
            if (simple) {
                // in simple versioning the history id needs to be calculated
                vh = (InternalVersionHistoryImpl) getVersionHistoryOfNode(
                        node.getNodeId());
            } else {
                // in full versioning, the history id can be retrieved via
                // the property
                vh = (InternalVersionHistoryImpl) getVersionHistory(
                        node.getPropertyValue(JCR_VERSIONHISTORY).getNodeId());
            }

            InternalVersion version =
                internalCheckin(vh, node, simple, created);

            operation.save();
            return version;
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        } finally {
            operation.close();
        }
    }

    /**
     * Checks in a node
     *
     * @param history the version history
     * @param node node to checkin
     * @param simple flag indicates simple versioning
     * @param created optional created date.
     * @return internal version
     * @throws javax.jcr.RepositoryException if an error occurs
     * @see javax.jcr.Node#checkin()
     */
    protected InternalVersion internalCheckin(
            InternalVersionHistoryImpl history,
            NodeStateEx node, boolean simple, Calendar created)
            throws RepositoryException {
        String versionName = calculateCheckinVersionName(history, node, simple);
        InternalVersionImpl v = history.checkin(
                NameFactoryImpl.getInstance().create("", versionName),
                node, created);

        // check for jcr:activity
        if (node.hasProperty(JCR_ACTIVITY)) {
            NodeId actId = node.getPropertyValue(JCR_ACTIVITY).getNodeId();
            InternalActivityImpl act = (InternalActivityImpl) getItem(actId);
            act.addVersion(v);
        }

        return v;
    }

    /**
     * Calculates the name of the new version that will be created by a
     * checkin call. The name is determined as follows:
     * <ul>
     * <li> first the predecessor version with the shortest name is searched.
     * <li> if that predecessor version is the root version, the new version gets
     *      the name "{number of successors}+1" + ".0"
     * <li> if that predecessor version has no successor, the last digit of it's
     *      version number is incremented.
     * <li> if that predecessor version has successors but the incremented name
     *      does not exist, that name is used.
     * <li> otherwise a ".0" is added to the name until a non conflicting name
     *      is found.
     * <ul>
     *
     * Example Graph:
     * <xmp>
     * jcr:rootVersion
     *  |     |
     * 1.0   2.0
     *  |
     * 1.1
     *  |
     * 1.2 ---\  ------\
     *  |      \        \
     * 1.3   1.2.0   1.2.0.0
     *  |      |
     * 1.4   1.2.1 ----\
     *  |      |        \
     * 1.5   1.2.2   1.2.1.0
     *  |      |        |
     * 1.6     |     1.2.1.1
     *  |-----/
     * 1.7
     * </xmp>
     *
     * @param history the version history
     * @param node the node to checkin
     * @param simple if <code>true</code> indicates simple versioning
     * @return the new version name
     * @throws RepositoryException if an error occurs.
     */
    protected String calculateCheckinVersionName(InternalVersionHistoryImpl history,
                                                 NodeStateEx node, boolean simple)
            throws RepositoryException {

        if (history == null) {
            String message = "Node " + node.getNodeId() + " has no version history";
            log.error(message);
            throw new VersionException(message);
        }

        InternalVersion best = null;
        if (simple) {
            // 1. in simple versioning just take the 'head' version
            Name[] names = history.getVersionNames();
            best = history.getVersion(names[names.length - 1]);
        } else {
            // 1. search a predecessor, suitable for generating the new name
            InternalValue[] values = node.getPropertyValues(NameConstants.JCR_PREDECESSORS);

            if (values == null || values.length == 0) {
                String message;
                if (values == null) {
                    message = "Mandatory jcr:predecessors property missing on node " + node.getNodeId();
                } else {
                    message = "Mandatory jcr:predecessors property is empty on node " + node.getNodeId();
                }
                log.error(message);
                throw new VersionException(message);
            }

            for (InternalValue value: values) {
                InternalVersion pred = history.getVersion(value.getNodeId());
                if (pred == null) {
                    String message = "Could not instantiate InternalVersion for nodeId " + value.getNodeId() + " (VHR + " + history.getId() + ", node " + node.getNodeId() + ")";
                    log.error(message);
                    throw new VersionException(message);
                }
                if (best == null
                        || pred.getName().getLocalName().length() < best.getName().getLocalName().length()) {
                    best = pred;
                }
            }
        }

        if (best == null) {
            String message = "Could not find 'best' predecessor node for " + node.getNodeId();
            log.error(message);
            throw new VersionException(message);
        }

        // 2. generate version name (assume no namespaces in version names)
        String versionName = best.getName().getLocalName();
        int pos = versionName.lastIndexOf('.');
        if (pos > 0) {
            String newVersionName = versionName.substring(0, pos + 1)
                + (Integer.parseInt(versionName.substring(pos + 1)) + 1);
            while (history.hasVersion(NameFactoryImpl.getInstance().create("", newVersionName))) {
                versionName += ".0";
                newVersionName = versionName;
            }
            return newVersionName;
        } else {
            // best is root version
            return String.valueOf(best.getSuccessors().size() + 1) + ".0";
        }
    }

    /**
     * Removes the specified version from the history
     *
     * @param history the version history from where to remove the version.
     * @param name the name of the version to remove.
     * @throws javax.jcr.version.VersionException if the version <code>history</code> does
     *  not have a version with <code>name</code>.
     * @throws javax.jcr.RepositoryException if any other error occurs.
     */
    protected void internalRemoveVersion(InternalVersionHistoryImpl history, Name name)
            throws VersionException, RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            history.removeVersion(name);
            operation.save();
        } catch (ItemStateException e) {
            log.error("Error while storing: " + e.toString());
        } finally {
            operation.close();
        }
    }

    /**
     * Removes the specified history from the storage
     *
     * @param history the version history to remove
     * @throws VersionException
     * @throws RepositoryException
     */
    public void internalRemoveVersionHistory(InternalVersionHistoryImpl history)
            throws VersionException, RepositoryException {
        String versionableUuid = history.getVersionableId().toString();
        WriteOperation operation = startWriteOperation();
        try {
            NodeStateEx parent = getParentNode(getHistoryRoot(), versionableUuid, null);
            parent.removeNode(history.node.getName());
            parent.store();
            operation.save();
        } catch (ItemStateException e) {
            log.error("Error while storing: " + e.toString());
        } finally {
            operation.close();
        }
    }

    /**
     * Set version label on the specified version.
     *
     * @param history version history
     * @param version version name
     * @param label version label
     * @param move <code>true</code> to move from existing version;
     *             <code>false</code> otherwise.
     * @return The internal version.
     * @throws RepositoryException if an error occurs
     */
    protected InternalVersion setVersionLabel(InternalVersionHistoryImpl history,
                                              Name version, Name label,
                                              boolean move)
            throws RepositoryException {
        WriteOperation operation = startWriteOperation();
        try {
            InternalVersion v = history.setVersionLabel(version, label, move);
            operation.save();
            return v;
        } catch (ItemStateException e) {
            log.error("Error while storing: " + e.toString());
            return null;
        } finally {
            operation.close();
        }
    }

    /**
     * Invoked when a new internal item has been created.
     * @param version internal version item
     */
    protected void versionCreated(InternalVersion version) {
    }

    /**
     * Invoked when a new internal item has been destroyed.
     * @param version internal version item
     */
    protected void versionDestroyed(InternalVersion version) {
    }

    /**
     * Invoked by the internal version item itself, when it's underlying
     * persistence state was discarded.
     *
     * @param item item that was discarded
     */
    protected void itemDiscarded(InternalVersionItem item) {
    }

    /**
     * Creates an {@link InternalVersionItem} based on the {@link NodeState}
     * identified by <code>id</code>.
     *
     * @param id    the node id of the version item.
     * @return the version item or <code>null</code> if there is no node state
     *         with the given <code>id</code>.
     * @throws RepositoryException if an error occurs while reading from the
     *                             version storage.
     */
    protected InternalVersionItem createInternalVersionItem(NodeId id)
            throws RepositoryException {
        try {
            if (stateMgr.hasItemState(id)) {
                NodeState state = (NodeState) stateMgr.getItemState(id);
                NodeStateEx pNode = new NodeStateEx(stateMgr, ntReg, state, null);
                NodeId parentId = pNode.getParentId();
                InternalVersionItem parent = getItem(parentId);
                Name ntName = state.getNodeTypeName();
                if (ntName.equals(NameConstants.NT_FROZENNODE)) {
                    return new InternalFrozenNodeImpl(this, pNode, parent);
                } else if (ntName.equals(NameConstants.NT_VERSIONEDCHILD)) {
                    return new InternalFrozenVHImpl(this, pNode, parent);
                } else if (ntName.equals(NameConstants.NT_VERSION)) {
                    return ((InternalVersionHistory) parent).getVersion(id);
                } else if (ntName.equals(NameConstants.NT_VERSIONHISTORY)) {
                    return new InternalVersionHistoryImpl(this, pNode);
                } else if (ntName.equals(NameConstants.NT_ACTIVITY)) {
                    return new InternalActivityImpl(this, pNode);
                } else {
                    return null;
                }
            } else {
                return null;
            }
        } catch (ItemStateException e) {
            throw new RepositoryException(e);
        }
    }

    public NodeIdFactory getNodeIdFactory() {
        return nodeIdFactory;
    }

}
