org.eclipse.ecr.core.api.AbstractSession.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ecr.core.api.AbstractSession.java

Source

/*
 * Copyright (c) 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Bogdan Stefanescu
 *     Florent Guillaume
 */

package org.eclipse.ecr.core.api;

import static org.eclipse.ecr.core.api.security.SecurityConstants.ADD_CHILDREN;
import static org.eclipse.ecr.core.api.security.SecurityConstants.BROWSE;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ_CHILDREN;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ_LIFE_CYCLE;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ_PROPERTIES;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ_SECURITY;
import static org.eclipse.ecr.core.api.security.SecurityConstants.READ_VERSION;
import static org.eclipse.ecr.core.api.security.SecurityConstants.REMOVE;
import static org.eclipse.ecr.core.api.security.SecurityConstants.REMOVE_CHILDREN;
import static org.eclipse.ecr.core.api.security.SecurityConstants.SYSTEM_USERNAME;
import static org.eclipse.ecr.core.api.security.SecurityConstants.UNLOCK;
import static org.eclipse.ecr.core.api.security.SecurityConstants.WRITE;
import static org.eclipse.ecr.core.api.security.SecurityConstants.WRITE_LIFE_CYCLE;
import static org.eclipse.ecr.core.api.security.SecurityConstants.WRITE_PROPERTIES;
import static org.eclipse.ecr.core.api.security.SecurityConstants.WRITE_SECURITY;
import static org.eclipse.ecr.core.api.security.SecurityConstants.WRITE_VERSION;

import java.io.InputStream;
import java.io.Serializable;
import java.security.Principal;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.ecr.core.CoreService;
import org.eclipse.ecr.core.NXCore;
import org.eclipse.ecr.core.api.AlreadyConnectedException;
import org.eclipse.ecr.core.api.ClientException;
import org.eclipse.ecr.core.api.CoreInstance;
import org.eclipse.ecr.core.api.CoreSession;
import org.eclipse.ecr.core.api.DataModel;
import org.eclipse.ecr.core.api.DetachedAdapter;
import org.eclipse.ecr.core.api.DocumentException;
import org.eclipse.ecr.core.api.DocumentModel;
import org.eclipse.ecr.core.api.DocumentModelIterator;
import org.eclipse.ecr.core.api.DocumentModelList;
import org.eclipse.ecr.core.api.DocumentModelsChunk;
import org.eclipse.ecr.core.api.DocumentRef;
import org.eclipse.ecr.core.api.DocumentSecurityException;
import org.eclipse.ecr.core.api.Filter;
import org.eclipse.ecr.core.api.IdRef;
import org.eclipse.ecr.core.api.IterableQueryResult;
import org.eclipse.ecr.core.api.Lock;
import org.eclipse.ecr.core.api.NuxeoPrincipal;
import org.eclipse.ecr.core.api.PathRef;
import org.eclipse.ecr.core.api.SerializableInputStream;
import org.eclipse.ecr.core.api.SimplePrincipal;
import org.eclipse.ecr.core.api.Sorter;
import org.eclipse.ecr.core.api.VersionModel;
import org.eclipse.ecr.core.api.VersioningChangeNotifier;
import org.eclipse.ecr.core.api.VersioningOption;
import org.eclipse.ecr.core.api.DocumentModel.DocumentModelRefresh;
import org.eclipse.ecr.core.api.event.CoreEventConstants;
import org.eclipse.ecr.core.api.event.DocumentEventCategories;
import org.eclipse.ecr.core.api.event.DocumentEventTypes;
import org.eclipse.ecr.core.api.facet.VersioningDocument;
import org.eclipse.ecr.core.api.impl.DocsQueryProviderDef;
import org.eclipse.ecr.core.api.impl.DocumentModelImpl;
import org.eclipse.ecr.core.api.impl.DocumentModelIteratorImpl;
import org.eclipse.ecr.core.api.impl.DocumentModelListImpl;
import org.eclipse.ecr.core.api.impl.FacetFilter;
import org.eclipse.ecr.core.api.impl.UserPrincipal;
import org.eclipse.ecr.core.api.impl.VersionModelImpl;
import org.eclipse.ecr.core.api.model.DocumentPart;
import org.eclipse.ecr.core.api.operation.Operation;
import org.eclipse.ecr.core.api.operation.OperationHandler;
import org.eclipse.ecr.core.api.operation.ProgressMonitor;
import org.eclipse.ecr.core.api.operation.Status;
import org.eclipse.ecr.core.api.security.ACP;
import org.eclipse.ecr.core.api.security.SecurityConstants;
import org.eclipse.ecr.core.api.security.SecuritySummaryEntry;
import org.eclipse.ecr.core.api.security.UserEntry;
import org.eclipse.ecr.core.api.security.impl.ACPImpl;
import org.eclipse.ecr.core.api.security.impl.UserEntryImpl;
import org.eclipse.ecr.core.event.Event;
import org.eclipse.ecr.core.event.EventService;
import org.eclipse.ecr.core.event.impl.DocumentEventContext;
import org.eclipse.ecr.core.event.impl.EventContextImpl;
import org.eclipse.ecr.core.lifecycle.LifeCycleConstants;
import org.eclipse.ecr.core.lifecycle.LifeCycleException;
import org.eclipse.ecr.core.lifecycle.LifeCycleService;
import org.eclipse.ecr.core.model.Document;
import org.eclipse.ecr.core.model.DocumentIterator;
import org.eclipse.ecr.core.model.DocumentProxy;
import org.eclipse.ecr.core.model.NoSuchDocumentException;
import org.eclipse.ecr.core.model.PathComparator;
import org.eclipse.ecr.core.model.Session;
import org.eclipse.ecr.core.query.FilterableQuery;
import org.eclipse.ecr.core.query.Query;
import org.eclipse.ecr.core.query.QueryFilter;
import org.eclipse.ecr.core.query.QueryParseException;
import org.eclipse.ecr.core.query.QueryResult;
import org.eclipse.ecr.core.query.sql.model.SQLQuery.Transformer;
import org.eclipse.ecr.core.repository.RepositoryInitializationHandler;
import org.eclipse.ecr.core.schema.DocumentType;
import org.eclipse.ecr.core.schema.NXSchema;
import org.eclipse.ecr.core.schema.SchemaManager;
import org.eclipse.ecr.core.schema.types.CompositeType;
import org.eclipse.ecr.core.schema.types.Schema;
import org.eclipse.ecr.core.security.SecurityService;
import org.eclipse.ecr.core.utils.SIDGenerator;
import org.eclipse.ecr.core.versioning.VersioningService;
import org.eclipse.ecr.runtime.api.Framework;
import org.eclipse.ecr.runtime.services.streaming.InputStreamSource;
import org.eclipse.ecr.runtime.services.streaming.StreamManager;
import org.nuxeo.common.collections.ScopeType;
import org.nuxeo.common.collections.ScopedMap;
import org.nuxeo.common.utils.IdUtils;

/**
 * Abstract implementation of the client interface.
 * <p>
 * This handles all the aspects that are independent on the final implementation
 * (like running inside a J2EE platform or not).
 * <p>
 * The only aspect not implemented is the session management that should be
 * handled by subclasses.
 *
 * @author Bogdan Stefanescu
 * @author Florent Guillaume
 */
public abstract class AbstractSession implements CoreSession, OperationHandler, Serializable {

    public static final NuxeoPrincipal ANONYMOUS = new UserPrincipal("anonymous", new ArrayList<String>(), true,
            false);

    private static final Log log = LogFactory.getLog(CoreSession.class);

    private static final long serialVersionUID = 6585443198474361876L;

    private static final Comparator<? super Document> pathComparator = new PathComparator();

    // the repository name
    protected String repositoryName;

    protected Map<String, Serializable> sessionContext;

    /**
     * Private access to protected it again direct access since this field is
     * lazy loaded. You must use {@link #getEventService()} to get the service
     */
    private transient EventService eventService;

    /**
     * Used to check permissions.
     */
    private transient SecurityService securityService;

    protected SecurityService getSecurityService() {
        if (securityService == null) {
            securityService = NXCore.getSecurityService();
        }
        return securityService;
    }

    private transient VersioningService versioningService;

    protected VersioningService getVersioningService() {
        if (versioningService == null) {
            try {
                versioningService = Framework.getService(VersioningService.class);
            } catch (Exception e) {
                throw new RuntimeException("VersioningService not found", e);
            }
        }
        return versioningService;
    }

    /**
     * Used to resolve core documents based on session.
     */
    protected final DocumentResolver documentResolver = new DocumentResolver();

    private String sessionId;

    /**
     * Internal method: Gets the current session based on the client session id.
     *
     * @return the repository session
     */
    public abstract Session getSession() throws ClientException;

    @Override
    public String connect(String repositoryName, Map<String, Serializable> context) throws ClientException {
        if (null == context) {
            context = new HashMap<String, Serializable>();
        }

        if (sessionId != null) {
            throw new AlreadyConnectedException();
        }
        this.repositoryName = repositoryName;
        sessionContext = context;
        // this session is valid until the disconnect method is called
        sessionId = createSessionId();
        sessionContext.put("SESSION_ID", sessionId);
        // register this session locally -> this way document models can
        // retrieve their session on the server side
        CoreInstance.getInstance().registerSession(sessionId, this);

        // <------------ begin repository initialization
        // we need to initialize the repository if this is the first time it is
        // accessed in this JVM session.
        // For this we get the session and test if the
        // "REPOSITORY_FIRST_ACCESS" is set after the session is created. We
        // need to synchronize the call to be sure we initialize it only once.
        synchronized (AbstractSession.class) {
            Session session = getSession(); // force the creation of the
            // underlying session
            if (sessionContext.remove("REPOSITORY_FIRST_ACCESS") != null) {
                // this is the first time we access the repository in this JVM
                // notify the InitializationHandler if any.
                RepositoryInitializationHandler handler = RepositoryInitializationHandler.getInstance();
                if (handler != null) {
                    // change principal to give all rights
                    Principal ctxPrincipal = (Principal) sessionContext.get("principal");
                    try {
                        // change current principal to give all right to the
                        // handler
                        // FIXME: this should be fixed by using
                        // SystemPrincipal => we must synchronize this with
                        // SecurityService check
                        sessionContext.put("principal", new SimplePrincipal(SYSTEM_USERNAME));
                        try {
                            handler.initializeRepository(this);
                            session.save();
                        } catch (ClientException e) {
                            // shouldn't remove the root? ... to restart with
                            // an empty repository
                            log.error("Failed to initialize repository content", e);
                        } catch (DocumentException e) {
                            log.error("Unable to save session after repository init : " + e.getMessage());
                        }
                    } finally {
                        sessionContext.remove("principal");
                        if (ctxPrincipal != null) { // restore principal
                            sessionContext.put("principal", (Serializable) ctxPrincipal);
                        }
                    }
                }
            }
        }
        // <------------- end repository initialization

        return sessionId;
    }

    /**
     * Default implementation for session ID generation.
     * <p>
     * The ID has the following format:
     * &lt;repository-name&gt;-&lt;JVM-Unique-ID&gt; where the JVM-Unique-ID is
     * an unique ID on a running JVM and repository-name is a used to avoid name
     * clashes with sessions on different machines (the repository name should
     * be unique in the system)
     * <ul>
     * <li>A is the repository name (which uniquely identifies the repository in
     * the system)
     * <li>B is the time of the session creation in milliseconds
     * </ul>
     */
    protected String createSessionId() {
        return repositoryName + '-' + SIDGenerator.next();
    }

    @Override
    public DocumentType getDocumentType(String type) {
        return NXSchema.getSchemaManager().getDocumentType(type);
    }

    /**
     * Utility method to generate VersionModel labels.
     *
     * @return the String representation of an auto-incremented counter that not
     *         used in any label of docRef
     */
    @Override
    public String generateVersionLabelFor(DocumentRef docRef) throws ClientException {
        // find the biggest label that is castable to an int
        int max = 0;
        for (VersionModel version : getVersionsForDocument(docRef)) {
            try {
                int current = Integer.parseInt(version.getLabel());
                max = current > max ? current : max;
            } catch (NumberFormatException e) {
                // ignore labels that are not parsable as int
            }
        }
        return Integer.toString(max + 1);
    }

    @Override
    public String getSessionId() {
        return sessionId;
    }

    @Override
    public Principal getPrincipal() {
        return ANONYMOUS;
    }

    protected final void checkPermission(Document doc, String permission)
            throws DocumentSecurityException, DocumentException {
        if (isAdministrator()) {
            return;
        }
        if (!hasPermission(doc, permission)) {
            throw new DocumentSecurityException(
                    "Privilege '" + permission + "' is not granted to '" + getPrincipal().getName() + "'");
        }
    }

    protected Map<String, Serializable> getContextMapEventInfo(DocumentModel doc) {
        Map<String, Serializable> options = new HashMap<String, Serializable>();
        if (doc != null) {
            ScopedMap ctxData = doc.getContextData();
            if (ctxData != null) {
                options.putAll(ctxData.getDefaultScopeValues());
                options.putAll(ctxData.getScopeValues(ScopeType.REQUEST));
            }
        }
        return options;
    }

    public DocumentEventContext newEventContext(DocumentModel source) {
        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);
        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, repositoryName);
        ctx.setProperty(CoreEventConstants.SESSION_ID, sessionId);
        return ctx;
    }

    public EventService getEventService() {
        if (eventService == null) {
            try {
                eventService = Framework.getLocalService(EventService.class);
            } catch (Exception e) {
                throw new RuntimeException("Core Event Service not found", e);
            }
        }
        return eventService;
    }

    @Override
    public void afterBegin() {
        if (log.isTraceEnabled()) {
            log.trace("Transaction started");
        }
        try {
            getEventService().transactionStarted();
        } catch (Exception e) {
            log.error("Error while notifying transaction start", e);
        }
    }

    @Override
    public void beforeCompletion() {
    }

    @Override
    public void afterCompletion(boolean committed) {
        if (log.isTraceEnabled()) {
            log.trace("Transaction " + (committed ? "committed" : "rolled back"));
        }
        try {
            if (committed) {
                getEventService().transactionCommitted();
            } else {
                getEventService().transactionRolledback();
            }
        } catch (Exception e) {
            log.error("Error while notifying transaction completion", e);
        }
    }

    public void fireEvent(Event event) throws ClientException {
        getEventService().fireEvent(event);
    }

    protected void notifyEvent(String eventId, DocumentModel source, Map<String, Serializable> options,
            String category, String comment, boolean withLifeCycle, boolean inline) throws ClientException {

        DocumentEventContext ctx = new DocumentEventContext(this, getPrincipal(), source);

        // compatibility with old code (< 5.2.M4) - import info from old event
        // model
        if (options != null) {
            ctx.setProperties(options);
        }
        ctx.setProperty(CoreEventConstants.REPOSITORY_NAME, repositoryName);
        ctx.setProperty(CoreEventConstants.SESSION_ID, sessionId);
        // Document life cycle
        if (source != null && withLifeCycle) {
            String currentLifeCycleState = null;
            try {
                currentLifeCycleState = source.getCurrentLifeCycleState();
            } catch (ClientException err) {
                // FIXME no lifecycle -- this shouldn't generated an
                // exception (and ClientException logs the spurious error)
            }
            ctx.setProperty(CoreEventConstants.DOC_LIFE_CYCLE, currentLifeCycleState);
        }
        if (comment != null) {
            ctx.setProperty("comment", comment);
        }
        ctx.setProperty("category", category == null ? DocumentEventCategories.EVENT_DOCUMENT_CATEGORY : category);
        // compat code: mark SAVE event as a commit event
        Event event = ctx.newEvent(eventId);
        if (DocumentEventTypes.SESSION_SAVED.equals(eventId)) {
            event.setIsCommitEvent(true);
        }
        if (inline) {
            event.setInline(true);
        }
        // compat code: set isLocal on event if JMS is blocked
        if (source != null) {
            Boolean blockJms = (Boolean) source.getContextData("BLOCK_JMS_PRODUCING");
            if (blockJms != null && blockJms) {
                event.setLocal(true);
                event.setInline(true);
            }
        }
        fireEvent(event);
    }

    /**
     * Copied from obsolete VersionChangeNotifier.
     * <p>
     * Sends change notifications to core event listeners. The event contains
     * info with older document (before version change) and newer doc (current
     * document).
     *
     * @param oldDocument
     * @param newDocument
     * @param options additional info to pass to the event
     */
    protected void notifyVersionChange(DocumentModel oldDocument, DocumentModel newDocument,
            Map<String, Serializable> options) throws ClientException {
        final Map<String, Serializable> info = new HashMap<String, Serializable>();
        if (options != null) {
            info.putAll(options);
        }
        info.put(VersioningChangeNotifier.EVT_INFO_NEW_DOC_KEY, newDocument);
        info.put(VersioningChangeNotifier.EVT_INFO_OLD_DOC_KEY, oldDocument);
        notifyEvent(VersioningChangeNotifier.CORE_EVENT_ID_VERSIONING_CHANGE, newDocument, info,
                DocumentEventCategories.EVENT_CLIENT_NOTIF_CATEGORY, null, false, false);
    }

    @Override
    public boolean hasPermission(Principal principal, DocumentRef docRef, String permission)
            throws ClientException {
        try {
            Session session = getSession();
            Document doc = DocumentResolver.resolveReference(session, docRef);
            return hasPermission(principal, doc, permission);
        } catch (DocumentException e) {
            throw new ClientException("Failed to resolve document ref: " + docRef.toString(), e);
        }
    }

    protected final boolean hasPermission(Principal principal, Document doc, String permission)
            throws DocumentException {
        return getSecurityService().checkPermission(doc, principal, permission);
    }

    @Override
    public boolean hasPermission(DocumentRef docRef, String permission) throws ClientException {
        try {
            Session session = getSession();
            Document doc = DocumentResolver.resolveReference(session, docRef);
            return hasPermission(doc, permission);
        } catch (DocumentException e) {
            throw new ClientException("Failed to resolve document ref: " + docRef.toString(), e);
        }
    }

    protected final boolean hasPermission(Document doc, String permission) throws DocumentException {
        // TODO: optimize this - usually ACP is already available when calling
        // this method.
        // -> cache ACP at securitymanager level or try to reuse the ACP when
        // it is known
        return getSecurityService().checkPermission(doc, getPrincipal(), permission);
        // return doc.getSession().getSecurityManager().checkPermission(doc,
        // getPrincipal().getName(), permission);
    }

    protected final Document resolveReference(DocumentRef docRef) throws DocumentException, ClientException {
        return DocumentResolver.resolveReference(getSession(), docRef);
    }

    /**
     * Gets the document model for the given core document.
     *
     * @param doc the document
     * @return the document model
     */
    protected DocumentModel readModel(Document doc) throws ClientException {
        try {
            return DocumentModelFactory.createDocumentModel(doc, null);
        } catch (DocumentException e) {
            throw new ClientException("Failed to create document model", e);
        }
    }

    /**
     * Gets the document model for the given core document, preserving the contextData.
     *
     * @param doc the document
     * @return the document model
     */
    protected DocumentModel readModel(Document doc, DocumentModel docModel) throws ClientException {
        DocumentModel newModel = readModel(doc);
        newModel.copyContextData(docModel);
        return newModel;
    }

    /**
     * @deprecated unused
     */
    @Deprecated
    protected DocumentModel readModel(Document doc, String[] schemas) throws ClientException {
        try {
            return DocumentModelFactory.createDocumentModel(doc, schemas);
        } catch (DocumentException e) {
            throw new ClientException("Failed to create document model", e);
        }
    }

    protected DocumentModel writeModel(Document doc, DocumentModel docModel)
            throws DocumentException, ClientException {
        return DocumentModelFactory.writeDocumentModel(docModel, doc);
    }

    @Override
    public DocumentModel copy(DocumentRef src, DocumentRef dst, String name) throws ClientException {
        try {
            Document srcDoc = resolveReference(src);
            Document dstDoc = resolveReference(dst);
            checkPermission(dstDoc, ADD_CHILDREN);

            DocumentModel srcDocModel = readModel(srcDoc);
            notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true);

            Document doc = getSession().copy(srcDoc, dstDoc, name);
            // no need to clear lock, locks table is not copied

            Map<String, Serializable> options = new HashMap<String, Serializable>();

            // notify document created by copy
            DocumentModel docModel = readModel(doc);

            String comment = srcDoc.getRepository().getName() + ':' + src.toString();
            notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);
            docModel = writeModel(doc, docModel);

            // notify document copied
            comment = doc.getRepository().getName() + ':' + docModel.getRef().toString();

            notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);

            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to copy document: " + e.getMessage(), e);
        }
    }

    @Override
    public List<DocumentModel> copy(List<DocumentRef> src, DocumentRef dst) throws ClientException {
        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();

        for (DocumentRef ref : src) {
            newDocuments.add(copy(ref, dst, null));
        }

        return newDocuments;
    }

    @Override
    public DocumentModel copyProxyAsDocument(DocumentRef src, DocumentRef dst, String name) throws ClientException {
        try {
            Document srcDoc = resolveReference(src);
            if (!srcDoc.isProxy()) {
                return copy(src, dst, name);
            }
            Document dstDoc = resolveReference(dst);
            checkPermission(dstDoc, WRITE);

            // create a new document using the expanded proxy
            DocumentModel srcDocModel = readModel(srcDoc);
            String docName = (name != null) ? name : srcDocModel.getName();
            DocumentModel docModel = createDocumentModel(dstDoc.getPath(), docName, srcDocModel.getType());
            docModel.copyContent(srcDocModel);
            notifyEvent(DocumentEventTypes.ABOUT_TO_COPY, srcDocModel, null, null, null, true, true);
            docModel = createDocument(docModel);
            Document doc = resolveReference(docModel.getRef());

            Map<String, Serializable> options = new HashMap<String, Serializable>();
            // notify document created by copy
            String comment = srcDoc.getRepository().getName() + ':' + src.toString();
            notifyEvent(DocumentEventTypes.DOCUMENT_CREATED_BY_COPY, docModel, options, null, comment, true, false);

            // notify document copied
            comment = doc.getRepository().getName() + ':' + docModel.getRef().toString();
            notifyEvent(DocumentEventTypes.DOCUMENT_DUPLICATED, srcDocModel, options, null, comment, true, false);

            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to copy document: " + e.getMessage(), e);
        }
    }

    @Override
    public List<DocumentModel> copyProxyAsDocument(List<DocumentRef> src, DocumentRef dst) throws ClientException {
        List<DocumentModel> newDocuments = new ArrayList<DocumentModel>();

        for (DocumentRef ref : src) {
            newDocuments.add(copyProxyAsDocument(ref, dst, null));
        }

        return newDocuments;
    }

    @Override
    public DocumentModel move(DocumentRef src, DocumentRef dst, String name) throws ClientException {
        try {
            Document srcDoc = resolveReference(src);
            Document dstDoc = resolveReference(dst);
            checkPermission(dstDoc, ADD_CHILDREN);
            checkPermission(srcDoc.getParent(), REMOVE_CHILDREN);
            checkPermission(srcDoc, REMOVE);

            DocumentModel srcDocModel = readModel(srcDoc);
            notifyEvent(DocumentEventTypes.ABOUT_TO_MOVE, srcDocModel, null, null, null, true, true);

            String comment = srcDoc.getRepository().getName() + ':' + srcDoc.getParent().getUUID();

            if (name == null) {
                name = srcDoc.getName();
            }
            name = generateDocumentName(dstDoc, name);
            Document doc = getSession().move(srcDoc, dstDoc, name);

            // notify document moved
            DocumentModel docModel = readModel(doc);
            Map<String, Serializable> options = new HashMap<String, Serializable>();
            options.put(CoreEventConstants.PARENT_PATH, srcDocModel.getParentRef());
            notifyEvent(DocumentEventTypes.DOCUMENT_MOVED, docModel, options, null, comment, true, false);

            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to move document: " + e.getMessage(), e);
        }
    }

    @Override
    public void move(List<DocumentRef> src, DocumentRef dst) throws ClientException {
        for (DocumentRef ref : src) {
            move(ref, dst, null);
        }
    }

    @Override
    public ACP getACP(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_SECURITY);
            return getSession().getSecurityManager().getMergedACP(doc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get acp", e);
        }
    }

    @Override
    public void setACP(DocumentRef docRef, ACP newAcp, boolean overwrite) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, WRITE_SECURITY);
            DocumentModel docModel = readModel(doc);

            Map<String, Serializable> options = new HashMap<String, Serializable>();
            options.put(CoreEventConstants.OLD_ACP, (Serializable) docModel.getACP().clone());
            options.put(CoreEventConstants.NEW_ACP, (Serializable) newAcp.clone());

            notifyEvent(DocumentEventTypes.BEFORE_DOC_SECU_UPDATE, docModel, options, null, null, true, true);
            getSession().getSecurityManager().setACP(doc, newAcp, overwrite);
            docModel = readModel(doc);
            notifyEvent(DocumentEventTypes.DOCUMENT_SECURITY_UPDATED, docModel, options, null, null, true, false);
        } catch (DocumentException e) {
            throw new ClientException("Failed to set acp", e);
        }
    }

    @Override
    public void cancel() throws ClientException {
        try {
            getSession().cancel();
        } catch (DocumentException e) {
            throw new ClientException("Failed to cancel session", e);
        }
    }

    private DocumentModel createDocumentModelFromTypeName(String typeName, Map<String, Serializable> options)
            throws ClientException {
        try {
            DocumentType docType = getSession().getTypeManager().getDocumentType(typeName);
            if (docType == null) {
                throw new ClientException(typeName + " is not a registered core type");
            }
            DocumentModel docModel = DocumentModelFactory.createDocumentModel(sessionId, docType);
            if (options == null) {
                options = new HashMap<String, Serializable>();
            }
            // do not forward this event on the JMS Bus
            options.put("BLOCK_JMS_PRODUCING", true);
            notifyEvent(DocumentEventTypes.EMPTY_DOCUMENTMODEL_CREATED, docModel, options, null, null, false, true);
            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to create document model", e);
        }
    }

    @Override
    public DocumentModel createDocumentModel(String typeName) throws ClientException {
        Map<String, Serializable> options = new HashMap<String, Serializable>();
        return createDocumentModelFromTypeName(typeName, options);
    }

    @Override
    public DocumentModel createDocumentModel(String parentPath, String id, String typeName) throws ClientException {
        Map<String, Serializable> options = new HashMap<String, Serializable>();
        options.put(CoreEventConstants.PARENT_PATH, parentPath);
        options.put(CoreEventConstants.DOCUMENT_MODEL_ID, id);
        DocumentModel model = createDocumentModelFromTypeName(typeName, options);
        model.setPathInfo(parentPath, id);
        return model;
    }

    @Override
    public DocumentModel createDocumentModel(String typeName, Map<String, Object> options) throws ClientException {

        Map<String, Serializable> serializableOptions = new HashMap<String, Serializable>();

        for (Entry<String, Object> entry : options.entrySet()) {
            serializableOptions.put(entry.getKey(), (Serializable) entry.getValue());
        }
        return createDocumentModelFromTypeName(typeName, serializableOptions);
    }

    @Override
    public DocumentModel createDocument(DocumentModel docModel) throws ClientException {
        String typeName = docModel.getType();
        DocumentRef parentRef = docModel.getParentRef();
        if (typeName == null) {
            throw new ClientException(
                    String.format("cannot create document '%s' with undefined type name", docModel.getTitle()));
        }
        if (parentRef == null && !isAdministrator()) {
            throw new ClientException("Only Administrators can create placeless documents");
        }
        try {
            Document folder = parentRef == null ? null : resolveReference(parentRef);
            if (folder != null) {
                checkPermission(folder, ADD_CHILDREN);
            }

            // get initial life cycle state info
            String initialLifecycleState = null;
            Object lifecycleStateInfo = docModel
                    .getContextData(LifeCycleConstants.INITIAL_LIFECYCLE_STATE_OPTION_NAME);
            if (lifecycleStateInfo instanceof String) {
                initialLifecycleState = (String) lifecycleStateInfo;
            }

            Map<String, Serializable> options = getContextMapEventInfo(docModel);
            notifyEvent(DocumentEventTypes.ABOUT_TO_CREATE, docModel, options, null, null, false, true); // no lifecycle yet
            String name = docModel.getName();
            name = generateDocumentName(folder, name);
            if (folder == null) {
                folder = getSession().getNullDocument();
            }
            Document doc = folder.addChild(name, typeName);

            // init document life cycle
            LifeCycleService service = NXCore.getLifeCycleService();
            if (service != null) {
                try {
                    service.initialize(doc, initialLifecycleState);
                } catch (Exception e) {
                    throw new ClientException("Failed to initialize document lifecycle", e);
                }
            } else {
                log.debug("No lifecycle service registered");
            }

            // init document with data from doc model
            docModel = writeModel(doc, docModel);

            if (!Boolean.TRUE
                    .equals(docModel.getContextData(ScopeType.REQUEST, VersioningService.SKIP_VERSIONING))) {
                // during remote publishing we want to skip versioning
                // to avoid overwriting the version number
                getVersioningService().doPostCreate(doc, options);
                docModel = readModel(doc, docModel);
            }

            notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, docModel, options, null, null, true, false);
            docModel = writeModel(doc, docModel);

            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to create document: " + docModel.getName(), e);
        }
    }

    @Override
    public void importDocuments(List<DocumentModel> docModels) throws ClientException {
        try {
            for (DocumentModel docModel : docModels) {
                importDocument(docModel);
            }
        } catch (DocumentException e) {
            throw new ClientException("Failed to import documents", e);
        }
    }

    protected static final PathRef EMPTY_PATH = new PathRef("");

    protected void importDocument(DocumentModel docModel) throws DocumentException, ClientException {
        if (!isAdministrator()) {
            throw new DocumentSecurityException("Only Administrator can import");
        }
        String name = docModel.getName();
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("Invalid empty name");
        }
        String typeName = docModel.getType();
        if (typeName == null || typeName.length() == 0) {
            throw new IllegalArgumentException("Invalid empty type");
        }
        String id = docModel.getId();
        if (id == null || id.length() == 0) {
            throw new IllegalArgumentException("Invalid empty id");
        }
        DocumentRef parentRef = docModel.getParentRef();
        Document parent = parentRef == null || EMPTY_PATH.equals(parentRef) ? null : resolveReference(parentRef);
        Map<String, Serializable> props = docModel.getContextData().getDefaultScopeValues();

        if (parent != null) {
            name = generateDocumentName(parent, name);
        }

        // create the document
        Document doc = getSession().importDocument(id, parent, name, typeName, props);

        if (typeName.equals(CoreSession.IMPORT_PROXY_TYPE)) {
            // just reread the final document
            docModel = readModel(doc);
        } else {
            // init document with data from doc model
            docModel = writeModel(doc, docModel);
        }

        // send an event about the import
        notifyEvent(DocumentEventTypes.DOCUMENT_IMPORTED, docModel, null, null, null, true, false);
    }

    /**
     * Generate a non-null unique name within given parent's children.
     * <p>
     * If name is null, a name is generated. If name is already used, a random
     * suffix is appended to it.
     *
     * @return a unique name within given parent's children
     */
    public String generateDocumentName(Document parent, String name) throws DocumentException {
        if (name == null || name.length() == 0) {
            name = IdUtils.generateStringId();
        }
        if (parent != null && parent.hasChild(name)) {
            name += '.' + String.valueOf(System.currentTimeMillis());
        }
        return name;
    }

    @Override
    public DocumentModel[] createDocument(DocumentModel[] docModels) throws ClientException {
        DocumentModel[] models = new DocumentModel[docModels.length];
        int i = 0;
        // TODO: optimize this (do not call at each iteration createDocument())
        for (DocumentModel docModel : docModels) {
            models[i++] = createDocument(docModel);
        }
        return models;
    }

    public abstract boolean isSessionAlive();

    @Override
    public void disconnect() throws ClientException {
        if (isSessionAlive()) {
            getSession().dispose();
        }
        if (sessionId != null) {
            CoreInstance.getInstance().unregisterSession(sessionId);
        }
        sessionContext = null;
        sessionId = null;
        repositoryName = null;
    }

    @Override
    public boolean exists(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            return hasPermission(doc, BROWSE);
        } catch (NoSuchDocumentException e) {
            return false;
        } catch (DocumentException e) {
            throw new ClientException("Failed to check existence of " + docRef, e);
        }
    }

    @Override
    public DocumentModel getChild(DocumentRef parent, String name) throws ClientException {
        try {
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Document child = doc.getChild(name);
            checkPermission(child, READ);
            return readModel(child);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get child " + name, e);
        }
    }

    @Override
    public DocumentModelList getChildren(DocumentRef parent) throws ClientException {
        return getChildren(parent, null, READ, null, null);
    }

    @Override
    public DocumentModelIterator getChildrenIterator(DocumentRef parent) throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_CHILDREN);
        def.setParent(parent);
        return new DocumentModelIteratorImpl(this, 15, def, null, READ, null);
    }

    @Override
    public DocumentModelList getChildren(DocumentRef parent, String type) throws ClientException {
        return getChildren(parent, type, READ, null, null);
    }

    @Override
    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type) throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_CHILDREN);
        def.setParent(parent);
        return new DocumentModelIteratorImpl(this, 15, def, type, null, null);
    }

    @Override
    public DocumentModelList getChildren(DocumentRef parent, String type, String perm) throws ClientException {
        return getChildren(parent, type, perm, null, null);
    }

    @Override
    public DocumentModelList getChildren(DocumentRef parent, String type, Filter filter, Sorter sorter)
            throws ClientException {
        return getChildren(parent, type, null, filter, sorter);
    }

    @Override
    public DocumentModelList getChildren(DocumentRef parent, String type, String perm, Filter filter, Sorter sorter)
            throws ClientException {
        try {
            if (perm == null) {
                perm = READ;
            }
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            DocumentModelList docs = new DocumentModelListImpl();
            while (children.hasNext()) {
                Document child = children.next();
                if (hasPermission(child, perm)) {
                    if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) {
                        DocumentModel childModel = readModel(child);
                        if (filter == null || filter.accept(childModel)) {
                            docs.add(childModel);
                        }
                    }
                }
            }
            if (sorter != null) {
                Collections.sort(docs, sorter);
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get children for " + parent.toString(), e);
        }
    }

    @Override
    public List<DocumentRef> getChildrenRefs(DocumentRef parentRef, String perm) throws ClientException {
        if (perm != null) {
            // XXX TODO
            throw new ClientException("perm != null not implemented");
        }
        try {
            Document parent = resolveReference(parentRef);
            checkPermission(parent, READ_CHILDREN);
            List<String> ids = parent.getChildrenIds();
            List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
            for (String id : ids) {
                refs.add(new IdRef(id));
            }
            return refs;
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    /**
     * Method used internally to retrieve frames of a long result.
     */
    @Override
    public DocumentModelsChunk getDocsResultChunk(DocsQueryProviderDef def, String type, String perm, Filter filter,
            final int start, final int max) throws ClientException {
        // convention: if count == 0 return all results to the end
        if (max < 0) {
            throw new IllegalArgumentException("invalid count=" + max);
        }
        int count = max;

        DocsQueryProviderFactory dqpFactory = new DocsQueryProviderFactory(this);
        try {
            if (perm == null) {
                perm = READ;
            }

            DocsQueryProvider dqp = dqpFactory.getDQLbyType(def);
            // Document doc = resolveReference(parent);
            // checkPermission(doc, READ_CHILDREN);
            // Iterator<Document> children = doc.getChildren(start);
            final DocumentIterator children = dqp.getDocs(start);
            DocumentModelList docs = new DocumentModelListImpl();
            int lastIndex = start;
            boolean hasMore = false;
            while (children.hasNext()) {
                lastIndex++;
                Document child = children.next();

                // 1st filter:
                if (!dqp.accept(child)) {
                    continue;
                }

                if (hasPermission(child, perm)) {
                    if (child.getType() != null && (type == null || type.equals(child.getType().getName()))) {
                        DocumentModel childModel = readModel(child);
                        if (filter == null || filter.accept(childModel)) {
                            if (count == 0) {
                                // end of page
                                hasMore = true;
                                break;
                            }
                            count--;
                            docs.add(childModel);
                        }
                    }
                }
            }

            final long total = children.getSize();

            return new DocumentModelsChunk(docs, lastIndex - 1, hasMore, total);
        } catch (DocumentException e) {
            if (def.getParent() != null) {
                throw new ClientException("Failed to get children for " + def.getParent().toString(), e);
            } else {
                throw new ClientException("Failed to get documents for query: " + def.getQuery(), e);
            }
        }
    }

    @Override
    public DocumentModelIterator getChildrenIterator(DocumentRef parent, String type, String perm, Filter filter)
            throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_CHILDREN);
        def.setParent(parent);
        return new DocumentModelIteratorImpl(this, 15, def, type, perm, filter);
    }

    @Override
    public DocumentModel getDocument(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            return readModel(doc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get document " + docRef.toString(), e);
        }
    }

    /**
     * @deprecated unused
     */
    @Override
    @Deprecated
    public DocumentModel getDocument(DocumentRef docRef, String[] schemas) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            return readModel(doc, schemas);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get document " + docRef, e);
        }
    }

    @Override
    public DocumentModelList getDocuments(DocumentRef[] docRefs) throws ClientException {
        List<DocumentModel> docs = new ArrayList<DocumentModel>(docRefs.length);
        for (DocumentRef docRef : docRefs) {
            Document doc;
            try {
                doc = resolveReference(docRef);
                checkPermission(doc, READ);
            } catch (DocumentException e) {
                // no permission, or other low-level error
                continue;
            }
            docs.add(readModel(doc));
        }
        return new DocumentModelListImpl(docs);
    }

    @Override
    public DocumentModelList getFiles(DocumentRef parent) throws ClientException {
        try {
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            DocumentModelList docs = new DocumentModelListImpl();
            while (children.hasNext()) {
                Document child = children.next();
                if (!child.isFolder() && hasPermission(child, READ)) {
                    docs.add(readModel(child));
                }
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get leaf children for " + parent.toString(), e);
        }
    }

    @Override
    public DocumentModelIterator getFilesIterator(DocumentRef parent) throws ClientException {

        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_CHILDREN_NON_FOLDER);
        def.setParent(parent);
        return new DocumentModelIteratorImpl(this, 15, def, null, null, null);
    }

    @Override
    public DocumentModelList getFiles(DocumentRef parent, Filter filter, Sorter sorter) throws ClientException {
        try {
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            DocumentModelList docs = new DocumentModelListImpl();
            while (children.hasNext()) {
                Document child = children.next();
                if (!child.isFolder() && hasPermission(child, READ)) {
                    DocumentModel docModel = readModel(doc);
                    if (filter == null || filter.accept(docModel)) {
                        docs.add(readModel(child));
                    }
                }
            }
            if (sorter != null) {
                Collections.sort(docs, sorter);
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get files for " + parent.toString(), e);
        }
    }

    @Override
    public DocumentModelList getFolders(DocumentRef parent) throws ClientException {
        try {
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            DocumentModelList docs = new DocumentModelListImpl();
            while (children.hasNext()) {
                Document child = children.next();
                if (child.isFolder() && hasPermission(child, READ)) {
                    docs.add(readModel(child));
                }
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get folders " + parent, e);
        }
    }

    @Override
    public DocumentModelIterator getFoldersIterator(DocumentRef parent) throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_CHILDREN_FOLDERS);
        def.setParent(parent);
        return new DocumentModelIteratorImpl(this, 15, def, null, null, null);
    }

    @Override
    public DocumentModelList getFolders(DocumentRef parent, Filter filter, Sorter sorter) throws ClientException {
        try {
            Document doc = resolveReference(parent);
            checkPermission(doc, READ_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            DocumentModelList docs = new DocumentModelListImpl();
            while (children.hasNext()) {
                Document child = children.next();
                if (child.isFolder() && hasPermission(child, READ)) {
                    DocumentModel docModel = readModel(doc);
                    if (filter == null || filter.accept(docModel)) {
                        docs.add(readModel(child));
                    }
                }
            }
            if (sorter != null) {
                Collections.sort(docs, sorter);
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get folders " + parent.toString(), e);
        }
    }

    @Override
    public DocumentRef getParentDocumentRef(DocumentRef docRef) throws ClientException {
        try {
            final Document doc = resolveReference(docRef);
            Document parentDoc = doc.getParent();
            return parentDoc != null ? new IdRef(parentDoc.getUUID()) : null;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get parent document ref for: " + docRef, e);
        }
    }

    @Override
    public DocumentModel getParentDocument(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document parentDoc = doc.getParent();
            if (parentDoc == null) {
                return null;
            }
            if (!hasPermission(parentDoc, READ)) {
                throw new DocumentSecurityException("Privilege READ is not granted to " + getPrincipal().getName());
            }
            return readModel(parentDoc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get parent document of " + docRef, e);
        }
    }

    @Override
    public List<DocumentModel> getParentDocuments(final DocumentRef docRef) throws ClientException {

        if (null == docRef) {
            throw new IllegalArgumentException("null docRef");
        }

        final List<DocumentModel> docsList = new ArrayList<DocumentModel>();
        try {
            Document doc = resolveReference(docRef);
            String rootPath = getSession().getRootDocument().getPath();
            while (!doc.getPath().equals(rootPath)) {
                // XXX OG: shouldn't we check BROWSE and READ_PROPERTIES
                // instead?
                if (!hasPermission(doc, READ)) {
                    break;
                }
                docsList.add(readModel(doc));
                doc = doc.getParent();
            }
        } catch (DocumentException e) {
            throw new ClientException("Failed to get parent documents: " + docRef, e);
        }
        Collections.reverse(docsList);

        return docsList;
    }

    @Override
    public DocumentModel getRootDocument() throws ClientException {
        try {
            return readModel(getSession().getRootDocument());
        } catch (DocumentException e) {
            throw new ClientException("Failed to get the root document", e);
        }
    }

    @Override
    public boolean hasChildren(DocumentRef docRef) throws ClientException {
        try {
            // TODO: validate permission check with td
            Document doc = resolveReference(docRef);
            checkPermission(doc, BROWSE);
            return doc.hasChildren();
        } catch (DocumentException e) {
            throw new ClientException("Failed to check for children for " + docRef, e);
        }
    }

    @Override
    public DocumentModelList query(String query) throws ClientException {
        return query(query, null, 0, 0, false);
    }

    @Override
    public DocumentModelList query(String query, int max) throws ClientException {
        return query(query, null, max, 0, false);
    }

    @Override
    public DocumentModelList query(String query, Filter filter) throws ClientException {
        return query(query, filter, 0, 0, false);
    }

    @Override
    public DocumentModelList query(String query, Filter filter, int max) throws ClientException {
        return query(query, filter, max, 0, false);
    }

    @Override
    @SuppressWarnings("null")
    public DocumentModelList query(String query, Filter filter, long limit, long offset, boolean countTotal)
            throws ClientException {
        SecurityService securityService = getSecurityService();
        Principal principal = getPrincipal();
        try {
            Query compiledQuery = getSession().createQuery(query, Query.Type.NXQL);
            QueryResult results;
            boolean postFilterPermission;
            boolean postFilterFilter;
            boolean postFilterPolicies;
            boolean postFilter;
            String permission = BROWSE;
            if (compiledQuery instanceof FilterableQuery) {
                postFilterPermission = false;
                String repoName = getRepositoryName();
                postFilterPolicies = !securityService.arePoliciesExpressibleInQuery(repoName);
                postFilterFilter = filter != null && !(filter instanceof FacetFilter);
                postFilter = postFilterPolicies || postFilterFilter;
                String[] principals;
                if (isAdministrator()) {
                    principals = null; // means: no security check needed
                } else {
                    principals = SecurityService.getPrincipalsToCheck(principal);
                }
                String[] permissions = securityService.getPermissionsToCheck(permission);
                QueryFilter queryFilter = new QueryFilter(principal, principals, permissions,
                        filter instanceof FacetFilter ? (FacetFilter) filter : null,
                        securityService.getPoliciesQueryTransformers(repoName), postFilter ? 0 : limit,
                        postFilter ? 0 : offset);
                results = ((FilterableQuery) compiledQuery).execute(queryFilter, countTotal && !postFilter);
            } else {
                postFilterPermission = true;
                postFilterPolicies = securityService.arePoliciesRestrictingPermission(permission);
                postFilterFilter = filter != null;
                postFilter = true;
                results = compiledQuery.execute();
            }

            DocumentModelList dms = results.getDocumentModels();

            if (!postFilter) {
                // the backend has done all the needed filtering
                return dms;
            }

            // post-filter the results "by hand", the backend couldn't do it
            long start = limit == 0 || offset < 0 ? 0 : offset;
            long stop = start + (limit == 0 ? dms.size() : limit);
            int n = 0;
            DocumentModelListImpl docs = new DocumentModelListImpl();
            for (DocumentModel model : dms) {
                if (postFilterPermission || postFilterPolicies) {
                    if (!hasPermission(model.getRef(), permission)) {
                        continue;
                    }
                }
                if (postFilterFilter) {
                    if (!filter.accept(model)) {
                        continue;
                    }
                }
                if (n < start) {
                    n++;
                    continue;
                }
                if (n >= stop) {
                    if (!countTotal) {
                        // can break early
                        break;
                    }
                    n++;
                    continue;
                }
                n++;
                docs.add(model);
            }
            if (countTotal) {
                docs.setTotalSize(n);
            }
            return docs;
        } catch (Exception e) {
            throw new ClientException("Failed to execute query: " + tryToExtractMeaningfulErrMsg(e), e);
        }
    }

    @Override
    public IterableQueryResult queryAndFetch(String query, String queryType, Object... params)
            throws ClientException {
        try {
            SecurityService securityService = getSecurityService();
            Principal principal = getPrincipal();
            String[] principals;
            if (isAdministrator()) {
                principals = null; // means: no security check needed
            } else {
                principals = SecurityService.getPrincipalsToCheck(principal);
            }
            String permission = BROWSE;
            String[] permissions = securityService.getPermissionsToCheck(permission);
            Collection<Transformer> transformers;
            if ("NXQL".equals(queryType)) {
                String repoName = getRepositoryName();
                if (!securityService.arePoliciesExpressibleInQuery(repoName)) {
                    log.warn("Security policy cannot be expressed in query");
                }
                transformers = securityService.getPoliciesQueryTransformers(repoName);
            } else {
                transformers = Collections.emptyList();
            }
            QueryFilter queryFilter = new QueryFilter(principal, principals, permissions, null, transformers, 0, 0);
            return getSession().queryAndFetch(query, queryType, queryFilter, params);
        } catch (Exception e) {
            throw new ClientException(
                    "Failed to execute query: " + queryType + ": " + query + ": " + tryToExtractMeaningfulErrMsg(e),
                    e);
        }
    }

    @Override
    public DocumentModelIterator queryIt(String query, Filter filter, int max) throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_QUERY);
        def.setQuery(query);
        return new DocumentModelIteratorImpl(this, 15, def, null, BROWSE, filter);
    }

    private String tryToExtractMeaningfulErrMsg(Throwable t) {
        if (t instanceof QueryParseException) {
            return t.getMessage();
        }
        if (t.getCause() != null) {
            return tryToExtractMeaningfulErrMsg(t.getCause());
        }
        return t.getMessage();
    }

    @Override
    @Deprecated
    public DocumentModelList querySimpleFts(String keywords) throws ClientException {
        return querySimpleFts(keywords, null);
    }

    @Override
    @Deprecated
    public DocumentModelList querySimpleFts(String keywords, Filter filter) throws ClientException {
        try {
            // TODO this is hardcoded query : need to add support for CONTAINS
            // in NXQL
            // TODO check (repair) for keywords sanity to avoid xpath injection
            final String xpathQ = "//element(*, ecmnt:document)[jcr:contains(.,'*" + keywords + "*')]";
            final Query compiledQuery = getSession().createQuery(xpathQ, Query.Type.XPATH);
            final QueryResult qr = compiledQuery.execute();

            final DocumentModelList retrievedDocs = qr.getDocumentModels();

            final DocumentModelList docs = new DocumentModelListImpl();
            for (DocumentModel model : retrievedDocs) {
                if (hasPermission(model.getRef(), READ)) {
                    if (filter == null || filter.accept(model)) {
                        docs.add(model);
                    }
                }
            }

            return docs;

        } catch (Exception e) {
            log.error("failed to execute query", e);
            throw new ClientException("Failed to get the root document", e);
        }
    }

    @Override
    @Deprecated
    public DocumentModelIterator querySimpleFtsIt(String query, Filter filter, int pageSize)
            throws ClientException {
        return querySimpleFtsIt(query, null, filter, pageSize);
    }

    @Override
    @Deprecated
    public DocumentModelIterator querySimpleFtsIt(String query, String startingPath, Filter filter, int pageSize)
            throws ClientException {
        DocsQueryProviderDef def = new DocsQueryProviderDef(DocsQueryProviderDef.DefType.TYPE_QUERY_FTS);
        def.setQuery(query);
        def.setStartingPath(startingPath);
        return new DocumentModelIteratorImpl(this, pageSize, def, null, BROWSE, filter);
    }

    @Override
    public void removeChildren(DocumentRef docRef) throws ClientException {
        // TODO: check req permissions with td
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, REMOVE_CHILDREN);
            Iterator<Document> children = doc.getChildren();
            while (children.hasNext()) {
                // iterator remove method is not supported by jcr
                Document child = children.next();
                if (hasPermission(child, REMOVE)) {
                    removeNotifyOneDoc(child);
                }
            }
        } catch (DocumentException e) {
            throw new ClientException("Failed to remove children for " + docRef, e);
        }
    }

    @Override
    public boolean canRemoveDocument(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            return canRemoveDocument(doc);
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    protected boolean canRemoveDocument(Document doc) throws ClientException, DocumentException {
        if (isAdministrator()) {
            return true;
        }
        if (doc.isVersion()) {
            // TODO a hasProxies method would be more efficient
            Collection<Document> proxies = getSession().getProxies(doc, null);
            if (!proxies.isEmpty()) {
                return false;
            }
            // find a working document to check security
            Document working;
            try {
                working = doc.getSourceDocument();
            } catch (DocumentException e) {
                working = null;
            }
            if (working != null) {
                return hasPermission(working, WRITE_VERSION);
            } else {
                // no working document, only admins can remove
                return false;
            }
        } else {
            if (!hasPermission(doc, REMOVE)) {
                return false;
            }
            Document parent = doc.getParent();
            return parent == null || hasPermission(parent, REMOVE_CHILDREN);
        }
    }

    @Override
    public void removeDocument(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            removeDocument(doc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to fetch document " + docRef + " before removal", e);
        }
    }

    protected void removeDocument(Document doc) throws ClientException {
        try {
            if (!canRemoveDocument(doc)) {
                throw new DocumentSecurityException("Permission denied: cannot remove document " + doc.getUUID());
            }
            removeNotifyOneDoc(doc);

        } catch (DocumentException e) {
            throw new ClientException("Failed to remove document " + doc.getUUID(), e);
        }
    }

    protected void removeNotifyOneDoc(Document doc) throws ClientException, DocumentException {
        // XXX notify with options if needed
        DocumentModel docModel = readModel(doc);
        Map<String, Serializable> options = new HashMap<String, Serializable>();
        if (docModel != null) {
            options.put("docTitle", docModel.getTitle());
        }
        String versionLabel = "";
        Document sourceDoc = null;
        // notify different events depending on wether the document is a
        // version or not
        if (!doc.isVersion()) {
            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE, docModel, options, null, null, true, true);
            CoreService coreService = Framework.getLocalService(CoreService.class);
            coreService.getVersionRemovalPolicy().removeVersions(getSession(), doc, this);
        } else {
            versionLabel = docModel.getVersionLabel();
            try {
                sourceDoc = doc.getSourceDocument();
            } catch (DocumentException e) {
                sourceDoc = null;
            }
            notifyEvent(DocumentEventTypes.ABOUT_TO_REMOVE_VERSION, docModel, options, null, null, true, true);

        }
        doc.remove();
        if (doc.isVersion()) {
            if (sourceDoc != null) {
                DocumentModel sourceDocModel = readModel(sourceDoc);
                if (sourceDocModel != null) {
                    options.put("comment", versionLabel); // to be used by audit
                    // service
                    notifyEvent(DocumentEventTypes.VERSION_REMOVED, sourceDocModel, options, null, null, false,
                            false);
                    options.remove("comment");
                }
                options.put("docSource", sourceDoc.getUUID());
            }
        }
        notifyEvent(DocumentEventTypes.DOCUMENT_REMOVED, docModel, options, null, null, false, false);
    }

    /**
     * Implementation uses the fact that the lexicographic ordering of paths is
     * a refinement of the "contains" partial ordering.
     */
    @Override
    public void removeDocuments(DocumentRef[] docRefs) throws ClientException {
        Document[] docs = new Document[docRefs.length];

        for (int i = 0; i < docs.length; i++) {
            try {
                docs[i] = resolveReference(docRefs[i]);
            } catch (DocumentException e) {
                throw new ClientException("Failed to resolve reference " + docRefs[i], e);
            }
        }
        // TODO OPTIM: it's not guaranteed that getPath is cheap and
        // we call it a lot. Should use an object for pairs (document, path)
        // to call it just once per doc.
        Arrays.sort(docs, pathComparator);
        String[] paths = new String[docs.length];
        try {
            for (int i = 0; i < docs.length; i++) {
                paths[i] = docs[i].getPath();
            }
            String latestRemoved = null;
            for (int i = 0; i < docs.length; i++) {
                if (i == 0 || !paths[i].startsWith(latestRemoved + "/")) {
                    removeDocument(docs[i]);
                    latestRemoved = paths[i];
                }
            }
        } catch (DocumentException e) {
            throw new ClientException("Failed to get path of document", e);
        }
    }

    @Override
    public void save() throws ClientException {
        try {
            final Map<String, Serializable> options = new HashMap<String, Serializable>();
            getSession().save();
            notifyEvent(DocumentEventTypes.SESSION_SAVED, null, options, null, null, true, false);
        } catch (DocumentException e) {
            throw new ClientException("Failed to save session", e);
        }
    }

    @Override
    public DocumentModel saveDocument(DocumentModel docModel) throws ClientException {
        try {
            if (docModel.getRef() == null) {
                throw new ClientException(String.format("cannot save document '%s' with null reference: "
                        + "document has probably not yet been created " + "in the repository with "
                        + "'CoreSession.createDocument(docModel)'", docModel.getTitle()));
            }
            Document doc = resolveReference(docModel.getRef());
            checkPermission(doc, WRITE_PROPERTIES);

            Map<String, Serializable> options = getContextMapEventInfo(docModel);

            if (!docModel.isImmutable()) {
                // regular event, last chance to modify docModel
                notifyEvent(DocumentEventTypes.BEFORE_DOC_UPDATE, docModel, options, null, null, true, true);
            }

            VersioningOption versioningOption = (VersioningOption) docModel
                    .getContextData(VersioningService.VERSIONING_OPTION);
            docModel.putContextData(VersioningService.VERSIONING_OPTION, null);
            String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
            docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
            // compat
            boolean snapshot = Boolean.TRUE.equals(
                    docModel.getContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY));
            docModel.putContextData(ScopeType.REQUEST, VersioningDocument.CREATE_SNAPSHOT_ON_SAVE_KEY, null);
            boolean dirty = isDirty(docModel);
            if (versioningOption == null && snapshot && dirty) {
                String key = String
                        .valueOf(docModel.getContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION));
                docModel.putContextData(ScopeType.REQUEST, VersioningDocument.KEY_FOR_INC_OPTION, null);
                versioningOption = "inc_major".equals(key) ? VersioningOption.MAJOR : VersioningOption.MINOR;
            }

            if (!docModel.isImmutable()) {
                // pre-save versioning
                versioningOption = getVersioningService().doPreSave(doc, dirty, versioningOption, checkinComment,
                        options);
            }

            // actual save
            docModel = writeModel(doc, docModel);
            Document checkedInDoc = null;

            if (!docModel.isImmutable()) {
                // post-save versioning
                checkedInDoc = getVersioningService().doPostSave(doc, versioningOption, checkinComment, options);
            }

            // post-save events
            docModel = readModel(doc);
            if (checkedInDoc != null) {
                DocumentRef checkedInVersionRef = new IdRef(checkedInDoc.getUUID());
                notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
            }
            notifyEvent(DocumentEventTypes.DOCUMENT_UPDATED, docModel, options, null, null, true, false);

            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to save document " + docModel, e);
        }
    }

    @Override
    @Deprecated
    public boolean isDirty(DocumentRef docRef) throws ClientException {
        try {
            return resolveReference(docRef).isCheckedOut();
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    /**
     * Checks if the document has actual data to write (dirty parts).
     * <p>
     * Used to avoid doing an auto-checkout if there's nothing to update.
     */
    protected boolean isDirty(DocumentModel doc) throws ClientException {
        // get loaded data models
        for (DocumentPart part : doc.getParts()) {
            if (part.isDirty()) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void saveDocuments(DocumentModel[] docModels) throws ClientException {
        // TODO: optimize this - avoid calling at each iteration saveDoc...
        for (DocumentModel docModel : docModels) {
            saveDocument(docModel);
        }
    }

    @Override
    public DocumentModel getSourceDocument(DocumentRef docRef) throws ClientException {
        assert null != docRef;

        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            Document headDocument = doc.getSourceDocument();
            if (headDocument == null) {
                throw new DocumentException("Source document has been deleted");
            }
            return readModel(headDocument);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get head document for " + docRef, e);
        }
    }

    protected VersionModel getVersionModel(Document version) throws DocumentException {
        VersionModel versionModel = new VersionModelImpl();
        versionModel.setId(version.getUUID());
        versionModel.setCreated(version.getVersionCreationDate());
        versionModel.setDescription(version.getCheckinComment());
        versionModel.setLabel(version.getVersionLabel());
        return versionModel;
    }

    @Override
    public VersionModel getLastVersion(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            Document version = doc.getLastVersion();
            return version == null ? null : getVersionModel(version);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }
    }

    @Override
    public DocumentModel getLastDocumentVersion(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            Document version = doc.getLastVersion();
            return version == null ? null : readModel(version);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }
    }

    @Override
    public DocumentRef getLastDocumentVersionRef(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            Document version = doc.getLastVersion();
            return version == null ? null : new IdRef(version.getUUID());
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }
    }

    @Override
    public List<DocumentRef> getVersionsRefs(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            List<String> ids = doc.getVersionsIds();
            List<DocumentRef> refs = new ArrayList<DocumentRef>(ids.size());
            for (String id : ids) {
                refs.add(new IdRef(id));
            }
            return refs;
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    @Override
    public List<DocumentModel> getVersions(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            List<Document> docVersions = doc.getVersions();
            List<DocumentModel> versions = new ArrayList<DocumentModel>(docVersions.size());
            for (Document version : docVersions) {
                versions.add(readModel(version));
            }
            return versions;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }
    }

    @Override
    public List<VersionModel> getVersionsForDocument(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            List<Document> docVersions = doc.getVersions();
            List<VersionModel> versions = new ArrayList<VersionModel>(docVersions.size());
            for (Document version : docVersions) {
                versions.add(getVersionModel(version));
            }
            return versions;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }

    }

    @Override
    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document ver = resolveReference(versionRef);
            return restoreToVersion(doc, ver, false, true);
        } catch (DocumentException e) {
            throw new ClientException("Failed to restore document", e);
        }
    }

    @Override
    @Deprecated
    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version) throws ClientException {
        return restoreToVersion(docRef, version, false);
    }

    @Override
    @Deprecated
    public DocumentModel restoreToVersion(DocumentRef docRef, VersionModel version, boolean skipSnapshotCreation)
            throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document ver = doc.getVersion(version.getLabel());
            return restoreToVersion(doc, ver, skipSnapshotCreation, false);
        } catch (DocumentException e) {
            throw new ClientException("Failed to restore document", e);
        }
    }

    @Override
    public DocumentModel restoreToVersion(DocumentRef docRef, DocumentRef versionRef, boolean skipSnapshotCreation,
            boolean skipCheckout) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document ver = resolveReference(versionRef);
            return restoreToVersion(doc, ver, skipSnapshotCreation, skipCheckout);
        } catch (DocumentException e) {
            throw new ClientException("Failed to restore document", e);
        }
    }

    protected DocumentModel restoreToVersion(Document doc, Document version, boolean skipSnapshotCreation,
            boolean skipCheckout) throws ClientException {
        try {
            checkPermission(doc, WRITE_VERSION);

            DocumentModel docModel = readModel(doc);

            // we're about to overwrite the document, make sure it's archived
            if (!skipSnapshotCreation && doc.isCheckedOut()) {
                String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
                docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
                getVersioningService().doCheckIn(doc, null, checkinComment);
            }

            final Map<String, Serializable> options = new HashMap<String, Serializable>();

            // FIXME: the fields are hardcoded. should be moved in versioning
            // component
            // HOW?
            final Long majorVer = doc.getLong("major_version");
            final Long minorVer = doc.getLong("minor_version");
            if (majorVer != null || minorVer != null) {
                options.put(VersioningDocument.CURRENT_DOCUMENT_MAJOR_VERSION_KEY, majorVer);
                options.put(VersioningDocument.CURRENT_DOCUMENT_MINOR_VERSION_KEY, minorVer);
            }
            // add the uuid of the version being restored
            String versionUUID = version.getUUID();
            options.put(VersioningDocument.RESTORED_VERSION_UUID_KEY, versionUUID);

            notifyEvent(DocumentEventTypes.BEFORE_DOC_RESTORE, docModel, options, null, null, true, true);
            writeModel(doc, docModel);

            doc.restore(version);

            if (!skipCheckout) {
                // restore gives us a checked in document, so do a checkout
                getVersioningService().doCheckOut(doc);
            }

            // re-read doc model after restoration
            docModel = readModel(doc);
            notifyEvent(DocumentEventTypes.DOCUMENT_RESTORED, docModel, options, null, null, true, false);
            docModel = writeModel(doc, docModel);

            log.debug("Document restored to version:" + version.getUUID());
            return docModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to restore document " + doc, e);
        }
    }

    @Override
    public DocumentRef getBaseVersion(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            Document ver = doc.getBaseVersion();
            if (ver == null) {
                return null;
            }
            checkPermission(ver, READ);
            return new IdRef(ver.getUUID());
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    @Override
    @Deprecated
    public DocumentModel checkIn(DocumentRef docRef, VersionModel ver) throws ClientException {
        try {
            DocumentRef verRef = checkIn(docRef, VersioningOption.MINOR, ver == null ? null : ver.getDescription());
            return readModel(resolveReference(verRef));
        } catch (DocumentException e) {
            throw new ClientException("Failed to check in document " + docRef, e);
        }
    }

    @Override
    public DocumentRef checkIn(DocumentRef docRef, VersioningOption option, String checkinComment)
            throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, WRITE_PROPERTIES);
            DocumentModel docModel = readModel(doc);

            Map<String, Serializable> options = new HashMap<String, Serializable>();
            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKIN, docModel, options, null, null, true, true);
            writeModel(doc, docModel);

            Document version = getVersioningService().doCheckIn(doc, option, checkinComment);
            DocumentModel versionModel = readModel(version);

            notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, versionModel, options, null, null, true, false);

            docModel = readModel(doc);
            DocumentRef checkedInVersionRef = versionModel.getRef();
            notifyCheckedInVersion(docModel, checkedInVersionRef, options, checkinComment);
            writeModel(doc, docModel);

            return versionModel.getRef();
        } catch (DocumentException e) {
            throw new ClientException("Failed to check in document " + docRef, e);
        }
    }

    /**
     * Send a core event for the creation of a new check in version. The source
     * document is the live document model used as the source for the checkin,
     * not the archived version it-self.
     *
     * @param docModel work document that has been checked-in as a version
     * @param checkedInVersionRef document ref of the new checked-in version
     * @param options initial option map, or null
     * @param checkinComment
     * @throws ClientException
     */
    protected void notifyCheckedInVersion(DocumentModel docModel, DocumentRef checkedInVersionRef,
            Map<String, Serializable> options, String checkinComment) throws ClientException {
        String label = getVersioningService().getVersionLabel(docModel);
        if (options == null) {
            options = new HashMap<String, Serializable>();
        }
        options.put("versionLabel", label);
        options.put("checkInComment", checkinComment);
        options.put("checkedInVersionRef", checkedInVersionRef);
        String comment = checkinComment == null ? label : label + ' ' + checkinComment;
        options.put("comment", comment); // compat, used in audit
        notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDIN, docModel, options, null, null, true, false);
    }

    @Override
    public void checkOut(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            // TODO: add a new permission names CHECKOUT and use it instead of
            // WRITE_PROPERTIES
            checkPermission(doc, WRITE_PROPERTIES);
            DocumentModel docModel = readModel(doc);
            Map<String, Serializable> options = new HashMap<String, Serializable>();

            notifyEvent(DocumentEventTypes.ABOUT_TO_CHECKOUT, docModel, options, null, null, true, true);

            getVersioningService().doCheckOut(doc);
            docModel = readModel(doc);

            notifyEvent(DocumentEventTypes.DOCUMENT_CHECKEDOUT, docModel, options, null, null, true, false);
            writeModel(doc, docModel);
        } catch (DocumentException e) {
            throw new ClientException("Failed to check out document " + docRef, e);
        }
    }

    public void internalCheckOut(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
        } catch (DocumentException e) {
            throw new ClientException("Failed to check out document " + docRef, e);
        }
    }

    @Override
    public boolean isCheckedOut(DocumentRef docRef) throws ClientException {
        assert null != docRef;

        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, BROWSE);
            return doc.isCheckedOut();
        } catch (DocumentException e) {
            throw new ClientException("Failed to check out document " + docRef, e);
        }
    }

    @Override
    public String getVersionSeriesId(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            return doc.getVersionSeriesId();
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    @Override
    public DocumentModel getWorkingCopy(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_VERSION);
            Document pwc = doc.getWorkingCopy();
            checkPermission(pwc, READ);
            return pwc == null ? null : readModel(pwc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get versions for " + docRef, e);
        }
    }

    @Override
    public DocumentModel getVersion(String versionableId, VersionModel versionModel) throws ClientException {
        String id = versionModel.getId();
        if (id != null) {
            return getDocument(new IdRef(id));
        }
        try {
            Document doc = getSession().getVersion(versionableId, versionModel);
            if (doc == null) {
                return null;
            }
            checkPermission(doc, READ_PROPERTIES);
            checkPermission(doc, READ_VERSION);
            return readModel(doc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get version " + versionModel.getLabel() + " for " + versionableId,
                    e);
        }
    }

    @Override
    public String getVersionLabel(DocumentModel docModel) throws ClientException {
        return getVersioningService().getVersionLabel(docModel);
    }

    @Override
    public DocumentModel getDocumentWithVersion(DocumentRef docRef, VersionModel version) throws ClientException {
        String id = version.getId();
        if (id != null) {
            return getDocument(new IdRef(id));
        }
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ_PROPERTIES);
            checkPermission(doc, READ_VERSION);
            String docPath = doc.getPath();
            doc = doc.getVersion(version.getLabel());
            if (doc == null) {
                // SQL Storage uses to return null if version not found
                log.debug("Version " + version.getLabel() + " does not exist for " + docPath);
                return null;
            }
            log.debug("Retrieved the version " + version.getLabel() + " of the document " + docPath);
            return readModel(doc);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get version for " + docRef, e);
        }
    }

    @Override
    public DocumentModel createProxy(DocumentRef docRef, DocumentRef folderRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document fold = resolveReference(folderRef);
            checkPermission(doc, READ);
            checkPermission(fold, ADD_CHILDREN);
            return createProxyInternal(doc, fold, new HashMap<String, Serializable>());
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    protected DocumentModel createProxyInternal(Document doc, Document folder, Map<String, Serializable> options)
            throws ClientException {
        try {
            // create the new proxy
            Document proxy = getSession().createProxy(doc, folder);
            DocumentModel proxyModel = readModel(proxy);

            notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false);
            notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false);
            DocumentModel folderModel = readModel(folder);
            notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, folderModel, options, null, null, true,
                    false);
            return proxyModel;
        } catch (DocumentException e) {
            throw new ClientException("Failed to create proxy for doc: " + doc, e);
        }
    }

    @Override
    @Deprecated
    public DocumentModel createProxy(DocumentRef parentRef, DocumentRef docRef, VersionModel version,
            boolean overwriteExistingProxy) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document sec = resolveReference(parentRef);
            checkPermission(doc, READ);
            checkPermission(sec, ADD_CHILDREN);

            DocumentModel proxyModel = null;
            Map<String, Serializable> options = new HashMap<String, Serializable>();
            String vlabel = version.getLabel();

            if (overwriteExistingProxy) {
                Document target = getSession().getVersion(doc.getUUID(), version);
                if (target == null) {
                    throw new ClientException("Document " + docRef + " has no version " + vlabel);
                }
                proxyModel = updateExistingProxies(doc, sec, target);
                // proxyModel is null is update fails
                if (proxyModel != null) {
                    // notify for proxy updates
                    notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxyModel, options, null, null, true,
                            false);
                } else {
                    List<String> removedProxyIds = Collections.emptyList();
                    removedProxyIds = removeExistingProxies(doc, sec);
                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
                }
            }

            if (proxyModel == null) {
                // create the new proxy
                Document proxy = getSession().createProxyForVersion(sec, doc, vlabel);
                log.debug("Created proxy for version " + vlabel + " of the document " + doc.getPath());
                // notify for reindexing
                proxyModel = readModel(proxy);

                // notify for document creation (proxy)
                notifyEvent(DocumentEventTypes.DOCUMENT_CREATED, proxyModel, options, null, null, true, false);
            }

            notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxyModel, options, null, null, true, false);

            DocumentModel sectionModel = readModel(sec);
            notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, sectionModel, options, null, null, true,
                    false);

            return proxyModel;

        } catch (DocumentException e) {
            throw new ClientException(
                    "Failed to create proxy for doc " + docRef + " , version: " + version.getLabel(), e);
        }
    }

    /**
     * Remove proxies for the same base document in the folder. doc may be a
     * normal document or a proxy.
     */
    protected List<String> removeExistingProxies(Document doc, Document folder)
            throws DocumentException, ClientException {
        Collection<Document> otherProxies = getSession().getProxies(doc, folder);
        List<String> removedProxyIds = new ArrayList<String>(otherProxies.size());
        for (Document otherProxy : otherProxies) {
            removedProxyIds.add(otherProxy.getUUID());
            removeNotifyOneDoc(otherProxy);
        }
        return removedProxyIds;
    }

    /**
     * Update the proxy for doc in the given section to point to the new target.
     * Do nothing if there are several proxies.
     *
     * @return the proxy if it was updated, or {@code null} if none or several
     *         were found
     */
    protected DocumentModel updateExistingProxies(Document doc, Document folder, Document target)
            throws DocumentException, ClientException {
        Collection<Document> proxies = getSession().getProxies(doc, folder);
        try {
            if (proxies.size() == 1) {
                for (Document proxy : proxies) {
                    if (proxy instanceof DocumentProxy) {
                        ((DocumentProxy) proxy).setTargetDocument(target);
                        return readModel(proxy);
                    }
                }
            }
        } catch (UnsupportedOperationException e) {
            log.error("Cannot update proxy, try to remove");
        }
        return null;
    }

    @Override
    public DocumentModelList getProxies(DocumentRef docRef, DocumentRef folderRef) throws ClientException {
        try {
            Document folder = null;
            if (folderRef != null) {
                folder = resolveReference(folderRef);
                checkPermission(folder, READ_CHILDREN);
            }
            Document doc = resolveReference(docRef);
            Collection<Document> children = getSession().getProxies(doc, folder);
            DocumentModelList docs = new DocumentModelListImpl();
            for (Document child : children) {
                if (hasPermission(child, READ)) {
                    docs.add(readModel(child));
                }
            }
            return docs;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get children for " + folderRef, e);
        }
    }

    @Override
    public String[] getProxyVersions(DocumentRef docRef, DocumentRef folderRef) throws ClientException {
        try {
            Document folder = resolveReference(folderRef);
            Document doc = resolveReference(docRef);
            checkPermission(folder, READ_CHILDREN);
            Collection<Document> children = getSession().getProxies(doc, folder);
            if (children.isEmpty()) {
                return null;
            }
            List<String> versions = new ArrayList<String>();
            for (Document child : children) {
                if (hasPermission(child, READ)) {
                    Document target = ((DocumentProxy) child).getTargetDocument();
                    if (target.isVersion()) {
                        versions.add(target.getVersionLabel());
                    } else {
                        // live proxy
                        versions.add("");
                    }
                }
            }
            return versions.toArray(new String[versions.size()]);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get children for " + folderRef.toString(), e);
        }
    }

    @Override
    public List<String> getAvailableSecurityPermissions() throws ClientException {
        // XXX: add security check?
        return Arrays.asList(getSecurityService().getPermissionProvider().getPermissions());
    }

    @Override
    @Deprecated
    public DataModel getDataModel(DocumentRef docRef, String schema) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            Schema docSchema = doc.getType().getSchema(schema);
            return DocumentModelFactory.createDataModel(doc, docSchema);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get data model for " + docRef + ':' + schema, e);
        }
    }

    @Override
    public DataModel getDataModel(DocumentRef docRef, Schema schema) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            return DocumentModelFactory.createDataModel(doc, schema);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get data model for " + docRef + ':' + schema, e);
        }
    }

    @Override
    @Deprecated
    public Object getDataModelField(DocumentRef docRef, String schema, String field) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            if (doc != null) {
                checkPermission(doc, READ);
                Schema docSchema = doc.getType().getSchema(schema);
                if (docSchema != null) {
                    String prefix = docSchema.getNamespace().prefix;
                    if (prefix != null && prefix.length() > 0) {
                        field = prefix + ':' + field;
                    }
                    return doc.getPropertyValue(field);
                } else {
                    log.warn("Cannot find schema with name=" + schema);
                }
            } else {
                log.warn("Cannot resolve docRef=" + docRef);
            }
            return null;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get data model field " + schema + ':' + field, e);
        }
    }

    @Override
    @Deprecated
    public Object[] getDataModelFields(DocumentRef docRef, String schema, String[] fields) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            Schema docSchema = doc.getType().getSchema(schema);
            String prefix = docSchema.getNamespace().prefix;
            if (prefix != null && prefix.length() > 0) {
                prefix += ':';
            }
            // prefix is not used for the moment
            // else {
            // prefix = null;
            // }
            Object[] values = new Object[fields.length];
            for (int i = 0; i < fields.length; i++) {
                if (prefix != null) {
                    values[i] = doc.getPropertyValue(fields[i]);
                }
            }
            return values;
        } catch (DocumentException e) {
            throw new ClientException("Failed to check out document " + docRef, e);
        }
    }

    @Override
    public SerializableInputStream getContentData(String key) throws ClientException {
        try {
            InputStream in = getSession().getDataStream(key);
            return new SerializableInputStream(in);
        } catch (Exception e) {
            throw new ClientException("Failed to get data stream for " + key, e);
        }
    }

    @Override
    public String getStreamURI(String blobPropertyId) throws ClientException {
        String uri;
        try {
            InputStream in = getContentData(blobPropertyId);
            StreamManager sm = Framework.getLocalService(StreamManager.class);
            if (sm == null) {
                throw new ClientException("No Streaming Service was registered");
            }
            uri = sm.addStream(new InputStreamSource(in));
        } catch (ClientException e) {
            throw e;
        } catch (Exception e) {
            throw new ClientException("Failed to register blob stream: " + blobPropertyId, e);
        }
        return uri;
    }

    @Override
    public String getCurrentLifeCycleState(DocumentRef docRef) throws ClientException {
        String lifeCycleState;
        try {
            Document doc = resolveReference(docRef);

            checkPermission(doc, READ_LIFE_CYCLE);
            lifeCycleState = doc.getLifeCycleState();
        } catch (LifeCycleException e) {
            ClientException ce = new ClientException("Failed to get life cycle " + docRef, e);
            ce.fillInStackTrace();
            throw ce;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get content data " + docRef, e);
        }
        return lifeCycleState;
    }

    @Override
    public String getLifeCyclePolicy(DocumentRef docRef) throws ClientException {
        String lifecyclePolicy;
        try {
            Document doc = resolveReference(docRef);

            checkPermission(doc, READ_LIFE_CYCLE);
            lifecyclePolicy = doc.getLifeCyclePolicy();
        } catch (LifeCycleException e) {
            ClientException ce = new ClientException("Failed to get life cycle policy" + docRef, e);
            ce.fillInStackTrace();
            throw ce;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get content data " + docRef, e);
        }
        return lifecyclePolicy;
    }

    @Override
    public boolean followTransition(DocumentRef docRef, String transition) throws ClientException {
        boolean operationResult;
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, WRITE_LIFE_CYCLE);
            String formerStateName = doc.getLifeCycleState();
            operationResult = doc.followTransition(transition);

            if (operationResult) {
                // Construct a map holding meta information about the event.
                Map<String, Serializable> options = new HashMap<String, Serializable>();
                options.put(org.eclipse.ecr.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_FROM,
                        formerStateName);
                options.put(org.eclipse.ecr.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TO,
                        doc.getLifeCycleState());
                options.put(org.eclipse.ecr.core.api.LifeCycleConstants.TRANSTION_EVENT_OPTION_TRANSITION,
                        transition);
                DocumentModel docModel = readModel(doc);
                notifyEvent(org.eclipse.ecr.core.api.LifeCycleConstants.TRANSITION_EVENT, docModel, options,
                        DocumentEventCategories.EVENT_LIFE_CYCLE_CATEGORY, null, true, false);
            }
        } catch (LifeCycleException e) {
            ClientException ce = new ClientException(
                    "Unable to follow transition <" + transition + "> for document : " + docRef, e);
            ce.fillInStackTrace();
            throw ce;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get content data " + docRef, e);
        }
        return operationResult;
    }

    @Override
    public Collection<String> getAllowedStateTransitions(DocumentRef docRef) throws ClientException {
        Collection<String> allowedStateTransitions;
        try {
            Document doc = resolveReference(docRef);

            checkPermission(doc, READ_LIFE_CYCLE);
            allowedStateTransitions = doc.getAllowedStateTransitions();
        } catch (LifeCycleException e) {
            ClientException ce = new ClientException(
                    "Unable to get allowed state transitions for document : " + docRef, e);
            ce.fillInStackTrace();
            throw ce;
        } catch (DocumentException e) {
            throw new ClientException("Failed to get content data " + docRef, e);
        }
        return allowedStateTransitions;
    }

    @Override
    public Object[] getDataModelsField(DocumentRef[] docRefs, String schema, String field) throws ClientException {

        assert docRefs != null;
        assert schema != null;
        assert field != null;

        final Object[] values = new Object[docRefs.length];
        int i = 0;
        for (DocumentRef docRef : docRefs) {
            final Object value = getDataModelField(docRef, schema, field);
            values[i++] = value;
        }

        return values;
    }

    @Override
    public DocumentRef[] getParentDocumentRefs(DocumentRef docRef) throws ClientException {

        final List<DocumentRef> docRefs = new ArrayList<DocumentRef>();
        try {
            final Document doc = resolveReference(docRef);

            Document parentDoc = doc.getParent();
            while (parentDoc != null) {
                final DocumentRef parentDocRef = new IdRef(parentDoc.getUUID());
                docRefs.add(parentDocRef);
                parentDoc = parentDoc.getParent();
            }
        } catch (DocumentException e) {
            throw new ClientException("Failed to get all parent documents: " + docRef, e);
        }

        DocumentRef[] refs = new DocumentRef[docRefs.size()];

        return docRefs.toArray(refs);
    }

    @Override
    public Object[] getDataModelsFieldUp(DocumentRef docRef, String schema, String field) throws ClientException {

        final DocumentRef[] parentRefs = getParentDocumentRefs(docRef);
        final DocumentRef[] allRefs = new DocumentRef[parentRefs.length + 1];
        allRefs[0] = docRef;
        System.arraycopy(parentRefs, 0, allRefs, 1, parentRefs.length);

        return getDataModelsField(allRefs, schema, field);
    }

    protected String oldLockKey(Lock lock) {
        if (lock == null) {
            return null;
        }
        // return deprecated format, like "someuser:Nov 29, 2010"
        return lock.getOwner() + ':' + DateFormat.getDateInstance(DateFormat.MEDIUM)
                .format(new Date(lock.getCreated().getTimeInMillis()));
    }

    @Override
    @Deprecated
    public String getLock(DocumentRef docRef) throws ClientException {
        Lock lock = getLockInfo(docRef);
        return oldLockKey(lock);
    }

    @Override
    @Deprecated
    public void setLock(DocumentRef docRef, String key) throws ClientException {
        setLock(docRef);
    }

    @Override
    @Deprecated
    public String unlock(DocumentRef docRef) throws ClientException {
        Lock lock = removeLock(docRef);
        return oldLockKey(lock);
    }

    @Override
    public Lock setLock(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            // TODO: add a new permission named LOCK and use it instead of
            // WRITE_PROPERTIES
            checkPermission(doc, WRITE_PROPERTIES);
            Lock lock = new Lock(getPrincipal().getName(), new GregorianCalendar());
            Lock oldLock = doc.setLock(lock);
            if (oldLock != null) {
                throw new ClientException("Document already locked by " + oldLock.getOwner() + ": " + docRef);
            }
            DocumentModel docModel = readModel(doc);
            Map<String, Serializable> options = new HashMap<String, Serializable>();
            options.put("lock", lock);
            notifyEvent(DocumentEventTypes.DOCUMENT_LOCKED, docModel, options, null, null, true, false);
            return lock;
        } catch (DocumentException e) {
            throw new ClientException("Failed to set lock on " + docRef, e);
        }
    }

    @Override
    public Lock getLockInfo(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            checkPermission(doc, READ);
            return doc.getLock();
        } catch (DocumentException e) {
            throw new ClientException("Failed to get lock info on " + docRef, e);
        }
    }

    @Override
    public Lock removeLock(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            String owner;
            if (hasPermission(docRef, UNLOCK)) {
                // always unlock
                owner = null;
            } else {
                owner = getPrincipal().getName();
            }
            Lock lock = doc.removeLock(owner);
            if (lock == null) {
                // there was no lock, we're done
                return null;
            }
            if (lock.getFailed()) {
                // lock removal failed due to owner check
                throw new ClientException("Document already locked by " + lock.getOwner() + ": " + docRef);
            }
            DocumentModel docModel = readModel(doc);
            Map<String, Serializable> options = new HashMap<String, Serializable>();
            options.put("lock", lock);
            notifyEvent(DocumentEventTypes.DOCUMENT_UNLOCKED, docModel, options, null, null, true, false);
            return lock;
        } catch (DocumentException e) {
            throw new ClientException("Failed to set lock on " + docRef, e);
        }
    }

    protected boolean isAdministrator() {
        Principal principal = getPrincipal();
        // FIXME: this is inconsistent with NuxeoPrincipal#isAdministrator
        // method because it allows hardcoded Administrator user
        if (Framework.isTestModeSet()) {
            if (SecurityConstants.ADMINISTRATOR.equals(principal.getName())) {
                return true;
            }
        }
        if (SYSTEM_USERNAME.equals(principal.getName())) {
            return true;
        }
        if (principal instanceof NuxeoPrincipal) {
            return ((NuxeoPrincipal) principal).isAdministrator();
        }
        return false;
    }

    @Override
    public void applyDefaultPermissions(String userOrGroupName) throws ClientException {
        if (null == userOrGroupName) {
            throw new ClientException("Null parameters received.");
        }
        if (!isAdministrator()) {
            throw new DocumentSecurityException("You need to be an Administrator to do this.");
        }
        DocumentModel rootDocument = getRootDocument();
        ACP acp = new ACPImpl();

        UserEntry userEntry = new UserEntryImpl(userOrGroupName);
        userEntry.addPrivilege(READ, true, false);

        acp.setRules(new UserEntry[] { userEntry });

        setACP(rootDocument.getRef(), acp, false);
    }

    @Override
    public void destroy() {
        log.debug("Destroying core session ...");
        try {
            disconnect();
        } catch (Exception e) {
            log.error("Failed to destroy core session", e);
        }
    }

    @Override
    public DocumentModel publishDocument(DocumentModel docToPublish, DocumentModel section) throws ClientException {
        return publishDocument(docToPublish, section, true);
    }

    @Override
    public DocumentModel publishDocument(DocumentModel docModel, DocumentModel section,
            boolean overwriteExistingProxy) throws ClientException {
        try {
            Document doc = resolveReference(docModel.getRef());
            Document sec = resolveReference(section.getRef());
            checkPermission(doc, READ);
            checkPermission(sec, ADD_CHILDREN);

            Map<String, Serializable> options = new HashMap<String, Serializable>();
            DocumentModel proxy = null;
            Document target;
            if (docModel.isProxy()) {
                if (overwriteExistingProxy) {
                    // remove previous
                    List<String> removedProxyIds = removeExistingProxies(doc, sec);
                    options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
                }
                target = doc;
            } else {
                String checkinComment = (String) docModel.getContextData(VersioningService.CHECKIN_COMMENT);
                docModel.putContextData(VersioningService.CHECKIN_COMMENT, null);
                if (doc.isCheckedOut() || doc.getLastVersion() == null) {
                    if (!doc.isCheckedOut()) {
                        // last version was deleted while leaving a checked in
                        // doc. recreate a version
                        getVersioningService().doCheckOut(doc);
                    }
                    getVersioningService().doCheckIn(doc, null, checkinComment);
                    docModel.refresh(DocumentModel.REFRESH_STATE, null);
                }
                target = doc.getLastVersion();
                if (overwriteExistingProxy) {
                    proxy = updateExistingProxies(doc, sec, target);
                    if (proxy == null) {
                        // no or several proxies, remove them
                        List<String> removedProxyIds = removeExistingProxies(doc, sec);
                        options.put(CoreEventConstants.REPLACED_PROXY_IDS, (Serializable) removedProxyIds);
                    } else {
                        // notify proxy updates
                        notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_UPDATED, proxy, options, null, null, true,
                                false);
                        notifyEvent(DocumentEventTypes.DOCUMENT_PROXY_PUBLISHED, proxy, options, null, null, true,
                                false);
                        notifyEvent(DocumentEventTypes.SECTION_CONTENT_PUBLISHED, section, options, null, null,
                                true, false);
                    }
                }
            }
            if (proxy == null) {
                proxy = createProxyInternal(target, sec, options);
            }
            return proxy;
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    @Override
    public String getSuperParentType(DocumentModel doc) throws ClientException {
        DocumentModel superSpace = getSuperSpace(doc);
        if (superSpace == null) {
            return null;
        } else {
            return superSpace.getType();
        }
    }

    @Override
    public DocumentModel getSuperSpace(DocumentModel doc) throws ClientException {
        if (doc == null) {
            throw new ClientException("getSuperSpace: document is null");
        }
        if (doc.hasFacet("SuperSpace")) {
            return doc;
        } else {

            DocumentModel parent = getDirectAccessibleParent(doc.getRef());
            if (parent == null || "/".equals(parent.getPathAsString())) {
                // return Root instead of null
                return getRootDocument();
            } else {
                return getSuperSpace(parent);
            }
        }
    }

    // walk the tree up until a accessible doc is found
    private DocumentModel getDirectAccessibleParent(DocumentRef docRef) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            Document parentDoc = doc.getParent();
            if (parentDoc == null) {
                return readModel(doc);
            }
            if (!hasPermission(parentDoc, READ)) {
                String parentPath = parentDoc.getPath();
                if ("/".equals(parentPath)) {
                    return getRootDocument();
                } else {
                    // try on parent
                    return getDirectAccessibleParent(new PathRef(parentDoc.getPath()));
                }
            }
            return readModel(parentDoc);
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

    @Override
    public List<SecuritySummaryEntry> getSecuritySummary(DocumentModel docModel, Boolean includeParents)
            throws ClientException {
        if (docModel == null) {
            docModel = getRootDocument();
        }
        Document doc;
        try {
            doc = resolveReference(docModel.getRef());

        } catch (DocumentException e) {
            throw new ClientException("Failed to get document " + docModel.getRef().toString(), e);
        }

        return getSecurityService().getSecuritySummary(doc, includeParents);
    }

    @Override
    public String getRepositoryName() {
        return repositoryName;
    }

    @Override
    public <T extends Serializable> T getDocumentSystemProp(DocumentRef ref, String systemProperty, Class<T> type)
            throws ClientException, DocumentException {

        Document doc;
        try {
            doc = resolveReference(ref);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get document " + ref, e);
        }

        return doc.getSystemProp(systemProperty, type);
    }

    @Override
    public <T extends Serializable> void setDocumentSystemProp(DocumentRef ref, String systemProperty, T value)
            throws ClientException, DocumentException {
        Document doc;
        try {
            doc = resolveReference(ref);
        } catch (DocumentException e) {
            throw new ClientException("Failed to get document " + ref, e);
        }
        doc.setSystemProp(systemProperty, value);
    }

    @Override
    public void orderBefore(DocumentRef parent, String src, String dest) throws ClientException {
        try {
            if ((src == null) || (src.equals(dest))) {
                return;
            }
            Document doc = resolveReference(parent);
            doc.orderBefore(src, dest);
            Map<String, Serializable> options = new HashMap<String, Serializable>();

            // send event on container passing the reordered child as parameter
            DocumentModel docModel = readModel(doc);
            String comment = src;
            options.put(CoreEventConstants.REORDERED_CHILD, src);
            notifyEvent(DocumentEventTypes.DOCUMENT_CHILDREN_ORDER_CHANGED, docModel, options, null, comment, true,
                    false);

        } catch (DocumentException e) {
            throw new ClientException("Failed to resolve documents: " + src + ", " + dest, e);
        }
    }

    @Override
    public <T> T run(Operation<T> op) throws ClientException {
        return run(op, null);
    }

    @Override
    public <T> T run(Operation<T> op, ProgressMonitor monitor) throws ClientException {
        // double s = System.currentTimeMillis();
        T result = op.run(this, this, monitor);
        // System.out.println(">>>>> OPERATION "+op.getName()+" took: "+
        // ((System.currentTimeMillis()-s)/1000));
        Status status = op.getStatus();
        if (status.isOk()) {
            return result;
        } else {
            Throwable t = status.getException();
            if (t != null) {
                if (t instanceof ClientException) {
                    throw (ClientException) t;
                } else {
                    throw new ClientException(status.getMessage(), t);
                }
            } else {
                String msg = status.getMessage();
                if (msg == null) {
                    msg = "Unknown Error";
                }
                throw new ClientException(msg);
            }
        }
    }

    /**
     * This method is for compatibility reasons to notify an operation start.
     * Operations must be reworked to use the new event model. In order for
     * operation notification to work the event compatibility bundle must be
     * deployed.
     *
     * @see org.eclipse.ecr.core.event.compat.CompatibilityListener in
     *      nuxeo-core-event-compat
     */
    @Override
    public void startOperation(Operation<?> operation) {
        EventContextImpl ctx = new EventContextImpl(this, getPrincipal(), operation);
        Event event = ctx.newEvent("!OPERATION_START!");
        try {
            fireEvent(event);
        } catch (ClientException e) {
            log.error("Failed to notify operation start for: " + operation, e);
        }
        // old code was:
        // CoreEventListenerService service =
        // NXCore.getCoreEventListenerService();
        // service.fireOperationStarted(operation);
    }

    /**
     * This method is for compatibility reasons to notify an operation end.
     * Operations must be reworked to use the new event model. In order for
     * operation notification to work the event compatibility bundle must be
     * deployed.
     *
     * @see org.eclipse.ecr.core.event.compat.CompatibilityListener in
     *      nuxeo-core-event-compat
     */
    @Override
    public void endOperation(Operation<?> operation) {
        EventContextImpl ctx = new EventContextImpl(this, getPrincipal(), operation);
        Event event = ctx.newEvent("!OPERATION_END!");
        try {
            fireEvent(event);
        } catch (ClientException e) {
            log.error("Failed to notify operation end for: " + operation, e);
        }
        // old code was:
        // CoreEventListenerService service =
        // NXCore.getCoreEventListenerService();
        // service.fireOperationTerminated(operation);
    }

    @Override
    public DocumentModelRefresh refreshDocument(DocumentRef ref, int refreshFlags, String[] schemas)
            throws ClientException {
        try {
            Document doc = resolveReference(ref);
            if (doc == null) {
                throw new ClientException("No Such Document: " + ref);
            }

            // permission checks
            if ((refreshFlags & (DocumentModel.REFRESH_PREFETCH | DocumentModel.REFRESH_STATE
                    | DocumentModel.REFRESH_CONTENT)) != 0) {
                checkPermission(doc, READ);
            }
            if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
                checkPermission(doc, READ_SECURITY);
            }

            DocumentModelRefresh refresh = DocumentModelFactory.refreshDocumentModel(doc, refreshFlags, schemas);

            // ACPs need the session, so aren't done in the factory method
            if ((refreshFlags & DocumentModel.REFRESH_ACP) != 0) {
                refresh.acp = getSession().getSecurityManager().getMergedACP(doc);
            }

            return refresh;
        } catch (ClientException e) {
            throw e;
        } catch (Exception e) {
            throw new ClientException("Failed to get refresh data", e);
        }
    }

    @Override
    public String[] getPermissionsToCheck(String permission) {
        return getSecurityService().getPermissionsToCheck(permission);
    }

    @Override
    public <T extends DetachedAdapter> T adaptFirstMatchingDocumentWithFacet(DocumentRef docRef, String facet,
            Class<T> adapterClass) throws ClientException {
        Document doc = getFirstParentDocumentWithFacet(docRef, facet);
        if (doc != null) {
            DocumentModel docModel = readModel(doc);
            loadDataModelsForFacet(docModel, doc, facet);
            // detach the DocumentModel
            ((DocumentModelImpl) docModel).detach(false);
            return docModel.getAdapter(adapterClass);
        }
        return null;
    }

    protected void loadDataModelsForFacet(DocumentModel docModel, Document doc, String facetName)
            throws ClientException {
        // Load all the data related to facet's schemas
        SchemaManager schemaManager = NXSchema.getSchemaManager();
        CompositeType facet = schemaManager.getFacet(facetName);
        if (facet == null) {
            return;
        }

        String[] facetSchemas = facet.getSchemaNames();
        for (String schema : facetSchemas) {
            try {
                DataModel dm = DocumentModelFactory.createDataModel(doc, schemaManager.getSchema(schema));
                docModel.getDataModels().put(schema, dm);
            } catch (DocumentException e) {
                throw new ClientException(e);
            }
        }
    }

    /**
     * Returns the first {@code Document} with the given {@code facet},
     * recursively going up the parent hierarchy. Returns {@code null} if there
     * is no more parent.
     * <p>
     * This method does not check security rights.
     */
    protected Document getFirstParentDocumentWithFacet(DocumentRef docRef, String facet) throws ClientException {
        try {
            Document doc = resolveReference(docRef);
            while (doc != null && !doc.hasFacet(facet)) {
                doc = doc.getParent();
            }
            return doc;
        } catch (DocumentException e) {
            throw new ClientException(e);
        }
    }

}