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

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import javax.jcr.ItemNotFoundException;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockException;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;

import org.apache.commons.collections.map.LinkedMap;
import org.apache.commons.io.IOUtils;
import org.apache.jackrabbit.core.NodeImpl;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.SessionListener;
import org.apache.jackrabbit.core.WorkspaceImpl;
import org.apache.jackrabbit.core.cluster.ClusterOperation;
import org.apache.jackrabbit.core.cluster.LockEventChannel;
import org.apache.jackrabbit.core.cluster.LockEventListener;
import org.apache.jackrabbit.core.fs.FileSystem;
import org.apache.jackrabbit.core.fs.FileSystemException;
import org.apache.jackrabbit.core.fs.FileSystemResource;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.id.PropertyId;
import org.apache.jackrabbit.core.observation.EventImpl;
import org.apache.jackrabbit.core.observation.SynchronousEventListener;
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.UpdatableItemStateManager;
import org.apache.jackrabbit.core.util.XAReentrantLock;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Provides the functionality needed for locking and unlocking nodes.
 */
public class LockManagerImpl
        implements LockManager, SynchronousEventListener, LockEventListener {

    /**
     * Logger
     */
    private static final Logger log = LoggerFactory.getLogger(LockManagerImpl.class);

    /**
     * Name of the lock file
     */
    private static final String LOCKS_FILE = "locks";

    /**
     * Path map containing all locks at the leaves.
     */
    private final PathMap<LockInfo> lockMap = new PathMap<LockInfo>();

    /**
     * XA/Thread aware lock to path map.
     */
    private final XAReentrantLock lockMapLock = new XAReentrantLock();
    
    /**
     * XA/Thread aware lock for lock properties
     */
    private XAReentrantLock lockPropertiesLock = new XAReentrantLock();

    /**
     * The periodically invoked lock timeout handler.
     */
    private final ScheduledFuture<?> timeoutHandler;

    /**
     * System session
     */
    private final SessionImpl sysSession;

    /**
     * Locks file
     */
    private final FileSystemResource locksFile;

    /**
     * Flag indicating whether automatic saving is disabled.
     */
    private boolean savingDisabled;

    /**
     * Lock event channel.
     */
    private LockEventChannel eventChannel;

    /**
     * Create a new instance of this class.
     *
     * @param session  system session
     * @param fs       file system for persisting locks
     * @param executor scheduled executor service for handling lock timeouts
     * @throws RepositoryException if an error occurs
     */
    public LockManagerImpl(
            SessionImpl session, FileSystem fs,
            ScheduledExecutorService executor) throws RepositoryException {

        this.sysSession = session;
        this.locksFile = new FileSystemResource(fs, FileSystem.SEPARATOR + LOCKS_FILE);

        session.getWorkspace().getObservationManager().
                addEventListener(this, Event.NODE_ADDED | Event.NODE_REMOVED,
                        "/", true, null, null, true);

        try {
            if (locksFile.exists()) {
                load();
            }
        } catch (FileSystemException e) {
            throw new RepositoryException("I/O error while reading locks from '"
                    + locksFile.getPath() + "'", e);
        }

        timeoutHandler = executor.scheduleWithFixedDelay(
                new TimeoutHandler(), 1, 1, TimeUnit.SECONDS);
    }

    /**
     * Close this lock manager. Writes back all changes.
     */
    public void close() {
        timeoutHandler.cancel(false);
        save();
    }

    /**
     * Periodically (at one second delay) invoked timeout handler. Traverses
     * all locks and unlocks those that have expired.
     *
     * @see <a href="https://issues.apache.org/jira/browse/JCR-1590">JCR-1590</a>:
     *      JSR 283: Locking
     */
    private class TimeoutHandler implements Runnable {
        private final TimeoutHandlerVisitor visitor = new TimeoutHandlerVisitor();

        public void run() {
            lockMap.traverse(visitor, false);
        }
    }

    private class TimeoutHandlerVisitor implements
            PathMap.ElementVisitor<LockInfo> {
        public void elementVisited(PathMap.Element<LockInfo> element) {
            LockInfo info = element.get();
            if (info != null && info.isLive() && info.isExpired()) {
                NodeId id = info.getId();
                SessionImpl holder = info.getLockHolder();
                if (holder == null) {
                    info.setLockHolder(sysSession);
                    holder = sysSession;
                }
                try {
                    // FIXME: This session access is not thread-safe!
                    log.debug("Try to unlock expired lock. NodeId {}", id);
                    unlock(holder.getNodeById(id));
                } catch (RepositoryException e) {
                    log.warn("Unable to expire the lock. NodeId " + id, e);
                }
            }
        }
    }

    /**
     * Read locks from locks file and populate path map
     */
    private void load() throws FileSystemException {
        BufferedReader reader = null;

        try {
            reader = new BufferedReader(
                    new InputStreamReader(locksFile.getInputStream()));
            while (true) {
                String s = reader.readLine();
                if (s == null || s.equals("")) {
                    break;
                }
                reapplyLock(s);
            }
        } catch (IOException e) {
            throw new FileSystemException("error while reading locks file", e);
        } finally {
            IOUtils.closeQuietly(reader);
        }
    }

    /**
     * Reaply a lock given a lock token that was read from the locks file
     *
     * @param lockTokenLine lock token to apply
     */
    private void reapplyLock(String lockTokenLine) {
        String[] parts = lockTokenLine.split(",");
        String token = parts[0];
        long timeoutHint = Long.MAX_VALUE;
        if (parts.length > 1) {
            try {
                timeoutHint = Long.parseLong(parts[1]);
            } catch (NumberFormatException e) {
                log.warn("Unexpected timeout hint "
                        + parts[1] + " for lock token " + token, e);
            }
        }

        try {
        	acquire();
        	
            NodeId id = LockInfo.parseLockToken(parts[0]);
            NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id);
            Path path = getPath(sysSession, id);

            InternalLockInfo info = new InternalLockInfo(
                    id, false,
                    node.getProperty(NameConstants.JCR_LOCKISDEEP).getBoolean(),
                    node.getProperty(NameConstants.JCR_LOCKOWNER).getString(),
                    timeoutHint);
            info.setLive(true);
            lockMap.put(path, info);
        } catch (RepositoryException e) {
            log.warn("Unable to recreate lock '" + token + "': " + e.getMessage());
            log.debug("Root cause: ", e);
        } finally {
        	release();
        }
    }

    /**
     * Write locks to locks file
     */
    private void save() {
        if (savingDisabled) {
            return;
        }

        final ArrayList<LockInfo> list = new ArrayList<LockInfo>();

        lockMap.traverse(new PathMap.ElementVisitor<LockInfo>() {
            public void elementVisited(PathMap.Element<LockInfo> element) {
                LockInfo info = element.get();
                if (!info.isSessionScoped()) {
                    list.add(info);
                }
            }
        }, false);


        BufferedWriter writer = null;

        try {
            writer = new BufferedWriter(
                    new OutputStreamWriter(locksFile.getOutputStream()));
            for (LockInfo info : list) {
                writer.write(info.getLockToken());

                // Store the timeout hint, if one is specified
                if (info.getTimeoutHint() != Long.MAX_VALUE) {
                    writer.write(',');
                    writer.write(Long.toString(info.getTimeoutHint()));
                }

                writer.newLine();
            }
        } catch (FileSystemException fse) {
            log.warn("I/O error while saving locks to '"
                    + locksFile.getPath() + "': " + fse.getMessage());
            log.debug("Root cause: ", fse);
        } catch (IOException ioe) {
            log.warn("I/O error while saving locks to '"
                    + locksFile.getPath() + "': " + ioe.getMessage());
            log.debug("Root cause: ", ioe);
        } finally {
            IOUtils.closeQuietly(writer);
        }
    }

    static SessionLockManager getSessionLockManager(SessionImpl session) throws RepositoryException {
        Workspace wsp = session.getWorkspace();
        return (SessionLockManager) wsp.getLockManager();
    }

    /**
     * Internal <code>lock</code> implementation that takes the same parameters
     * as the public method.
     *
     * @param node node to lock
     * @param isDeep whether the lock applies to this node only
     * @param isSessionScoped whether the lock is session scoped
     * @param timeoutHint
     * @param ownerInfo
     * @return lock
     * @throws LockException       if the node is already locked
     * @throws RepositoryException if another error occurs
     */
    LockInfo internalLock(
            NodeImpl node, boolean isDeep, boolean isSessionScoped,
            long timeoutHint, String ownerInfo)
            throws LockException, RepositoryException {

        SessionImpl session = (SessionImpl) node.getSession();
        String lockOwner = (ownerInfo != null) ? ownerInfo : session.getUserID();
        InternalLockInfo info = new InternalLockInfo(
                node.getNodeId(), isSessionScoped, isDeep, lockOwner, timeoutHint);

        ClusterOperation operation = null;
        boolean successful = false;

        // Cluster is only informed about open-scoped locks
        if (eventChannel != null && !isSessionScoped) {
            operation = eventChannel.create(node.getNodeId(), isDeep, lockOwner);
        }

        acquire();

        try {
            // check whether node is already locked
            Path path = getPath(session, node.getId());
            PathMap.Element<LockInfo> element = lockMap.map(path, false);

            LockInfo other = element.get();
            if (other != null) {
                if (element.hasPath(path)) {
                    other.throwLockException(
                            "Node already locked: " + node, session);
                } else if (other.isDeep()) {
                    other.throwLockException(
                            "Parent node has a deep lock: " + node, session);
                }
            }
            if (info.isDeep() && element.hasPath(path)
                    && element.getChildrenCount() > 0) {
                info.throwLockException("Some child node is locked", session);
            }

            // create lock token
            info.setLockHolder(session);
            info.setLive(true);
            session.addListener(info);
            if (!info.isSessionScoped()) {
                getSessionLockManager(session).lockTokenAdded(info.getLockToken());
            }
            lockMap.put(path, info);

            if (!info.isSessionScoped()) {
                save();
                successful = true;
            }
            return info;

        } finally {
            release();
            if (operation != null) {
                operation.ended(successful);
            }
        }
    }

    /**
     * Unlock a node (internal implementation)
     * @param node node to unlock
     * @throws LockException       if the node can not be unlocked
     * @throws RepositoryException if another error occurs
     */
    boolean internalUnlock(NodeImpl node)
            throws LockException, RepositoryException {

        ClusterOperation operation = null;
        boolean successful = false;

        if (eventChannel != null) {
            operation = eventChannel.create(node.getNodeId());
        }

        acquire();

        try {
            SessionImpl session = (SessionImpl) node.getSession();
            // check whether node is locked by this session
            PathMap.Element<LockInfo> element =
                lockMap.map(getPath(session, node.getId()), true);
            if (element == null) {
                throw new LockException("Node not locked: " + node);
            }
            LockInfo info = element.get();
            if (info == null) {
                throw new LockException("Node not locked: " + node);
            }
            checkUnlock(info, session);

            getSessionLockManager(session).lockTokenRemoved(info.getLockToken());

            element.set(null);
            info.setLive(false);

            if (!info.isSessionScoped()) {
                save();
                successful = true;
            }
            return true;
        } finally {
            release();

            if (operation != null) {
                operation.ended(successful);
            }
        }
    }

    /**
     * Package-private low-level helper method returning all locks
     * associated with the specified session.
     * @param session session
     * @return an array of <code>AbstractLockInfo</code>s
     */
    LockInfo[] getLockInfos(final SessionImpl session) {
        final ArrayList<LockInfo> infos = new ArrayList<LockInfo>();
        lockMap.traverse(new PathMap.ElementVisitor<LockInfo>() {
            public void elementVisited(PathMap.Element<LockInfo> element) {
                LockInfo info = element.get();
                if (info.isLive() && info.isLockHolder(session)) {
                    infos.add(info);
                }
            }
        }, false);
        return infos.toArray(new LockInfo[infos.size()]);
    }

    /**
     * Helper method that copies all the active open-scoped locks from the
     * given source to this lock manager. This method is used when backing
     * up repositories, and only works correctly when the source lock manager
     * belongs to the original copy of the workspace being backed up.
     *
     * @see org.apache.jackrabbit.core.RepositoryCopier
     * @param source source lock manager
     */
    public void copyOpenScopedLocksFrom(LockManagerImpl source) {
        source.lockMap.traverse(new PathMap.ElementVisitor<LockInfo>() {
            public void elementVisited(PathMap.Element<LockInfo> element) {
                LockInfo info = element.get();
                if (info.isLive() && !info.isSessionScoped()) {
                    try {
                        lockMap.put(element.getPath(), info);
                    } catch (MalformedPathException e) {
                        log.warn("Ignoring invalid lock path: " + info, e);
                    }
                }
            }
        }, false);
    }

    /**
     * Return the most appropriate lock information for a node. This is either
     * the lock info for the node itself, if it is locked, or a lock info for one
     * of its parents, if that is deep locked.
     * @return lock info or <code>null</code> if node is not locked
     * @throws RepositoryException if an error occurs
     */
    public LockInfo getLockInfo(NodeId id) throws RepositoryException {
        Path path;
        try {
            path = getPath(sysSession, id);
        } catch (ItemNotFoundException e) {
            return null;
        }

        acquire();
        try {
            PathMap.Element<LockInfo> element = lockMap.map(path, false);
            LockInfo info = element.get();
            if (info != null) {
                if (element.hasPath(path) || info.isDeep()) {
                    return info;
                }
            }
            return null;
        } finally {
            release();
        }
    }

    //----------------------------------------------------------< LockManager >

    /**
     * {@inheritDoc}
     */
    public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped)
            throws LockException, RepositoryException {
        return lock(node, isDeep, isSessionScoped, Long.MAX_VALUE, null);
    }

    public Lock lock(NodeImpl node, boolean isDeep, boolean isSessionScoped, long timoutHint, String ownerInfo)
            throws LockException, RepositoryException {
        LockInfo info = internalLock(node, isDeep, isSessionScoped, timoutHint, ownerInfo);
        writeLockProperties(node, info.getLockOwner(), info.isDeep());

        return new LockImpl(info, node);
    }

    /**
     * {@inheritDoc}
     */
    public Lock getLock(NodeImpl node)
            throws LockException, RepositoryException {

        acquire();

        try {
            SessionImpl session = (SessionImpl) node.getSession();
            Path path = getPath(session, node.getId());

            PathMap.Element<LockInfo> element = lockMap.map(path, false);
            LockInfo info = element.get();
            if (info != null && (element.hasPath(path) || info.isDeep())) {
                NodeImpl lockHolder = (NodeImpl)
                    session.getItemManager().getItem(info.getId());
                return new LockImpl(info, lockHolder);
            } else {
                throw new LockException("Node not locked: " + node);
            }
        } catch (ItemNotFoundException e) {
            throw new LockException("Node not locked: " + node);
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     */
    public Lock[] getLocks(SessionImpl session) throws RepositoryException {

        acquire();

        LockInfo[] infos = getLockInfos(session);

        try {
            Lock[] locks = new Lock[infos.length];
            for (int i = 0; i < infos.length; i++) {
                NodeImpl holder = (NodeImpl)
                    session.getItemManager().getItem(infos[i].getId());
                locks[i] = new LockImpl(infos[i], holder);
            }
            return locks;
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     * <p>
     * In order to prevent deadlocks from within the synchronous dispatching of
     * events, content modifications should not be made from within code
     * sections that hold monitors. (see #JCR-194)
     */
    public void unlock(NodeImpl node) throws LockException, RepositoryException {
        removeLockProperties(node);
        internalUnlock(node);
    }

    /**
     * {@inheritDoc}
     */
    public boolean holdsLock(NodeImpl node) throws RepositoryException {
        acquire();

        try {
            SessionImpl session = (SessionImpl) node.getSession();
            PathMap.Element<LockInfo> element =
                lockMap.map(getPath(session, node.getId()), true);
            if (element == null) {
                return false;
            }
            return element.get() != null;
        } catch (ItemNotFoundException e) {
            return false;
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean isLocked(NodeImpl node) throws RepositoryException {
        acquire();

        try {
            SessionImpl session = (SessionImpl) node.getSession();
            Path path = getPath(session, node.getId());

            PathMap.Element<LockInfo> element = lockMap.map(path, false);
            LockInfo info = element.get();
            if (info == null) {
                return false;
            }
            if (element.hasPath(path)) {
                return true;
            } else {
                return info.isDeep();
            }
        } catch (ItemNotFoundException e) {
            return false;
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void checkLock(NodeImpl node)
            throws LockException, RepositoryException {

        SessionImpl session = (SessionImpl) node.getSession();
        checkLock(getPath(session, node.getId()), session);
    }

    /**
     * {@inheritDoc}
     */
    public void checkLock(Path path, Session session)
            throws LockException, RepositoryException {

        acquire();
        try {
            PathMap.Element<LockInfo> element = lockMap.map(path, false);
            LockInfo info = element.get();
            if (info != null) {
                if (element.hasPath(path) || info.isDeep()) {
                    checkLock(info, session);
                }
            }
        } finally {
            release();
        }
    }

    /**
     * Check whether a lock info allows access to a session. May be overridden
     * by subclasses to allow access to nodes for sessions other than the
     * lock holder itself.
     * <p>
     * Default implementation allows access to the lock holder only.
     *
     * @param info info to check
     * @param session session
     * @throws LockException if write access to the specified path is not allowed
     * @throws RepositoryException if some other error occurs
     */
    protected void checkLock(LockInfo info, Session session)
            throws LockException, RepositoryException {

        if (!info.isLockHolder(session)) {
            throw new LockException("Node locked.");
        }
    }

    /**
     * {@inheritDoc}
     */
    public void checkUnlock(Session session, NodeImpl node)
            throws LockException, RepositoryException {
        acquire();
        
        try {
            // check whether node is locked by this session
            PathMap.Element<LockInfo> element =
                lockMap.map(getPath((SessionImpl) session, node.getId()), true);
            if (element == null) {
                throw new LockException("Node not locked: " + node);
            }
            LockInfo info = element.get();
            if (info == null) {
                throw new LockException("Node not locked: " + node);
            }
            checkUnlock(info, session);
        } finally {
            release();
        }
    }

    /**
     * Check whether a session is allowed to unlock a node. May be overridden
     * by subclasses to allow this to sessions other than the lock holder
     * itself.
     * <p>
     * Default implementation allows unlocking to the lock holder only.
     *
     * @param info info to check
     * @param session session
     * @throws LockException if unlocking is denied
     * @throws RepositoryException if some other error occurs
     */
    protected void checkUnlock(LockInfo info, Session session)
            throws LockException, RepositoryException {

        if (!info.isLockHolder(session)) {
            throw new LockException("Node not locked by session: "
                    + info.getId());
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addLockToken(SessionImpl session, String lt) throws LockException, RepositoryException {
        try {
            acquire();
            
            NodeId id = LockInfo.parseLockToken(lt);

            NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id);
            Path path = node.getPrimaryPath();
            PathMap.Element<LockInfo> element = lockMap.map(path, true);
            if (element != null) {
                LockInfo info = element.get();
                if (info != null && !info.isLockHolder(session)) {
                    if (info.getLockHolder() == null) {
                        info.setLockHolder(session);
                        if (info instanceof InternalLockInfo) {
                            session.addListener((InternalLockInfo) info);
                        }
                    } else {
                        String msg = "Cannot add lock token: lock already held by other session.";
                        log.warn(msg);
                        info.throwLockException(msg, session);
                    }
                }
            }
            // inform SessionLockManager
            getSessionLockManager(session).lockTokenAdded(lt);
        } catch (IllegalArgumentException e) {
            String msg = "Bad lock token: " + e.getMessage();
            log.warn(msg);
            throw new LockException(msg);
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeLockToken(SessionImpl session, String lt)
            throws LockException, RepositoryException {

        try {
            acquire();
            
            NodeId id = LockInfo.parseLockToken(lt);

            NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(id);
            PathMap.Element<LockInfo> element =
                lockMap.map(node.getPrimaryPath(), true);
            if (element != null) {
                LockInfo info = element.get();
                if (info != null) {
                    if (info.isLockHolder(session)) {
                        info.setLockHolder(null);
                    } else if (info.getLockHolder() != null) {
                        String msg = "Cannot remove lock token: lock held by other session.";
                        log.warn(msg);
                        info.throwLockException(msg, session);
                    }
                }
            }
            // inform SessionLockManager
            getSessionLockManager(session).lockTokenRemoved(lt);
        } catch (IllegalArgumentException e) {
            String msg = "Bad lock token: " + e.getMessage();
            log.warn(msg);
            throw new LockException(msg);
        } finally {
            release();
        }
    }

    /**
     * Return the path of an item given its id. This method will lookup the
     * item inside the system session.
     */
    private Path getPath(SessionImpl session, ItemId id) throws RepositoryException {
        return session.getHierarchyManager().getPath(id);
    }

    /**
     * Acquire lock on the lock map.
     */
    private void acquire() {
        for (;;) {
            try {
           		lockMapLock.acquire();
                break;
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * Release lock on the lock map.
     */
    private void release() {
   		lockMapLock.release();
    }

    /**
     * Acquire lock for modifying lock properties
     */
    private void acquireLockPropertiesLock() {
        for (;;) {
            try {
                lockPropertiesLock.acquire();
                break;
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /**
     * Release lock on the lockPropertiesLock.
     */
    private void releaseLockPropertiesLock() {
        lockPropertiesLock.release();
    }

    /**
     * Start an update operation. This will acquire the lock on the lock map
     * and disable saving the lock map file.
     */
    public void beginUpdate() {
        acquire();
        savingDisabled = true;
    }

    /**
     * End an update operation. This will save the lock map file and release
     * the lock on the lock map.
     */
    public void endUpdate() {
        savingDisabled = false;
        save();
        release();
    }

    /**
     * Cancel an update operation. This will release the lock on the lock map.
     */
    public void cancelUpdate() {
        savingDisabled = false;
        release();
    }

    /**
     * Add the lock related properties to the target node.
     *
     * @param node
     * @param lockOwner
     * @param isDeep
     */
    protected void writeLockProperties(NodeImpl node, String lockOwner, boolean isDeep) throws RepositoryException {
        boolean success = false;

        SessionImpl editingSession = (SessionImpl) node.getSession();
        WorkspaceImpl wsp = (WorkspaceImpl) editingSession.getWorkspace();
        UpdatableItemStateManager stateMgr = wsp.getItemStateManager();

        try {
            acquireLockPropertiesLock();

            if (stateMgr.inEditMode()) {
                throw new RepositoryException("Unable to write lock properties.");
            }
            stateMgr.edit();
            try {
                // add properties to content
                NodeId nodeId = node.getNodeId();
                NodeState nodeState = (NodeState) stateMgr.getItemState(nodeId);

                PropertyState propState;
                if (!nodeState.hasPropertyName(NameConstants.JCR_LOCKOWNER)) {
                    propState = stateMgr.createNew(NameConstants.JCR_LOCKOWNER, nodeId);
                    propState.setType(PropertyType.STRING);
                    propState.setMultiValued(false);
                } else {
                    propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKOWNER));
                }
                propState.setValues(new InternalValue[] { InternalValue.create(lockOwner) });
                nodeState.addPropertyName(NameConstants.JCR_LOCKOWNER);
                stateMgr.store(nodeState);

                if (!nodeState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) {
                    propState = stateMgr.createNew(NameConstants.JCR_LOCKISDEEP, nodeId);
                    propState.setType(PropertyType.BOOLEAN);
                    propState.setMultiValued(false);
                } else {
                    propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKISDEEP));
                }
                propState.setValues(new InternalValue[] { InternalValue.create(isDeep) });
                nodeState.addPropertyName(NameConstants.JCR_LOCKISDEEP);
                stateMgr.store(nodeState);

                stateMgr.update();
                success = true;
            } catch (ItemStateException e) {
                throw new RepositoryException("Error while creating lock.", e);
            } finally {
                if (!success) {
                    // failed to set lock meta-data content, cleanup
                    stateMgr.cancel();
                    try {
                        unlock(node);
                    } catch (RepositoryException e) {
                        // cleanup failed
                        log.error("error while cleaning up after failed lock attempt", e);
                    }
                }
            }
        } finally {
            releaseLockPropertiesLock();
        }
    }

    /**
     *
     * @param node
     * @throws RepositoryException
     */
    protected void removeLockProperties(NodeImpl node) throws RepositoryException {
        boolean success = false;

        SessionImpl editingSession = (SessionImpl) node.getSession();
        WorkspaceImpl wsp = (WorkspaceImpl) editingSession.getWorkspace();
        UpdatableItemStateManager stateMgr = wsp.getItemStateManager();

        try {
            acquireLockPropertiesLock();

            // add properties to content
            if (stateMgr.inEditMode()) {
                throw new RepositoryException("Unable to remove lock properties.");
            }
            stateMgr.edit();
            try {
                NodeId nodeId = node.getNodeId();
                NodeState nodeState = (NodeState) stateMgr.getItemState(nodeId);
                if (nodeState.hasPropertyName(NameConstants.JCR_LOCKOWNER)) {
                    PropertyState propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKOWNER));
                    nodeState.removePropertyName(NameConstants.JCR_LOCKOWNER);
                    stateMgr.destroy(propState);
                    stateMgr.store(nodeState);
                }

                if (nodeState.hasPropertyName(NameConstants.JCR_LOCKISDEEP)) {
                    PropertyState propState = (PropertyState) stateMgr.getItemState(new PropertyId(nodeId, NameConstants.JCR_LOCKISDEEP));
                    nodeState.removePropertyName(NameConstants.JCR_LOCKISDEEP);
                    stateMgr.destroy(propState);
                    stateMgr.store(nodeState);
                }

                stateMgr.update();
                success = true;
            } catch (ItemStateException e) {
                throw new RepositoryException("Error while removing lock.", e);
            } finally {
                if (!success) {
                    // failed to set lock meta-data content, cleanup
                    stateMgr.cancel();
                }
            }
        } finally {
            releaseLockPropertiesLock();
        }
    }

    //----------------------------------------------< SynchronousEventListener >

    /**
     * Internal event class that holds old and new paths for moved nodes
     */
    private static class HierarchyEvent {

        /**
         * ID recorded in event
         */
        private final NodeId id;

        /**
         * Path recorded in event
         */
        private final Path path;

        /**
         * Old path in move operation
         */
        private Path oldPath;

        /**
         * New path in move operation
         */
        private Path newPath;

        /**
         * Event type, may be {@link Event#NODE_ADDED},
         * {@link Event#NODE_REMOVED} or a combination of both
         */
        private int type;

        /**
         * Create a new instance of this class.
         *
         * @param id id
         * @param path path
         * @param type event type
         */
        public HierarchyEvent(NodeId id, Path path, int type) {
            this.id = id;
            this.path = path;
            this.type = type;
        }

        /**
         * Merge this event with another event. The result will be stored in
         * this event
         *
         * @param event other event to merge with
         */
        public void merge(HierarchyEvent event) {
            type |= event.type;
            if (event.type == Event.NODE_ADDED) {
                newPath = event.path;
                oldPath = path;
            } else {
                oldPath = event.path;
                newPath = path;
            }
        }

        /**
         * Return the old path if this is a move operation
         *
         * @return old path
         */
        public Path getOldPath() {
            return oldPath;
        }

        /**
         * Return the new path if this is a move operation
         *
         * @return new path
         */
        public Path getNewPath() {
            return newPath;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void onEvent(EventIterator events) {
        Iterator<HierarchyEvent> iter = consolidateEvents(events);
        while (iter.hasNext()) {
            HierarchyEvent event = iter.next();
            if (event.type == Event.NODE_ADDED) {
                nodeAdded(event.path);
            } else if (event.type == Event.NODE_REMOVED) {
                nodeRemoved(event.path);
            } else if (event.type == (Event.NODE_ADDED | Event.NODE_REMOVED)) {
                nodeMoved(event.getOldPath(), event.getNewPath());
            }
        }
    }

    /**
     * Consolidate an event iterator obtained from observation, merging
     * add and remove operations on nodes with the same UUID into a move
     * operation.
     */
    @SuppressWarnings("unchecked")
	private Iterator<HierarchyEvent> consolidateEvents(EventIterator events) {
        LinkedMap eventMap = new LinkedMap();

        while (events.hasNext()) {
            EventImpl event = (EventImpl) events.nextEvent();
            HierarchyEvent he;

            try {
                he = new HierarchyEvent(event.getChildId(),
                        sysSession.getQPath(event.getPath()).getNormalizedPath(),
                        event.getType());
            } catch (MalformedPathException e) {
                log.info("Unable to get event's path: " + e.getMessage());
                continue;
            } catch (RepositoryException e) {
                log.info("Unable to get event's path: " + e.getMessage());
                continue;
            }

            HierarchyEvent heExisting = (HierarchyEvent) eventMap.get(he.id);
            if (heExisting != null) {
                heExisting.merge(he);
            } else {
                eventMap.put(he.id, he);
            }
        }
        return eventMap.values().iterator();
    }

    /**
     * Refresh a non-empty path element whose children might have changed
     * its position.
     */
    private void refresh(PathMap.Element<LockInfo> element) {
        final ArrayList<LockInfo> infos = new ArrayList<LockInfo>();
        boolean needsSave = false;

        // save away non-empty children
        element.traverse(new PathMap.ElementVisitor<LockInfo>() {
            public void elementVisited(PathMap.Element<LockInfo> element) {
                infos.add(element.get());
            }
        }, false);

        // remove all children
        element.removeAll();

        // now re-insert at appropriate location or throw away if node
        // does no longer exist
        for (int i = 0; i < infos.size(); i++) {
            LockInfo info = infos.get(i);
            try {
            	acquire();
            	
                NodeImpl node = (NodeImpl) sysSession.getItemManager().getItem(
                        info.getId());
                lockMap.put(node.getPrimaryPath(), info);
            } catch (RepositoryException e) {
                info.setLive(false);
                if (!info.isSessionScoped()) {
                    needsSave = true;
                }
            } finally {
            	release();
            }
        }

        // save if required
        if (needsSave) {
            save();
        }
    }

    /**
     * Invoked when some node has been added. If the parent of that node
     * exists, shift all name siblings of the new node having an index greater
     * or equal.
     *
     * @param path path of added node
     */
    private void nodeAdded(Path path) {
        acquire();

        try {
            PathMap.Element<LockInfo> parent =
                lockMap.map(path.getAncestor(1), true);
            if (parent != null) {
                refresh(parent);
            }
        } catch (RepositoryException e) {
            log.warn("Unable to determine path of added node's parent.", e);
        } finally {
            release();
        }
    }

    /**
     * Invoked when some node has been moved. Relink the child inside our
     * map to the new parent.
     *
     * @param oldPath old path
     * @param newPath new path
     */
    private void nodeMoved(Path oldPath, Path newPath) {
        acquire();

        try {
            PathMap.Element<LockInfo> parent =
                lockMap.map(oldPath.getAncestor(1), true);
            if (parent != null) {
                refresh(parent);
            }
        } catch (RepositoryException e) {
            log.warn("Unable to determine path of moved node's parent.", e);
        } finally {
            release();
        }
    }

    /**
     * Invoked when some node has been removed. Remove the child from our
     * path map. Disable all locks contained in that subtree.
     *
     * @param path path of removed node
     */
    private void nodeRemoved(Path path) {
        acquire();

        try {
            PathMap.Element<LockInfo> parent =
                lockMap.map(path.getAncestor(1), true);
            if (parent != null) {
                refresh(parent);
            }
        } catch (RepositoryException e) {
            log.warn("Unable to determine path of removed node's parent.", e);
        } finally {
            release();
        }
    }

    /**
     * Contains information about a lock and gets placed inside the child
     * information of a {@link org.apache.jackrabbit.spi.commons.name.PathMap}.
     */
    class InternalLockInfo extends LockInfo implements SessionListener {

        /**
         * Create a new instance of this class.
         *
         * @param lockToken     lock token
         * @param sessionScoped whether lock token is session scoped
         * @param deep          whether lock is deep
         * @param lockOwner     owner of lock
         * @param timeoutHint
         */
        public InternalLockInfo(NodeId lockToken, boolean sessionScoped,
                                boolean deep, String lockOwner, long timeoutHint) {
            super(lockToken, sessionScoped, deep, lockOwner, timeoutHint);
        }

        /**
         * {@inheritDoc}
         * <p>
         * When the owning session is logging out, we have to perform some
         * operations depending on the lock type.
         * (1) If the lock was session-scoped, we unlock the node.
         * (2) If the lock was open-scoped, we remove the lock token
         *     from the session and set the lockHolder field to <code>null</code>.
         */
        public void loggingOut(SessionImpl session) {
            if (isLive()) {
                if (isSessionScoped()) {
                    // if no session currently holds lock, reassign
                    SessionImpl lockHolder = getLockHolder();
                    if (lockHolder == null) {
                        setLockHolder(session);
                    }
                    try {
                        NodeImpl node = (NodeImpl) session.getItemManager().getItem(getId());
                        node.unlock();
                    } catch (RepositoryException e) {
                        // Session is not allowed/able to unlock.
                        // Use system session present with lock-mgr as fallback
                        // in order to make sure, that session-scoped locks are
                        // properly cleaned.
                        SessionImpl systemSession = LockManagerImpl.this.sysSession;
                        setLockHolder(systemSession);
                        try {
                            NodeImpl node = (NodeImpl) systemSession.getItemManager().getItem(getId());
                            node.unlock();
                        } catch (RepositoryException re) {
                            log.warn("Unable to remove session-scoped lock on node '" + getLockToken() + "': " + e.getMessage());
                            log.debug("Root cause: ", e);
                        }
                    }
                } else if (isLockHolder(session)) {
                    session.removeLockToken(getLockToken());
                    setLockHolder(null);
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        public void loggedOut(SessionImpl session) {
        }
    }

    //----------------------------------------------------< LockEventListener >

    /**
     * Set a lock event channel
     *
     * @param eventChannel lock event channel
     */
    public void setEventChannel(LockEventChannel eventChannel) {
        this.eventChannel = eventChannel;
        eventChannel.setListener(this);
    }

    /**
     * {@inheritDoc}
     */
    public void externalLock(NodeId nodeId, boolean isDeep, String lockOwner) throws RepositoryException {
        acquire();

        try {
            Path path = getPath(sysSession, nodeId);

            // create lock token
            InternalLockInfo info = new InternalLockInfo(
                    nodeId, false, isDeep, lockOwner, Long.MAX_VALUE);
            info.setLive(true);
            lockMap.put(path, info);

            save();
        } finally {
            release();
        }
    }

    /**
     * {@inheritDoc}
     */
    public void externalUnlock(NodeId nodeId) throws RepositoryException {
        acquire();

        try {
            Path path = getPath(sysSession, nodeId);
            PathMap.Element<LockInfo> element = lockMap.map(path, true);
            if (element == null) {
                throw new LockException("Node not locked: " + path.toString());
            }
            LockInfo info = element.get();
            if (info == null) {
                throw new LockException("Node not locked: " + path.toString());
            }
            element.set(null);
            info.setLive(false);

            save();

        } finally {
            release();
        }
    }

    /**
     * Dump contents of path map and elements included to a string.
     */
    public String toString() {
        final StringBuilder builder = new StringBuilder();
        lockMap.traverse(new PathMap.ElementVisitor<LockInfo>() {
            public void elementVisited(PathMap.Element<LockInfo> element) {
                for (int i = 0; i < element.getDepth(); i++) {
                    builder.append("--");
                }
                builder.append(element.getName());
                int index = element.getIndex();
                if (index != 0 && index != 1) {
                    builder.append('[');
                    builder.append(index);
                    builder.append(']');
                }
                builder.append("  ");
                builder.append(element.get());
                builder.append("\n");
            }
        }, true);
        return builder.toString();
    }
}
