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

import org.apache.jackrabbit.spi.ItemInfoCache;
import org.apache.jackrabbit.spi.RepositoryService;
import org.apache.jackrabbit.spi.IdFactory;
import org.apache.jackrabbit.spi.QValueFactory;
import org.apache.jackrabbit.spi.SessionInfo;
import org.apache.jackrabbit.spi.ItemId;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.PropertyId;
import org.apache.jackrabbit.spi.NodeInfo;
import org.apache.jackrabbit.spi.PropertyInfo;
import org.apache.jackrabbit.spi.Batch;
import org.apache.jackrabbit.spi.LockInfo;
import org.apache.jackrabbit.spi.QueryInfo;
import org.apache.jackrabbit.spi.EventFilter;
import org.apache.jackrabbit.spi.EventBundle;
import org.apache.jackrabbit.spi.QValue;
import org.apache.jackrabbit.spi.NameFactory;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.Subscription;
import org.apache.jackrabbit.spi.QNodeTypeDefinition;
import org.apache.jackrabbit.spi.Event;
import org.apache.jackrabbit.spi.ItemInfo;
import org.apache.jackrabbit.spi.ChildInfo;
import org.apache.jackrabbit.spi.commons.EventFilterImpl;
import org.apache.jackrabbit.spi.commons.EventBundleImpl;
import org.apache.jackrabbit.spi.commons.ItemInfoCacheImpl;
import org.apache.jackrabbit.spi.commons.QPropertyDefinitionImpl;
import org.apache.jackrabbit.spi.commons.QNodeDefinitionImpl;
import org.apache.jackrabbit.spi.commons.QNodeTypeDefinitionImpl;
import org.apache.jackrabbit.spi.commons.iterator.Iterators;
import org.apache.jackrabbit.spi.commons.namespace.NamespaceResolver;
import org.apache.jackrabbit.spi.commons.nodetype.NodeTypeDefinitionImpl;
import org.apache.jackrabbit.spi.commons.name.NameFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.apache.jackrabbit.spi.commons.conversion.NameException;
import org.apache.jackrabbit.spi.commons.conversion.NamePathResolver;
import org.apache.jackrabbit.spi.commons.conversion.MalformedPathException;
import org.apache.jackrabbit.spi.commons.conversion.DefaultNamePathResolver;
import org.apache.jackrabbit.spi.commons.value.QValueFactoryImpl;
import org.apache.jackrabbit.spi.commons.value.ValueFormat;
import org.apache.jackrabbit.spi.commons.value.ValueFactoryQImpl;
import org.apache.jackrabbit.JcrConstants;

import javax.jcr.RepositoryException;
import javax.jcr.Credentials;
import javax.jcr.LoginException;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.PathNotFoundException;
import javax.jcr.ValueFormatException;
import javax.jcr.AccessDeniedException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.ItemExistsException;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.MergeException;
import javax.jcr.NamespaceException;
import javax.jcr.Repository;
import javax.jcr.Session;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.NodeIterator;
import javax.jcr.NamespaceRegistry;
import javax.jcr.Workspace;
import javax.jcr.ImportUUIDBehavior;
import javax.jcr.Value;
import javax.jcr.ItemVisitor;
import javax.jcr.ValueFactory;
import javax.jcr.GuestCredentials;
import javax.jcr.PropertyIterator;
import javax.jcr.util.TraversingItemVisitor;
import javax.jcr.observation.ObservationManager;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.EventJournal;
import javax.jcr.query.InvalidQueryException;
import javax.jcr.query.QueryManager;
import javax.jcr.query.Query;
import javax.jcr.lock.LockException;
import javax.jcr.lock.Lock;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;
import javax.jcr.version.Version;
import javax.jcr.version.VersionManager;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.nodetype.NodeTypeIterator;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.InvalidNodeTypeDefinitionException;
import javax.jcr.nodetype.NodeTypeExistsException;
import javax.jcr.nodetype.NodeTypeDefinition;
import java.util.Map;
import java.util.Iterator;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.Arrays;
import java.util.Collections;
import java.util.Collection;
import java.io.InputStream;
import java.io.IOException;
import java.io.ByteArrayInputStream;
import java.security.AccessControlException;

/**
 * <code>RepositoryServiceImpl</code> implements a repository service on top
 * of a JCR Repository.
 */
public class RepositoryServiceImpl implements RepositoryService {

    /**
     * The JCR Repository.
     */
    private final Repository repository;

    /**
     * The configuration map used to determine the maximal depth of child
     * items to be accessed upon a call to {@link #getNodeInfo(SessionInfo, NodeId)}.
     */
    private final BatchReadConfig batchReadConfig;

    /**
     * The id factory.
     */
    private final IdFactoryImpl idFactory = (IdFactoryImpl) IdFactoryImpl.getInstance();

    /**
     * The QValueFactory
     */
    private QValueFactory qValueFactory = QValueFactoryImpl.getInstance();

    /**
     * Set to <code>true</code> if the underlying JCR repository supports
     * observation.
     */
    private final boolean supportsObservation;

    private final int itemInfoCacheSize;

    public RepositoryServiceImpl(Repository repository, BatchReadConfig batchReadConfig) {
        this(repository, batchReadConfig, ItemInfoCacheImpl.DEFAULT_CACHE_SIZE);
    }

    /**
     * Creates a new repository service based on the given
     * <code>repository</code>.
     *
     * @param repository a JCR repository instance.
     * @param batchReadConfig
     * {@link #getNodeInfo(SessionInfo, NodeId)}.
     */
    public RepositoryServiceImpl(Repository repository, BatchReadConfig batchReadConfig, int itemInfoCacheSize) {
        this.repository = repository;
        this.batchReadConfig = batchReadConfig;
        this.supportsObservation = "true".equals(repository.getDescriptor(Repository.OPTION_OBSERVATION_SUPPORTED));
        this.itemInfoCacheSize = itemInfoCacheSize;

        try {
            Session s = repository.login(new GuestCredentials());
            ValueFactory vf = s.getValueFactory();
            if (vf instanceof ValueFactoryQImpl) {
                qValueFactory = ((ValueFactoryQImpl) vf).getQValueFactory();
            }
        } catch (RepositoryException e) {
            // ignore
        }
    }

    /**
     * {@inheritDoc}
     */
    public IdFactory getIdFactory() {
        return idFactory;
    }

    /**
     * {@inheritDoc}
     */
    public NameFactory getNameFactory() {
        return NameFactoryImpl.getInstance();
    }

    /**
     * {@inheritDoc}
     */
    public PathFactory getPathFactory() {
        return PathFactoryImpl.getInstance();
    }

    /**
     * {@inheritDoc}
     */
    public QValueFactory getQValueFactory() {
        return qValueFactory;
    }

    /**
     * {@inheritDoc}
     */
    public ItemInfoCache getItemInfoCache(SessionInfo sessionInfo) throws RepositoryException {
        return new ItemInfoCacheImpl(itemInfoCacheSize);
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, QValue[]> getRepositoryDescriptors() throws RepositoryException {
        Map<String, QValue[]> descriptors = new HashMap<String, QValue[]>();
        for (String key : repository.getDescriptorKeys()) {
            if (key.equals(Repository.OPTION_TRANSACTIONS_SUPPORTED)) {
                descriptors.put(key, new QValue[] {qValueFactory.create(false)});
            } else {
                Value[] vs = repository.getDescriptorValues(key);
                QValue[] qvs = new QValue[vs.length];
                for (int i = 0; i < vs.length; i++) {
                    // Name and path resolver that uses a dummy namespace resolver
                    // as Name/Path values are not expected to occur in the
                    // descriptors. TODO: check again.
                    NamePathResolver resolver = new DefaultNamePathResolver(new NamespaceResolver() {
                        public String getURI(String prefix) {
                            return prefix;
                        }
                        public String getPrefix(String uri) {
                            return uri;
                        }
                    });
                    qvs[i] = ValueFormat.getQValue(vs[i], resolver, qValueFactory);
                }
                descriptors.put(key, qvs);
            }
        }
        return descriptors;
    }

    /**
     * {@inheritDoc}
     */
    public SessionInfo obtain(Credentials credentials, String workspaceName)
            throws LoginException, NoSuchWorkspaceException, RepositoryException {
        Credentials duplicate = SessionInfoImpl.duplicateCredentials(credentials);
        return new SessionInfoImpl(repository.login(credentials, workspaceName), duplicate, getNameFactory(), getPathFactory());
    }

    /**
     * {@inheritDoc}
     */
    public SessionInfo obtain(SessionInfo sessionInfo, String workspaceName)
            throws LoginException, NoSuchWorkspaceException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Session s = repository.login(sInfo.getCredentials(), workspaceName);
        return new SessionInfoImpl(s, sInfo.getCredentials(), getNameFactory(), getPathFactory());
    }

    /**
     * {@inheritDoc}
     */
    public SessionInfo impersonate(SessionInfo sessionInfo, Credentials credentials) throws LoginException, RepositoryException {
        Credentials duplicate = SessionInfoImpl.duplicateCredentials(credentials);
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return new SessionInfoImpl(sInfo.getSession().impersonate(credentials), duplicate, getNameFactory(), getPathFactory());
    }

    /**
     * {@inheritDoc}
     */
    public void dispose(SessionInfo sessionInfo) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        for (EventSubscription s : sInfo.getSubscriptions()) {
            s.dispose();
        }
        sInfo.getSession().logout();
    }

    /**
     * {@inheritDoc}
     */
    public String[] getWorkspaceNames(SessionInfo sessionInfo)
            throws RepositoryException {
        Session s = getSessionInfoImpl(sessionInfo).getSession();
        return s.getWorkspace().getAccessibleWorkspaceNames();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isGranted(SessionInfo sessionInfo,
                             ItemId itemId,
                             String[] actions) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        String path = pathForId(itemId, sInfo);

        try {
            String actStr;
            if (actions.length == 1) {
                actStr = actions[0];
            } else {
                String comma = "";
                actStr = "";
                for (String action : actions) {
                    actStr += comma;
                    actStr += action;
                    comma = ",";
                }
            }
            sInfo.getSession().checkPermission(path, actStr);
            return true;
        } catch (AccessControlException e) {
            return false;
        }
    }

    /**
     * {@inheritDoc}
     */
    public QNodeDefinition getNodeDefinition(SessionInfo sessionInfo,
                                             NodeId nodeId)
            throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        try {
            return new QNodeDefinitionImpl(getNode(nodeId, sInfo).getDefinition(),
                    sInfo.getNamePathResolver());
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public QPropertyDefinition getPropertyDefinition(SessionInfo sessionInfo,
                                                     PropertyId propertyId)
            throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        try {
            return new QPropertyDefinitionImpl(
                    getProperty(propertyId, sInfo).getDefinition(),
                    sInfo.getNamePathResolver(),
                    getQValueFactory());
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeInfo getNodeInfo(SessionInfo sessionInfo, NodeId nodeId)
            throws ItemNotFoundException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Node node = getNode(nodeId, sInfo);
        try {
            return new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver());
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<? extends ItemInfo> getItemInfos(SessionInfo sessionInfo, ItemId itemId)
            throws ItemNotFoundException, RepositoryException {

        if (!itemId.denotesNode()) {
            PropertyId propertyId = (PropertyId) itemId;
            PropertyInfo propertyInfo = getPropertyInfo(sessionInfo, propertyId);
            return Iterators.singleton(propertyInfo);
        }
        else {
            NodeId nodeId = (NodeId) itemId;
            final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
            Node node = getNode(nodeId, sInfo);
            Name ntName = null;
            try {
                ntName = sInfo.getNamePathResolver().getQName(node.getProperty(JcrConstants.JCR_PRIMARYTYPE).getString());
            } catch (NameException e) {
                // ignore. should never occur
            }
            int depth = batchReadConfig.getDepth(ntName);
            if (depth == BatchReadConfig.DEPTH_DEFAULT) {
                NodeInfo info;
                try {
                    info = new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver());
                } catch (NameException e) {
                    throw new RepositoryException(e);
                }
                return Collections.singletonList(info).iterator();
            } else {
                final List<ItemInfo> itemInfos = new ArrayList<ItemInfo>();
                ItemVisitor visitor = new TraversingItemVisitor(false, depth) {
                    @Override
                    protected void entering(Property property, int i) throws RepositoryException {
                        try {
                            itemInfos.add(new PropertyInfoImpl(property, idFactory, sInfo.getNamePathResolver(), getQValueFactory()));
                        } catch (NameException e) {
                            throw new RepositoryException(e);
                        }
                    }
                    @Override
                    protected void entering(Node node, int i) throws RepositoryException {
                        try {
                            itemInfos.add(new NodeInfoImpl(node, idFactory, sInfo.getNamePathResolver()));
                        } catch (NameException e) {
                            throw new RepositoryException(e);
                        }
                    }
                    @Override
                    protected void leaving(Property property, int i) {
                        // nothing to do
                    }
                    @Override
                    protected void leaving(Node node, int i) {
                        // nothing to do
                    }
                };
                visitor.visit(node);
                return itemInfos.iterator();
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<ChildInfo> getChildInfos(SessionInfo sessionInfo, NodeId parentId)
            throws ItemNotFoundException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NodeIterator children = getNode(parentId, sInfo).getNodes();
        List<ChildInfo> childInfos = new ArrayList<ChildInfo>();
        try {
            while (children.hasNext()) {
                childInfos.add(new ChildInfoImpl(children.nextNode(),
                        sInfo.getNamePathResolver()));
            }
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
        return childInfos.iterator();
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<PropertyId> getReferences(SessionInfo sessionInfo, NodeId nodeId, Name propertyName, boolean weakReferences) throws ItemNotFoundException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Node node = getNode(nodeId, sInfo);
        String jcrName = (propertyName == null) ? null : sInfo.getNamePathResolver().getJCRName(propertyName);

        List<PropertyId> ids = new ArrayList<PropertyId>();
        PropertyIterator it;
        if (weakReferences) {
            it = node.getWeakReferences(jcrName);
        } else {
            it = node.getReferences(jcrName);
        }
        while (it.hasNext()) {
            ids.add(idFactory.createPropertyId(it.nextProperty(), sInfo.getNamePathResolver()));
        }
        return ids.iterator();
    }

    /**
     * {@inheritDoc}
     */
    public PropertyInfo getPropertyInfo(SessionInfo sessionInfo,
                                        PropertyId propertyId)
            throws ItemNotFoundException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        try {
            return new PropertyInfoImpl(getProperty(propertyId, sInfo), idFactory,
                    sInfo.getNamePathResolver(), getQValueFactory());
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Batch createBatch(SessionInfo sessionInfo, ItemId itemId)
            throws RepositoryException {
        return new BatchImpl(getSessionInfoImpl(sessionInfo));
    }

    /**
     * {@inheritDoc}
     */
    public void submit(Batch batch) throws PathNotFoundException, ItemNotFoundException, NoSuchNodeTypeException, ValueFormatException, VersionException, LockException, ConstraintViolationException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
        if (batch instanceof BatchImpl) {
            ((BatchImpl) batch).end();
        } else {
            throw new RepositoryException("Unknown Batch implementation: " +
                    batch.getClass().getName());
        }
    }

    /**
     * {@inheritDoc}
     */
    public void importXml(final SessionInfo sessionInfo,
                          final NodeId parentId,
                          final InputStream xmlStream,
                          final int uuidBehaviour) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String path = pathForId(parentId, sInfo);
                try {
                    sInfo.getSession().getWorkspace().importXML(path, xmlStream, uuidBehaviour);
                } catch (IOException e) {
                    throw new RepositoryException(e.getMessage(), e);
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void move(final SessionInfo sessionInfo,
                     final NodeId srcNodeId,
                     final NodeId destParentNodeId,
                     final Name destName) throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String srcPath = pathForId(srcNodeId, sInfo);
                StringBuffer destPath = new StringBuffer(pathForId(destParentNodeId, sInfo));
                if (destPath.length() > 1) {
                    destPath.append("/");
                }
                destPath.append(sInfo.getNamePathResolver().getJCRName(destName));
                sInfo.getSession().getWorkspace().move(srcPath, destPath.toString());
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void copy(final SessionInfo sessionInfo,
                     final String srcWorkspaceName,
                     final NodeId srcNodeId,
                     final NodeId destParentNodeId,
                     final Name destName) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Workspace ws = sInfo.getSession().getWorkspace();
                String destPath = getDestinationPath(destParentNodeId, destName, sInfo);
                if (ws.getName().equals(srcWorkspaceName)) {
                    // inner-workspace copy
                    String srcPath = pathForId(srcNodeId, sInfo);
                    ws.copy(srcPath, destPath);
                } else {
                    SessionInfoImpl srcInfo = getSessionInfoImpl(obtain(sInfo, srcWorkspaceName));
                    try {
                        String srcPath = pathForId(srcNodeId, srcInfo);
                        ws.copy(srcWorkspaceName, srcPath, destPath);
                    } finally {
                        dispose(srcInfo);
                    }
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void update(final SessionInfo sessionInfo,
                       final NodeId nodeId,
                       final String srcWorkspaceName)
            throws NoSuchWorkspaceException, AccessDeniedException, LockException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                getNode(nodeId, sInfo).update(srcWorkspaceName);
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void clone(final SessionInfo sessionInfo,
                      final String srcWorkspaceName,
                      final NodeId srcNodeId,
                      final NodeId destParentNodeId,
                      final Name destName,
                      final boolean removeExisting) throws NoSuchWorkspaceException, ConstraintViolationException, VersionException, AccessDeniedException, PathNotFoundException, ItemExistsException, LockException, UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                SessionInfoImpl srcInfo = getSessionInfoImpl(obtain(sessionInfo, srcWorkspaceName));
                try {
                String srcPath = pathForId(srcNodeId, srcInfo);
                String destPath = getDestinationPath(destParentNodeId, destName, sInfo);

                Workspace wsp = sInfo.getSession().getWorkspace();
                wsp.clone(srcWorkspaceName, srcPath, destPath, removeExisting);
                } finally {
                    dispose(srcInfo);
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public LockInfo getLockInfo(SessionInfo sessionInfo, NodeId nodeId)
            throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        try {
            Lock lock = getNode(nodeId, sInfo).getLock();
            return LockInfoImpl.createLockInfo(lock, idFactory);
        } catch (LockException e) {
            // no lock present on this node.
            return null;
        }
    }

    /**
     * {@inheritDoc}
     */
    public LockInfo lock(final SessionInfo sessionInfo,
                         final NodeId nodeId,
                         final boolean deep,
                         final boolean sessionScoped)
            throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return (LockInfo) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Node n = getNode(nodeId, sInfo);
                Lock lock = n.lock(deep, sessionScoped);
                return LockInfoImpl.createLockInfo(lock, idFactory);
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public LockInfo lock(SessionInfo sessionInfo, final NodeId nodeId, final boolean deep, final boolean sessionScoped, final long timeoutHint, final String ownerHint) throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return (LockInfo) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Node n = getNode(nodeId, sInfo);
                Lock lock;
                javax.jcr.lock.LockManager lMgr = (sInfo.getSession().getWorkspace()).getLockManager();
                lock = lMgr.lock(n.getPath(), deep, sessionScoped, timeoutHint, ownerHint);
                return LockInfoImpl.createLockInfo(lock, idFactory);
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void refreshLock(SessionInfo sessionInfo, NodeId nodeId)
            throws LockException, RepositoryException {
        getNode(nodeId, getSessionInfoImpl(sessionInfo)).getLock().refresh();
    }

    /**
     * {@inheritDoc}
     */
    public void unlock(final SessionInfo sessionInfo, final NodeId nodeId)
            throws UnsupportedRepositoryOperationException, LockException, AccessDeniedException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                getNode(nodeId, sInfo).unlock();
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId checkin(final SessionInfo sessionInfo, final NodeId nodeId)
            throws VersionException, UnsupportedRepositoryOperationException, InvalidItemStateException, LockException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Version newVersion = (Version) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                return getNode(nodeId, getSessionInfoImpl(sessionInfo)).checkin();
            }
        }, sInfo);
        return idFactory.createNodeId(newVersion);
    }

    /**
     * {@inheritDoc}
     */
    public void checkout(final SessionInfo sessionInfo, final NodeId nodeId)
            throws UnsupportedRepositoryOperationException, LockException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                getNode(nodeId, sInfo).checkout();
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void checkout(final SessionInfo sessionInfo, final NodeId nodeId, NodeId activityId) throws UnsupportedRepositoryOperationException, LockException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Node activity = (activityId == null) ? null : getNode(activityId, sInfo);
        VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager();
        vMgr.setActivity(activity);
        try {
            executeWithLocalEvents(new Callable() {
                public Object run() throws RepositoryException {
                    getNode(nodeId, sInfo).checkout();
                    return null;
                }
            }, sInfo);
        } finally {
            vMgr.setActivity(null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public NodeId checkpoint(SessionInfo sessionInfo, final NodeId nodeId) throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Version newVersion = (Version) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager();
                return vMgr.checkpoint(getNodePath(nodeId, sInfo));
            }
        }, sInfo);
        return idFactory.createNodeId(newVersion);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId checkpoint(SessionInfo sessionInfo, final NodeId nodeId, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Node activity = (activityId == null) ? null : getNode(activityId, sInfo);
        VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager();
        vMgr.setActivity(activity);
        try {
            Version newVersion = (Version) executeWithLocalEvents(new Callable() {
                public Object run() throws RepositoryException {
                    VersionManager vMgr = sInfo.getSession().getWorkspace().getVersionManager();
                    return vMgr.checkpoint(getNodePath(nodeId, sInfo));
                }
            }, sInfo);
            return idFactory.createNodeId(newVersion);
        } finally {
            vMgr.setActivity(null);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void removeVersion(final SessionInfo sessionInfo,
                              final NodeId versionHistoryId,
                              final NodeId versionId)
            throws ReferentialIntegrityException, AccessDeniedException, UnsupportedRepositoryOperationException, VersionException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Node vHistory = getNode(versionHistoryId, sInfo);
                Node version = getNode(versionId, sInfo);
                if (vHistory instanceof VersionHistory) {
                    ((VersionHistory) vHistory).removeVersion(version.getName());
                } else {
                    throw new RepositoryException("versionHistoryId does not reference a VersionHistor node");
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void restore(final SessionInfo sessionInfo,
                        final NodeId nodeId,
                        final NodeId versionId,
                        final boolean removeExisting) throws VersionException, PathNotFoundException, ItemExistsException, UnsupportedRepositoryOperationException, LockException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Version v = (Version) getNode(versionId, sInfo);
                if (hasNode(sessionInfo, nodeId)) {
                    Node n = getNode(nodeId, sInfo);
                    n.restore(v, removeExisting);
                } else {
                    // restore with rel-Path part
                    Node n = null;
                    Path relPath = null;
                    Path path = nodeId.getPath();
                    if (nodeId.getUniqueID() != null) {
                        n = getNode(idFactory.createNodeId(nodeId.getUniqueID()), sInfo);
                        relPath = (path.isAbsolute()) ? getPathFactory().getRootPath().computeRelativePath(nodeId.getPath()) : path;
                    } else {
                        int degree = 0;
                        while (degree < path.getLength()) {
                            Path ancestorPath = path.getAncestor(degree);
                            NodeId parentId = idFactory.createNodeId(nodeId.getUniqueID(), ancestorPath);
                            if (hasNode(sessionInfo, parentId)) {
                                n = getNode(parentId, sInfo);
                                relPath = ancestorPath.computeRelativePath(path);
                            }
                            degree++;
                        }
                    }
                    if (n == null) {
                        throw new PathNotFoundException("Path not found " + nodeId);
                    } else {
                        n.restore(v, sInfo.getNamePathResolver().getJCRPath(relPath), removeExisting);
                    }
                }
                return null;
            }
        }, sInfo);
    }

    private boolean hasNode(SessionInfo sessionInfo, NodeId nodeId) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        try {
            getNode(nodeId, sInfo);
        } catch (ItemNotFoundException e) {
            return false;
        } catch (PathNotFoundException e) {
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     */
    public void restore(final SessionInfo sessionInfo,
                        final NodeId[] versionIds,
                        final boolean removeExisting) throws ItemExistsException, UnsupportedRepositoryOperationException, VersionException, LockException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Version[] versions = new Version[versionIds.length];
                for (int i = 0; i < versions.length; i++) {
                    Node n = getNode(versionIds[i], sInfo);
                    if (n instanceof Version) {
                        versions[i] = (Version) n;
                    } else {
                        throw new RepositoryException(n.getPath() +
                                " does not reference a Version node");
                    }
                }
                sInfo.getSession().getWorkspace().restore(versions, removeExisting);
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<NodeId> merge(final SessionInfo sessionInfo,
                          final NodeId nodeId,
                          final String srcWorkspaceName,
                          final boolean bestEffort)
            throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return (Iterator<NodeId>) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String nodePath = getNodePath(nodeId, sInfo);
                NodeIterator it = getVersionManager(sInfo).merge(nodePath, srcWorkspaceName, bestEffort);
                List<NodeId> ids = new ArrayList<NodeId>();
                while (it.hasNext()) {
                    ids.add(idFactory.createNodeId(it.nextNode()
                    ));
                }
                return ids.iterator();
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<NodeId> merge(final SessionInfo sessionInfo,
                          final NodeId nodeId,
                          final String srcWorkspaceName,
                          final boolean bestEffort,
                          final boolean isShallow)
            throws NoSuchWorkspaceException, AccessDeniedException, MergeException, LockException, InvalidItemStateException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return (Iterator<NodeId>) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String nodePath = getNodePath(nodeId, sInfo);
                NodeIterator it = getVersionManager(sInfo).merge(nodePath, srcWorkspaceName, bestEffort, isShallow);
                List<NodeId> ids = new ArrayList<NodeId>();
                while (it.hasNext()) {
                    ids.add(idFactory.createNodeId(it.nextNode()
                    ));
                }
                return ids.iterator();
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void resolveMergeConflict(final SessionInfo sessionInfo,
                                     final NodeId nodeId,
                                     final NodeId[] mergeFailedIds,
                                     final NodeId[] predecessorIds)
            throws VersionException, InvalidItemStateException, UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Node node = getNode(nodeId, sInfo);
                Version version = null;
                boolean cancel;
                NamePathResolver resolver = sInfo.getNamePathResolver();
                List<NodeId> l = Arrays.asList(mergeFailedIds);
                Property mergeFailed = node.getProperty(resolver.getJCRName(NameConstants.JCR_MERGEFAILED));
                for (Value value : mergeFailed.getValues()) {
                    String uuid = value.getString();
                    if (!l.contains(idFactory.createNodeId(uuid))) {
                        version = (Version) sInfo.getSession().getNodeByIdentifier(uuid);
                        break;
                    }
                }

                l = new ArrayList<NodeId>(predecessorIds.length);
                l.addAll(Arrays.asList(predecessorIds));
                Property predecessors = node.getProperty(resolver.getJCRName(NameConstants.JCR_PREDECESSORS));
                for (Value value : predecessors.getValues()) {
                    NodeId vId = idFactory.createNodeId(value.getString());
                    l.remove(vId);
                }
                cancel = l.isEmpty();
                if (cancel) {
                    node.cancelMerge(version);
                } else {
                    node.doneMerge(version);
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void addVersionLabel(final SessionInfo sessionInfo,
                                final NodeId versionHistoryId,
                                final NodeId versionId,
                                final Name label,
                                final boolean moveLabel) throws VersionException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String jcrLabel;
                jcrLabel = sInfo.getNamePathResolver().getJCRName(label);
                Node version = getNode(versionId, sInfo);
                Node vHistory = getNode(versionHistoryId, sInfo);
                if (vHistory instanceof VersionHistory) {
                    ((VersionHistory) vHistory).addVersionLabel(
                            version.getName(), jcrLabel, moveLabel);
                } else {
                    throw new RepositoryException("versionHistoryId does not reference a VersionHistory node");
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public void removeVersionLabel(final SessionInfo sessionInfo,
                                   final NodeId versionHistoryId,
                                   final NodeId versionId,
                                   final Name label) throws VersionException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                String jcrLabel;
                jcrLabel = sInfo.getNamePathResolver().getJCRName((label));
                Node vHistory = getNode(versionHistoryId, sInfo);
                if (vHistory instanceof VersionHistory) {
                    ((VersionHistory) vHistory).removeVersionLabel(jcrLabel);
                } else {
                    throw new RepositoryException("versionHistoryId does not reference a VersionHistory node");
                }
                return null;
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId createActivity(SessionInfo sessionInfo, final String title) throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        final VersionManager vMgr = getVersionManager(sInfo);
        Node activity = (Node) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                return vMgr.createActivity(title);
            }
        }, getSessionInfoImpl(sessionInfo));
        return idFactory.createNodeId(activity);
    }

    /**
     * {@inheritDoc}
     */
    public void removeActivity(SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        final VersionManager vMgr = getVersionManager(sInfo);
        executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                vMgr.removeActivity(getNode(activityId, sInfo));
                return null;
            }
        }, getSessionInfoImpl(sessionInfo));
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<NodeId> mergeActivity(SessionInfo sessionInfo, final NodeId activityId) throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return (Iterator<NodeId>) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                Node node = getNode(activityId, sInfo);
                NodeIterator it = getVersionManager(sInfo).merge(node);
                List<NodeId> ids = new ArrayList<NodeId>();
                while (it.hasNext()) {
                    ids.add(idFactory.createNodeId(it.nextNode()
                    ));
                }
                return ids.iterator();
            }
        }, sInfo);
    }

    /**
     * {@inheritDoc}
     */
    public NodeId createConfiguration(SessionInfo sessionInfo, final NodeId nodeId)
            throws UnsupportedRepositoryOperationException, RepositoryException {
        final SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        final VersionManager vMgr = getVersionManager(sInfo);
        Node configuration = (Node) executeWithLocalEvents(new Callable() {
            public Object run() throws RepositoryException {
                return vMgr.createConfiguration(getNodePath(nodeId, sInfo));
            }
        }, getSessionInfoImpl(sessionInfo));
        return idFactory.createNodeId(configuration);
    }

    /**
     * {@inheritDoc}
     */
    public String[] getSupportedQueryLanguages(SessionInfo sessionInfo)
            throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return sInfo.getSession().getWorkspace().getQueryManager().getSupportedQueryLanguages();
    }

    /**
     * {@inheritDoc}
     */
    public String[] checkQueryStatement(SessionInfo sessionInfo,
                                    String statement,
                                    String language,
                                    Map<String, String> namespaces)
            throws InvalidQueryException, RepositoryException {
        Query q = createQuery(getSessionInfoImpl(sessionInfo).getSession(),
                statement, language, namespaces);
        return q.getBindVariableNames();
    }

    /**
     * {@inheritDoc}
     */
    public QueryInfo executeQuery(SessionInfo sessionInfo, String statement, String language, Map<String, String> namespaces, long limit, long offset, Map<String, QValue> values) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Query query = createQuery(sInfo.getSession(), statement,
                language, namespaces);
        if (limit != -1) {
            query.setLimit(limit);
        }
        if (offset != -1) {
            query.setOffset(offset);
        }
        if (values != null && !values.isEmpty()) {
            for (Map.Entry<String, QValue> entry : values.entrySet()) {
                Value value = ValueFormat.getJCRValue(entry.getValue(), sInfo.getNamePathResolver(), sInfo.getSession().getValueFactory());
                query.bindValue(entry.getKey(), value);
            }
        }
        return new QueryInfoImpl(query.execute(), idFactory,
                sInfo.getNamePathResolver(), getQValueFactory());
    }

    /**
     * {@inheritDoc}
     */
    public EventFilter createEventFilter(SessionInfo sessionInfo,
                                         int eventTypes,
                                         Path absPath,
                                         boolean isDeep,
                                         String[] uuid,
                                         Name[] nodeTypeName,
                                         boolean noLocal)
            throws UnsupportedRepositoryOperationException, RepositoryException {
        Set<Name> ntNames = null;
        if (nodeTypeName != null) {
            ntNames = new HashSet<Name>(Arrays.asList(nodeTypeName));
        }
        return new EventFilterImpl(eventTypes, absPath, isDeep, uuid, ntNames, noLocal);
    }

    /**
     * {@inheritDoc}
     */
    public Subscription createSubscription(SessionInfo sessionInfo,
                                           EventFilter[] filters)
            throws UnsupportedRepositoryOperationException, RepositoryException {
        return getSessionInfoImpl(sessionInfo).createSubscription(idFactory, qValueFactory, filters);
    }

    /**
     * {@inheritDoc}
     */
    public EventBundle[] getEvents(Subscription subscription, long timeout)
            throws RepositoryException, UnsupportedRepositoryOperationException, InterruptedException {
        if (subscription instanceof EventSubscription) {
            return ((EventSubscription) subscription).getEventBundles(timeout);
        } else {
            throw new RepositoryException("Unknown subscription implementation: "
                    + subscription.getClass().getName());
        }
    }

    /**
     * {@inheritDoc}
     */
    public EventBundle getEvents(SessionInfo sessionInfo,
                                   EventFilter filter,
                                   long after)
            throws RepositoryException, UnsupportedRepositoryOperationException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        EventJournal journal = sInfo.getSession().getWorkspace().getObservationManager().getEventJournal();
        if (journal == null) {
            throw new UnsupportedRepositoryOperationException();
        }
        EventFactory factory = new EventFactory(sInfo.getSession(),
                sInfo.getNamePathResolver(), idFactory, qValueFactory);
        journal.skipTo(after);
        List<Event> events = new ArrayList<Event>();
        int batchSize = 1024;
        boolean distinctDates = true;
        long lastDate = Long.MIN_VALUE;
        while (journal.hasNext() && (batchSize > 0 || !distinctDates)) {
            Event e = factory.fromJCREvent(journal.nextEvent());
            if (filter.accept(e, false)) {
                distinctDates = lastDate != e.getDate();
                lastDate = e.getDate();
                events.add(e);
                batchSize--;
            }
        }
        return new EventBundleImpl(events, false);
    }

    /**
     * {@inheritDoc}
     */
    public void updateEventFilters(Subscription subscription,
                                   EventFilter[] filters)
            throws RepositoryException {
        getEventSubscription(subscription).setFilters(filters);
    }

    /**
     * {@inheritDoc}
     */
    public void dispose(Subscription subscription) throws RepositoryException {
        getEventSubscription(subscription).dispose();
    }

    /**
     * {@inheritDoc}
     */
    public Map<String, String> getRegisteredNamespaces(SessionInfo sessionInfo)
            throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NamespaceRegistry nsReg = sInfo.getSession().getWorkspace().getNamespaceRegistry();
        Map<String, String> namespaces = new HashMap<String, String>();
        for (String prefix : nsReg.getPrefixes()) {
            namespaces.put(prefix, nsReg.getURI(prefix));
        }
        return namespaces;
    }

    /**
     * {@inheritDoc}
     */
    public String getNamespaceURI(SessionInfo sessionInfo, String prefix)
            throws NamespaceException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return sInfo.getSession().getWorkspace().getNamespaceRegistry().getURI(prefix);
    }

    /**
     * {@inheritDoc}
     */
    public String getNamespacePrefix(SessionInfo sessionInfo, String uri)
            throws NamespaceException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        return sInfo.getSession().getWorkspace().getNamespaceRegistry().getPrefix(uri);
    }

    /**
     * {@inheritDoc}
     */
    public void registerNamespace(SessionInfo sessionInfo,
                                  String prefix,
                                  String uri) throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException {
        Session session = getSessionInfoImpl(sessionInfo).getSession();
        NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry();
        nsReg.registerNamespace(prefix, uri);
    }

    /**
     * {@inheritDoc}
     */
    public void unregisterNamespace(SessionInfo sessionInfo, String uri)
            throws NamespaceException, UnsupportedRepositoryOperationException, AccessDeniedException, RepositoryException {
        Session session = getSessionInfoImpl(sessionInfo).getSession();
        NamespaceRegistry nsReg = session.getWorkspace().getNamespaceRegistry();
        nsReg.unregisterNamespace(nsReg.getPrefix(uri));
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<QNodeTypeDefinition> getQNodeTypeDefinitions(SessionInfo sessionInfo) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager();
        List<QNodeTypeDefinition> nodeTypes = new ArrayList<QNodeTypeDefinition>();
        try {
            for (NodeTypeIterator it = ntMgr.getAllNodeTypes(); it.hasNext(); ) {
                NodeType nt = it.nextNodeType();
                nodeTypes.add(new QNodeTypeDefinitionImpl(nt,
                        sInfo.getNamePathResolver(), getQValueFactory()));
            }
        } catch (NameException e) {
            throw new RepositoryException(e);
        }
        return nodeTypes.iterator();
    }

    /**
     * {@inheritDoc}
     */
    public Iterator<QNodeTypeDefinition> getQNodeTypeDefinitions(SessionInfo sessionInfo, Name[] nodetypeNames) throws RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager();
        List<QNodeTypeDefinition> defs = new ArrayList<QNodeTypeDefinition>();
        for (Name nodetypeName : nodetypeNames) {
            try {
                String ntName = sInfo.getNamePathResolver().getJCRName(nodetypeName);
                NodeType nt = ntMgr.getNodeType(ntName);
                defs.add(new QNodeTypeDefinitionImpl(nt,
                        sInfo.getNamePathResolver(), getQValueFactory()));

                // in addition pack all supertypes into the return value
                NodeType[] supertypes = nt.getSupertypes();
                for (NodeType supertype : supertypes) {
                    defs.add(new QNodeTypeDefinitionImpl(supertype,
                            sInfo.getNamePathResolver(), getQValueFactory()));
                }
            } catch (NameException e) {
                throw new RepositoryException(e);
            }
        }
        return defs.iterator();
    }

    /**
     * {@inheritDoc}
     */
    public void registerNodeTypes(SessionInfo sessionInfo, QNodeTypeDefinition[] nodeTypeDefinitions, boolean allowUpdate) throws InvalidNodeTypeDefinitionException, NodeTypeExistsException, UnsupportedRepositoryOperationException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager();

        NodeTypeDefinition[] defs = new NodeTypeDefinition[nodeTypeDefinitions.length];
        for (int i = 0; i < nodeTypeDefinitions.length; i++) {
            defs[i] = new NodeTypeDefinitionImpl(nodeTypeDefinitions[i], sInfo.getNamePathResolver(), sInfo.getSession().getValueFactory()) {
            };
        }
        ntMgr.registerNodeTypes(defs, allowUpdate);
    }

    /**
     * {@inheritDoc}
     */
    public void unregisterNodeTypes(SessionInfo sessionInfo, Name[] nodeTypeNames) throws UnsupportedRepositoryOperationException, NoSuchNodeTypeException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        NodeTypeManager ntMgr = sInfo.getSession().getWorkspace().getNodeTypeManager();
        String[] names = new String[nodeTypeNames.length];
        for (int i = 0; i < nodeTypeNames.length; i++) {
            names[i] = sInfo.getNamePathResolver().getJCRName(nodeTypeNames[i]);
        }
        ntMgr.unregisterNodeTypes(names);
    }

    /**
     * {@inheritDoc}
     */
    public void createWorkspace(SessionInfo sessionInfo, String name, String srcWorkspaceName) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Workspace wsp = sInfo.getSession().getWorkspace();
        if (srcWorkspaceName == null) {
            wsp.createWorkspace(name);
        } else {
            wsp.createWorkspace(name, srcWorkspaceName);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteWorkspace(SessionInfo sessionInfo, String name) throws AccessDeniedException, UnsupportedRepositoryOperationException, NoSuchWorkspaceException, RepositoryException {
        SessionInfoImpl sInfo = getSessionInfoImpl(sessionInfo);
        Workspace wsp = sInfo.getSession().getWorkspace();
        wsp.deleteWorkspace(name);
    }

    //----------------------------< internal >----------------------------------

    private final class BatchImpl implements Batch {

        private final SessionInfoImpl sInfo;
        /* If this batch needs to remove multiple same-name-siblings starting
           from lower index, the index of the following siblings must be reset
           in order to avoid PathNotFoundException.
         */
        private final Set<NodeId> removedNodeIds = new HashSet<NodeId>();

        private boolean failed = false;

        BatchImpl(SessionInfoImpl sInfo) {
            this.sInfo = sInfo;
        }

        public void addNode(final NodeId parentId,
                            final Name nodeName,
                            final Name nodetypeName,
                            final String uuid) throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Session s = sInfo.getSession();
                    Node parent = getParent(parentId, sInfo);

                    String jcrName = getJcrName(nodeName);
                    String ntName = getJcrName(nodetypeName);
                    if (uuid == null) {
                        if (ntName == null) {
                            parent.addNode(jcrName);
                        } else {
                            parent.addNode(jcrName, ntName);
                        }
                    } else {
                        String xml = createXMLFragment(jcrName, ntName, uuid);
                        InputStream in = new ByteArrayInputStream(xml.getBytes());
                        try {
                            s.importXML(parent.getPath(), in, ImportUUIDBehavior.IMPORT_UUID_COLLISION_THROW);
                        } catch (IOException e) {
                            throw new RepositoryException(e.getMessage(), e);
                        }
                    }
                    return null;
                }
            });
        }

        public void addProperty(final NodeId parentId,
                                final Name propertyName,
                                final QValue value)
                throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Session s = sInfo.getSession();
                    Node parent = getParent(parentId, sInfo);
                    Value jcrValue = ValueFormat.getJCRValue(value,
                            sInfo.getNamePathResolver(), s.getValueFactory());
                    parent.setProperty(getJcrName(propertyName), jcrValue);
                    return null;
                }
            });
        }

        public void addProperty(final NodeId parentId,
                                final Name propertyName,
                                final QValue[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, PathNotFoundException, ItemExistsException, AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Session s = sInfo.getSession();
                    Node n = getParent(parentId, sInfo);
                    Value[] jcrValues = new Value[values.length];
                    for (int i = 0; i < jcrValues.length; i++) {
                        jcrValues[i] = ValueFormat.getJCRValue(values[i],
                                sInfo.getNamePathResolver(), s.getValueFactory());
                    }
                    n.setProperty(getJcrName(propertyName), jcrValues);
                    return null;
                }
            });
        }

        public void setValue(final PropertyId propertyId, final QValue value)
                throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Session s = sInfo.getSession();
                    Value jcrValue = ValueFormat.getJCRValue(value,
                            sInfo.getNamePathResolver(), s.getValueFactory());
                    getProperty(propertyId, sInfo).setValue(jcrValue);
                    return null;
                }
            });
        }

        public void setValue(final PropertyId propertyId, final QValue[] values)
                throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Session s = sInfo.getSession();
                    Value[] jcrValues = new Value[values.length];
                    for (int i = 0; i < jcrValues.length; i++) {
                        jcrValues[i] = ValueFormat.getJCRValue(values[i],
                                sInfo.getNamePathResolver(), s.getValueFactory());
                    }
                    getProperty(propertyId, sInfo).setValue(jcrValues);
                    return null;
                }
            });
        }

        public void remove(final ItemId itemId) throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    try {
                        if (itemId.denotesNode()) {
                            NodeId nodeId = calcRemoveNodeId(itemId);
                            getNode(nodeId, sInfo).remove();
                        } else {
                            getProperty((PropertyId) itemId, sInfo).remove();
                        }
                    } catch (ItemNotFoundException e) {
                        // item was present in jcr2spi but got removed on the
                        // persistent layer in the mean time.
                        throw new InvalidItemStateException(e);
                    } catch (PathNotFoundException e) {
                        // item was present in jcr2spi but got removed on the
                        // persistent layer in the mean time.
                        throw new InvalidItemStateException(e);
                    }
                    return null;
                }
            });
        }

        private NodeId calcRemoveNodeId(ItemId itemId) throws MalformedPathException {
            NodeId nodeId = (NodeId) itemId;
            Path p = itemId.getPath();
            if (p != null) {
                removedNodeIds.add(nodeId);
                int index = p.getNormalizedIndex();
                if (index > Path.INDEX_DEFAULT) {
                    Path.Element[] elems = p.getElements();
                    PathBuilder pb = new PathBuilder();
                    for (int i = 0; i <= elems.length - 2; i++) {
                        pb.addLast(elems[i]);
                    }
                    pb.addLast(p.getName(), index - 1);

                    NodeId prevSibling = idFactory.createNodeId(itemId.getUniqueID(), pb.getPath());
                    if (removedNodeIds.contains(prevSibling)) {
                        nodeId = prevSibling;
                    }
                }
            }
            return nodeId;
        }

        public void reorderNodes(final NodeId parentId,
                                 final NodeId srcNodeId,
                                 final NodeId beforeNodeId)
                throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Node parent = getParent(parentId, sInfo);
                    Node srcNode = getNode(srcNodeId, sInfo);
                    Node beforeNode = null;
                    if (beforeNodeId != null) {
                        beforeNode = getNode(beforeNodeId, sInfo);
                    }
                    String srcPath = srcNode.getName();
                    if (srcNode.getIndex() > 1) {
                        srcPath += "[" + srcNode.getIndex() + "]";
                    }
                    String beforePath = null;
                    if (beforeNode != null) {
                        beforePath = beforeNode.getName();
                        if (beforeNode.getIndex() > 1) {
                            beforePath += "[" + beforeNode.getIndex() + "]";
                        }
                    }
                    parent.orderBefore(srcPath, beforePath);
                    return null;
                }
            });
        }

        public void setMixins(final NodeId nodeId,
                              final Name[] mixinNodeTypeIds)
                throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Set<String> mixinNames = new HashSet<String>();
                    for (Name mixinNodeTypeId : mixinNodeTypeIds) {
                        mixinNames.add(getJcrName(mixinNodeTypeId));
                    }
                    Node n = getNode(nodeId, sInfo);
                    Set<String> currentMixins = new HashSet<String>();
                    for (NodeType nt : n.getMixinNodeTypes()) {
                        currentMixins.add(nt.getName());
                    }
                    Set<String> remove = new HashSet<String>(currentMixins);
                    remove.removeAll(mixinNames);
                    mixinNames.removeAll(currentMixins);
                    for (String mixName : remove) {
                        n.removeMixin(mixName);
                    }
                    for (String mixName : mixinNames) {
                        n.addMixin(mixName);
                    }
                    return null;
                }
            });
        }

        public void setPrimaryType(final NodeId nodeId, final Name primaryNodeTypeName) throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    Node n = getNode(nodeId, sInfo);
                    n.setPrimaryType(getJcrName(primaryNodeTypeName));
                    return null;
                }
            });
        }

        public void move(final NodeId srcNodeId,
                         final NodeId destParentNodeId,
                         final Name destName) throws RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    String srcPath = pathForId(srcNodeId, sInfo);
                    String destPath = pathForId(destParentNodeId, sInfo);
                    if (destPath.length() > 1) {
                        destPath += "/";
                    }
                    destPath += getJcrName(destName);
                    sInfo.getSession().move(srcPath, destPath);
                    return null;
                }
            });
        }

        private void executeGuarded(Callable call) throws RepositoryException {
            if (failed) {
                return;
            }
            try {
                call.run();
            } catch (RepositoryException e) {
                failed = true;
                sInfo.getSession().refresh(false);
                throw e;
            } catch (RuntimeException e) {
                failed = true;
                sInfo.getSession().refresh(false);
                throw e;
            }
        }

        private String getJcrName(Name name) throws RepositoryException {
            if (name == null) {
                return null;
            }
            return sInfo.getNamePathResolver().getJCRName((name));
        }

        private String createXMLFragment(String nodeName, String ntName, String uuid) {
            StringBuffer xml = new StringBuffer("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
            xml.append("<sv:node xmlns:jcr=\"http://www.jcp.org/jcr/1.0\" ");
            xml.append("xmlns:sv=\"http://www.jcp.org/jcr/sv/1.0\" ");
            xml.append("sv:name=\"").append(nodeName).append("\">");
            // jcr:primaryType
            xml.append("<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\">");
            xml.append("<sv:value>").append(ntName).append("</sv:value>");
            xml.append("</sv:property>");
            // jcr:uuid
            xml.append("<sv:property sv:name=\"jcr:uuid\" sv:type=\"String\">");
            xml.append("<sv:value>").append(uuid).append("</sv:value>");
            xml.append("</sv:property>");
            xml.append("</sv:node>");
            return xml.toString();
        }

        private void end() throws AccessDeniedException, ItemExistsException,
                ConstraintViolationException, InvalidItemStateException,
                VersionException, LockException, NoSuchNodeTypeException,
                RepositoryException {
            executeGuarded(new Callable() {
                public Object run() throws RepositoryException {
                    executeWithLocalEvents(new Callable() {
                        public Object run() throws RepositoryException {
                            sInfo.getSession().save();
                            return null;
                        }
                    }, sInfo);
                    return null;
                }
            });
        }
    }

    private interface Callable {
        public Object run() throws RepositoryException;
    }

    private SessionInfoImpl getSessionInfoImpl(SessionInfo sessionInfo)
            throws RepositoryException {
        if (sessionInfo instanceof SessionInfoImpl) {
            return (SessionInfoImpl) sessionInfo;
        } else {
            throw new RepositoryException("Unknown SessionInfo implementation: "
                    + sessionInfo.getClass().getName());
        }
    }

    private EventSubscription getEventSubscription(Subscription subscription)
            throws RepositoryException {
        if (subscription instanceof EventSubscription) {
            return (EventSubscription) subscription;
        } else {
            throw new RepositoryException("Unknown Subscription implementation: "
                    + subscription.getClass().getName());
        }
    }

    private String getDestinationPath(NodeId destParentNodeId, Name destName, SessionInfoImpl sessionInfo) throws RepositoryException {
        StringBuffer destPath = new StringBuffer(pathForId(destParentNodeId, sessionInfo));
        if (destPath.length() > 1) {
            destPath.append("/");
        }
        destPath.append(sessionInfo.getNamePathResolver().getJCRName(destName));
        return destPath.toString();
    }

    private String pathForId(ItemId id, SessionInfoImpl sessionInfo)
            throws RepositoryException {
        Session session = sessionInfo.getSession();
        StringBuffer path = new StringBuffer();
        if (id.getUniqueID() != null) {
            path.append(session.getNodeByIdentifier(id.getUniqueID()).getPath());
        } else {
            path.append("/");
        }

        if (id.getPath() == null) {
            // we're done
            return path.toString();
        }

        if (id.getPath().isAbsolute()) {
            if (path.length() == 1) {
                // root path ends with slash
                path.setLength(0);
            }
        } else {
            // path is relative
            if (path.length() > 1) {
                path.append("/");
            }
        }
        path.append(sessionInfo.getNamePathResolver().getJCRPath(id.getPath()));
        return path.toString();
    }

    private Node getParent(NodeId parentId, SessionInfoImpl sessionInfo) throws InvalidItemStateException, RepositoryException {
        try {
            return getNode(parentId, sessionInfo);
        } catch (PathNotFoundException e) {
            // if the parent of an batch operation is not available, this indicates
            // that it has been destroyed by another session.
            throw new InvalidItemStateException(e);
        } catch (ItemNotFoundException e) {
            // if the parent of an batch operation is not available, this indicates
            // that it has been destroyed by another session.
            throw new InvalidItemStateException(e);
        }
    }

    private Node getNode(NodeId id, SessionInfoImpl sessionInfo) throws ItemNotFoundException, PathNotFoundException, RepositoryException {
        Session session = sessionInfo.getSession();
        Node n;
        if (id.getUniqueID() != null) {
            n = session.getNodeByIdentifier(id.getUniqueID());
        } else {
            n = session.getRootNode();
        }
        Path path = id.getPath();
        if (path == null || path.denotesRoot()) {
            return n;
        }
        String jcrPath;
        jcrPath = sessionInfo.getNamePathResolver().getJCRPath(path);
        if (path.isAbsolute()) {
            jcrPath = jcrPath.substring(1, jcrPath.length());
        }
        return n.getNode(jcrPath);
    }

    private String getNodePath(NodeId nodeId, SessionInfoImpl sessionInfo) throws RepositoryException {
        // TODO: improve. avoid round trip over node access.
        return getNode(nodeId, sessionInfo).getPath();
    }

    private Property getProperty(PropertyId id, SessionInfoImpl sessionInfo) throws ItemNotFoundException, PathNotFoundException, RepositoryException {
        Session session = sessionInfo.getSession();
        Node n;
        if (id.getUniqueID() != null) {
            n = session.getNodeByIdentifier(id.getUniqueID());
        } else {
            n = session.getRootNode();
        }
        Path path = id.getPath();
        String jcrPath = sessionInfo.getNamePathResolver().getJCRPath(path);
        if (path.isAbsolute()) {
            jcrPath = jcrPath.substring(1, jcrPath.length());
        }
        return n.getProperty(jcrPath);
    }

    private VersionManager getVersionManager(SessionInfoImpl sessionInfo) throws RepositoryException {
        return sessionInfo.getSession().getWorkspace().getVersionManager();
    }

    private Query createQuery(Session session,
                              String statement,
                              String language,
                              Map<String, String> namespaces)
            throws InvalidQueryException, RepositoryException {
        QueryManager qMgr = session.getWorkspace().getQueryManager();

        // apply namespace mappings to session
        Map<String, String> previous = setNamespaceMappings(session, namespaces);
        try {
            return qMgr.createQuery(statement, language);
        } finally {
            // reset namespace mappings
            setNamespaceMappings(session, previous);
        }
    }

    /**
     * Utility method that changes the namespace mappings of the
     * given sessions to include the given prefix to URI mappings.
     *
     * @param session current session
     * @param namespaces prefix to URI mappings
     * @return the previous namespace mappings that were modified
     * @throws RepositoryException if a repository error occurs
     */
    private Map<String, String> setNamespaceMappings(Session session, Map<String, String> namespaces)
            throws RepositoryException {
        Map<String, String> previous = new HashMap<String, String>();

        for (Map.Entry<String, String> entry : namespaces.entrySet()) {
            String uri = entry.getValue();
            String prefix = entry.getKey();

            // Get the previous prefix for this URI, throws if
            // URI not found (which is OK, as that's an error)
            String oldPrefix = session.getNamespacePrefix(uri);
            // If the prefixes are different, we need to remap the namespace
            if (!prefix.equals(oldPrefix)) {
                // Check if the new prefix is mapped to some other URI
                String oldURI = safeGetURI(session, prefix);
                if (oldURI != null) {
                    // Find an unused prefix and map the old URI to it
                    int i = 2;
                    String tmpPrefix = oldPrefix + i++;
                    while (safeGetURI(session, tmpPrefix) != null
                            || namespaces.containsKey(tmpPrefix)) {
                        tmpPrefix = oldPrefix + i++;
                    }
                    session.setNamespacePrefix(tmpPrefix, oldURI);
                    previous.put(prefix, oldURI); // remember the old URI
                }
                // It's now safe to remap
                session.setNamespacePrefix(prefix, uri);
                previous.put(oldPrefix, uri); // remember the old prefix
            }
        }

        return previous;
    }

    /**
     * Utility method that returns the namespace URI mapped to the given
     * prefix, or <code>null</code> if the prefix is not mapped.
     *
     * @param session current session
     * @param prefix namespace prefix
     * @return namespace URI or <code>null</code>
     * @throws RepositoryException if a repository error occurs
     */
    private String safeGetURI(Session session, String prefix)
            throws RepositoryException {
        try {
            return session.getNamespaceURI(prefix);
        } catch (NamespaceException e) {
            return null;
        }
    }

    private Object executeWithLocalEvents(Callable call, SessionInfoImpl sInfo)
            throws RepositoryException {
        if (supportsObservation) {
            // register local event listener
            Collection<EventSubscription> subscr = sInfo.getSubscriptions();
            if (subscr.size() != 0) {
                ObservationManager obsMgr = sInfo.getSession().getWorkspace().getObservationManager();
                List<EventListener> listeners = new ArrayList<EventListener>(subscr.size());
                try {
                    for (EventSubscription s : subscr) {
                        EventListener listener = s.getLocalEventListener();
                        listeners.add(listener);
                        obsMgr.addEventListener(listener, EventSubscription.ALL_EVENTS,
                                "/", true, null, null, false);
                    }
                    return call.run();
                } finally {
                    for (EventListener listener : listeners) {
                        try {
                            obsMgr.removeEventListener(listener);
                        } catch (RepositoryException e) {
                            // ignore and remove next
                        }
                    }
                }
            }
        }
        // if we get here simply run as is
        return call.run();
    }
}
