org.eclipse.ecr.opencmis.impl.server.NuxeoCmisService.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.ecr.opencmis.impl.server.NuxeoCmisService.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:
 *     Florent Guillaume
 */
package org.eclipse.ecr.opencmis.impl.server;

import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;

import org.apache.chemistry.opencmis.client.api.ObjectId;
import org.apache.chemistry.opencmis.client.api.ObjectType;
import org.apache.chemistry.opencmis.client.api.OperationContext;
import org.apache.chemistry.opencmis.client.api.Policy;
import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
import org.apache.chemistry.opencmis.commons.PropertyIds;
import org.apache.chemistry.opencmis.commons.data.Ace;
import org.apache.chemistry.opencmis.commons.data.Acl;
import org.apache.chemistry.opencmis.commons.data.AllowableActions;
import org.apache.chemistry.opencmis.commons.data.ContentStream;
import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
import org.apache.chemistry.opencmis.commons.data.ObjectData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
import org.apache.chemistry.opencmis.commons.data.ObjectList;
import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
import org.apache.chemistry.opencmis.commons.data.Properties;
import org.apache.chemistry.opencmis.commons.data.PropertyData;
import org.apache.chemistry.opencmis.commons.data.RenditionData;
import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
import org.apache.chemistry.opencmis.commons.definitions.PropertyBooleanDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDateTimeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDecimalDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyHtmlDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyIdDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyIntegerDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyStringDefinition;
import org.apache.chemistry.opencmis.commons.definitions.PropertyUriDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
import org.apache.chemistry.opencmis.commons.enums.Cardinality;
import org.apache.chemistry.opencmis.commons.enums.ChangeType;
import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
import org.apache.chemistry.opencmis.commons.enums.Updatability;
import org.apache.chemistry.opencmis.commons.enums.VersioningState;
import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
import org.apache.chemistry.opencmis.commons.impl.Converter;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisTypeContainer;
import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
import org.apache.chemistry.opencmis.commons.server.CallContext;
import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
import org.apache.chemistry.opencmis.commons.spi.Holder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.ecr.audit.api.AuditReader;
import org.eclipse.ecr.audit.api.LogEntry;
import org.eclipse.ecr.core.api.ClientException;
import org.eclipse.ecr.core.api.CoreSession;
import org.eclipse.ecr.core.api.DocumentModel;
import org.eclipse.ecr.core.api.DocumentModelList;
import org.eclipse.ecr.core.api.DocumentRef;
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.LifeCycleConstants;
import org.eclipse.ecr.core.api.PathRef;
import org.eclipse.ecr.core.api.VersioningOption;
import org.eclipse.ecr.core.api.event.DocumentEventTypes;
import org.eclipse.ecr.core.api.impl.CompoundFilter;
import org.eclipse.ecr.core.api.impl.FacetFilter;
import org.eclipse.ecr.core.api.impl.LifeCycleFilter;
import org.eclipse.ecr.core.api.model.PropertyException;
import org.eclipse.ecr.core.api.pathsegment.PathSegmentService;
import org.eclipse.ecr.core.api.repository.Repository;
import org.eclipse.ecr.core.api.repository.RepositoryManager;
import org.eclipse.ecr.core.schema.FacetNames;
import org.eclipse.ecr.opencmis.impl.util.ListUtils;
import org.eclipse.ecr.opencmis.impl.util.ListUtils.BatchedList;
import org.eclipse.ecr.runtime.api.Framework;
import org.nuxeo.common.utils.Path;

/**
 * Nuxeo implementation of the CMIS Services, on top of a {@link CoreSession}.
 */
public class NuxeoCmisService extends AbstractCmisService {

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

    public static final int DEFAULT_TYPE_LEVELS = 2;

    public static final int DEFAULT_FOLDER_LEVELS = 2;

    public static final int DEFAULT_CHANGE_LOG_SIZE = 100;

    public static final int DEFAULT_QUERY_SIZE = 100;

    public static final int DEFAULT_MAX_CHILDREN = 100;

    public static final String NUXEO_WAR = "nuxeo.war";

    protected final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl();

    protected final NuxeoRepository repository;

    protected final CoreSession coreSession;

    /** When false, we don't own the core session and shouldn't close it. */
    protected final boolean coreSessionOwned;

    /** Filter that hides HiddenInNavigation and deleted objects. */
    protected final Filter documentFilter;

    protected final CallContext callContext;

    /** Constructor called by binding. */
    public NuxeoCmisService(NuxeoRepository repository, CallContext callContext) {
        this.repository = repository;
        this.callContext = callContext;
        this.coreSession = repository == null ? null : openCoreSession(repository.getId());
        coreSessionOwned = true;
        documentFilter = getDocumentFilter();
    }

    /** Constructor called by high-level session from existing core session. */
    public NuxeoCmisService(NuxeoRepository repository, CallContext callContext, CoreSession coreSession) {
        this.repository = repository;
        this.callContext = callContext;
        this.coreSession = coreSession;
        coreSessionOwned = false;
        documentFilter = getDocumentFilter();
    }

    // called in a finally block from dispatcher
    @Override
    public void close() {
        if (coreSession != null && coreSessionOwned) {
            closeCoreSession();
        }
        clearObjectInfos();
    }

    protected CoreSession openCoreSession(String repositoryId) {
        try {
            Repository repository = Framework.getService(RepositoryManager.class).getRepository(repositoryId);
            if (repository == null) {
                throw new CmisRuntimeException("Cannot get repository: " + repositoryId);
            }
            return repository.open();
        } catch (CmisRuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    protected void closeCoreSession() {
        try {
            Repository.close(coreSession);
        } catch (Exception e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    public NuxeoRepository getNuxeoRepository() {
        return repository;
    }

    public CoreSession getCoreSession() {
        return coreSession;
    }

    public BindingsObjectFactory getObjectFactory() {
        return objectFactory;
    }

    public CallContext getCallContext() {
        return callContext;
    }

    /** Gets the filter that hides HiddenInNavigation and deleted objects. */
    protected Filter getDocumentFilter() {
        Filter facetFilter = new FacetFilter(FacetNames.HIDDEN_IN_NAVIGATION, false);
        Filter lcFilter = new LifeCycleFilter(LifeCycleConstants.DELETED_STATE, false);
        return new CompoundFilter(facetFilter, lcFilter);
    }

    protected String getIdFromDocumentRef(DocumentRef ref) throws ClientException {
        if (ref instanceof IdRef) {
            return ((IdRef) ref).value;
        } else {
            return coreSession.getDocument(ref).getId();
        }
    }

    /* This is the only method that does not have a repositoryId / coreSession. */
    @Override
    public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {
        List<NuxeoRepository> repos = NuxeoRepositories.getRepositories();
        List<RepositoryInfo> infos = new ArrayList<RepositoryInfo>(repos.size());
        for (NuxeoRepository repo : repos) {
            String latestChangeLogToken = getLatestChangeLogToken(repo.getId());
            infos.add(repo.getRepositoryInfo(latestChangeLogToken));
        }
        return infos;
    }

    @Override
    public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) {
        String latestChangeLogToken = getLatestChangeLogToken(repositoryId);
        return NuxeoRepositories.getRepository(repositoryId).getRepositoryInfo(latestChangeLogToken);
    }

    @Override
    public TypeDefinition getTypeDefinition(String repositoryId, String typeId, ExtensionsData extension) {
        TypeDefinition type = repository.getTypeDefinition(typeId);
        if (type == null) {
            throw new CmisInvalidArgumentException("No such type: " + typeId);
        }
        // TODO copy only when local binding
        // clone
        return Converter.convert(Converter.convert(type));

    }

    @Override
    public TypeDefinitionList getTypeChildren(String repositoryId, String typeId,
            Boolean includePropertyDefinitions, BigInteger maxItems, BigInteger skipCount,
            ExtensionsData extension) {
        TypeDefinitionList types = repository.getTypeChildren(typeId, includePropertyDefinitions, maxItems,
                skipCount);
        // TODO copy only when local binding
        // clone
        return Converter.convert(Converter.convert(types));
    }

    @Override
    public List<TypeDefinitionContainer> getTypeDescendants(String repositoryId, String typeId, BigInteger depth,
            Boolean includePropertyDefinitions, ExtensionsData extension) {
        int d = depth == null ? DEFAULT_TYPE_LEVELS : depth.intValue();
        List<TypeDefinitionContainer> types = repository.getTypeDescendants(typeId, d, includePropertyDefinitions);
        // clone
        // TODO copy only when local binding
        List<CmisTypeContainer> tmp = new ArrayList<CmisTypeContainer>(types.size());
        Converter.convertTypeContainerList(types, tmp);
        return Converter.convertTypeContainerList(tmp);
    }

    protected DocumentModel getDocumentModel(String id) {
        DocumentRef docRef = new IdRef(id);
        try {
            if (!coreSession.exists(docRef)) {
                throw new CmisObjectNotFoundException(docRef.toString());
            }
            DocumentModel doc = coreSession.getDocument(docRef);
            if (isFilteredOut(doc)) {
                throw new CmisObjectNotFoundException(docRef.toString());
            }
            return doc;
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public NuxeoObjectData getObject(String repositoryId, String objectId, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
        DocumentModel doc = getDocumentModel(objectId);
        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePolicyIds, includeAcl, extension);
        collectObjectInfo(repositoryId, data);
        return data;
    }

    /**
     * Checks if the doc should be ignored because it is "invisible" (deleted,
     * hidden in navigation).
     */
    public boolean isFilteredOut(DocumentModel doc) {
        return !documentFilter.accept(doc);
    }

    protected DocumentModel createDocumentModel(ObjectId folder, TypeDefinition type) {
        DocumentModel doc;
        String typeId = type.getId();
        String nuxeoTypeId = type.getLocalName();
        if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FILE;
        } else if (BaseTypeId.CMIS_FOLDER.value().equals(typeId)) {
            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FOLDER;
        } else if (BaseTypeId.CMIS_RELATIONSHIP.value().equals(typeId)) {
            nuxeoTypeId = NuxeoTypeHelper.NUXEO_RELATION;
        }
        try {
            doc = coreSession.createDocumentModel(nuxeoTypeId);
        } catch (ClientException e) {
            throw new IllegalArgumentException(typeId);
        }
        if (folder != null) {
            DocumentModel parentDoc;
            try {
                parentDoc = coreSession.getDocument(new IdRef(folder.getId()));
            } catch (ClientException e) {
                throw new CmisRuntimeException("Cannot create object", e);
            }
            String pathSegment = nuxeoTypeId; // default path segment based on
                                              // id
            doc.setPathInfo(parentDoc.getPathAsString(), pathSegment);
        }
        return doc;
    }

    // create and save session
    protected NuxeoObjectData createObject(Properties properties, ObjectId folder, BaseTypeId baseType,
            ContentStream contentStream) {
        String typeId;
        Map<String, PropertyData<?>> p;
        PropertyData<?> d;
        TypeDefinition type = null;
        if (properties != null //
                && (p = properties.getProperties()) != null //
                && (d = p.get(PropertyIds.OBJECT_TYPE_ID)) != null) {
            typeId = (String) d.getFirstValue();
            if (baseType == null) {
                type = repository.getTypeDefinition(typeId);
                if (type == null) {
                    throw new IllegalArgumentException(typeId);
                }
                baseType = type.getBaseTypeId();
            }
        } else {
            typeId = null;
        }
        if (typeId == null) {
            switch (baseType) {
            case CMIS_DOCUMENT:
                typeId = "File"; // TODO constant
                break;
            case CMIS_FOLDER:
                typeId = BaseTypeId.CMIS_FOLDER.value();
                break;
            case CMIS_POLICY:
                throw new CmisRuntimeException("Cannot create policy");
            case CMIS_RELATIONSHIP:
                throw new CmisRuntimeException("Cannot create relationship");
            default:
                throw new CmisRuntimeException("No base type");
            }
        }
        if (type == null) {
            type = repository.getTypeDefinition(typeId);
        }
        if (type == null || type.getBaseTypeId() != baseType) {
            throw new CmisInvalidArgumentException(typeId);
        }
        if (type.isCreatable() == Boolean.FALSE) {
            throw new CmisInvalidArgumentException("Not creatable: " + typeId);
        }
        DocumentModel doc = createDocumentModel(folder, type);
        NuxeoObjectData data = new NuxeoObjectData(this, doc);
        updateProperties(data, null, properties, true);
        try {
            if (contentStream != null) {
                if (contentStream.getFileName() == null) {
                    // infer filename from properties
                    PropertyData<?> pd = properties.getProperties().get(PropertyIds.NAME);
                    if (pd != null) {
                        String filename = (String) pd.getFirstValue();
                        contentStream = new ContentStreamImpl(filename, contentStream.getBigLength(),
                                contentStream.getMimeType(), contentStream.getStream());
                    }
                }
                try {
                    NuxeoPropertyData.setContentStream(doc, contentStream, true);
                } catch (CmisContentAlreadyExistsException e) {
                    // cannot happen, overwrite = true
                }
            }
            // set path segment from title
            PathSegmentService pss = Framework.getService(PathSegmentService.class);
            String pathSegment = pss.generatePathSegment(doc);
            Path path = doc.getPath();
            doc.setPathInfo(path == null ? null : path.removeLastSegments(1).toString(), pathSegment);
            data.doc = coreSession.createDocument(doc);
            coreSession.save();
        } catch (IOException e) {
            throw new CmisRuntimeException(e.toString(), e);
        } catch (Exception e) {
            throw new CmisRuntimeException("Cannot create", e);
        }
        return data;
    }

    protected <T> void updateProperties(NuxeoObjectData object, String changeToken, Properties properties,
            boolean creation) {
        TypeDefinition type = object.getTypeDefinition();
        // TODO changeToken
        Map<String, PropertyData<?>> p;
        if (properties == null || (p = properties.getProperties()) == null) {
            return;
        }
        for (Entry<String, PropertyData<?>> en : p.entrySet()) {
            String key = en.getKey();
            PropertyData<?> d = en.getValue();
            setObjectProperty(object, key, d, type, creation);
        }
    }

    protected <T> void updateProperties(NuxeoObjectData object, String changeToken, Map<String, ?> properties,
            TypeDefinition type, boolean creation) {
        // TODO changeToken
        if (properties == null) {
            return;
        }
        for (Entry<String, ?> en : properties.entrySet()) {
            String key = en.getKey();
            Object value = en.getValue();
            @SuppressWarnings("unchecked")
            PropertyDefinition<T> pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
            if (pd == null) {
                throw new CmisRuntimeException("Unknown property: " + key);
            }
            setObjectProperty(object, key, value, pd, creation);
        }
    }

    protected <T> void setObjectProperty(NuxeoObjectData object, String key, PropertyData<T> d, TypeDefinition type,
            boolean creation) {
        @SuppressWarnings("unchecked")
        PropertyDefinition<T> pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
        if (pd == null) {
            throw new CmisRuntimeException("Unknown property: " + key);
        }
        Object value;
        if (d == null) {
            value = null;
        } else if (pd.getCardinality() == Cardinality.SINGLE) {
            value = d.getFirstValue();
        } else {
            value = d.getValues();
        }
        setObjectProperty(object, key, value, pd, creation);
    }

    protected <T> void setObjectProperty(NuxeoObjectData object, String key, Object value, PropertyDefinition<T> pd,
            boolean creation) {
        Updatability updatability = pd.getUpdatability();
        if (updatability == Updatability.READONLY || (updatability == Updatability.ONCREATE && !creation)) {
            // log.error("Read-only property, ignored: " + key);
            return;
        }
        if (PropertyIds.OBJECT_TYPE_ID.equals(key) || PropertyIds.LAST_MODIFICATION_DATE.equals(key)) {
            return;
        }
        // TODO avoid constructing property object just to set value
        NuxeoPropertyDataBase<T> np = (NuxeoPropertyDataBase<T>) NuxeoPropertyData.construct(object, pd,
                callContext);
        np.setValue(value);
    }

    protected NuxeoObjectData setInitialVersioningState(NuxeoObjectData object, VersioningState versioningState) {
        try {
            if (versioningState == null) {
                // default is MAJOR, per spec
                versioningState = VersioningState.MAJOR;
            }
            switch (versioningState) {
            case NONE: // cannot be made non-versionable in Nuxeo
            case CHECKEDOUT:
                break;
            case MINOR:
                object.doc.checkIn(VersioningOption.MINOR, null);
                object.doc.getCoreSession().save();
                break;
            case MAJOR:
                object.doc.checkIn(VersioningOption.MAJOR, null);
                object.doc.getCoreSession().save();
                break;
            }
            return object;
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public String create(String repositoryId, Properties properties, String folderId, ContentStream contentStream,
            VersioningState versioningState, List<String> policies, ExtensionsData extension) {
        // TODO policies
        NuxeoObjectData object = createObject(properties, new ObjectIdImpl(folderId), null, contentStream);
        setInitialVersioningState(object, versioningState);
        return object.getId();
    }

    @Override
    public String createDocument(String repositoryId, Properties properties, String folderId,
            ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
            Acl removeAces, ExtensionsData extension) {
        // TODO policies, addAces, removeAces
        NuxeoObjectData object = createObject(properties, new ObjectIdImpl(folderId), BaseTypeId.CMIS_DOCUMENT,
                contentStream);
        setInitialVersioningState(object, versioningState);
        return object.getId();
    }

    @Override
    public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
            Acl addAces, Acl removeAces, ExtensionsData extension) {
        // TODO policies, addAces, removeAces
        NuxeoObjectData object = createObject(properties, new ObjectIdImpl(folderId), BaseTypeId.CMIS_FOLDER, null);
        return object.getId();
    }

    @Override
    public String createPolicy(String repositoryId, Properties properties, String folderId, List<String> policies,
            Acl addAces, Acl removeAces, ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public String createRelationship(String repositoryId, Properties properties, List<String> policies, Acl addAces,
            Acl removeAces, ExtensionsData extension) {
        NuxeoObjectData object = createObject(properties, null, BaseTypeId.CMIS_RELATIONSHIP, null);
        return object.getId();
    }

    @Override
    public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties,
            String folderId, VersioningState versioningState, List<String> policies, Acl addAces, Acl removeAces,
            ExtensionsData extension) {
        if (folderId == null) {
            // no unfileable objects for now
            throw new CmisInvalidArgumentException("Invalid null folder ID");
        }
        DocumentModel doc = getDocumentModel(sourceId);
        DocumentModel folder = getDocumentModel(folderId);
        try {
            DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
            NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc);
            if (properties != null && properties.getPropertyList() != null
                    && !properties.getPropertyList().isEmpty()) {
                updateProperties(copy, null, properties, false);
                copy.doc = coreSession.saveDocument(copyDoc);
            }
            coreSession.save();
            setInitialVersioningState(copy, versioningState);
            return copy.getId();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    public NuxeoObjectData copy(String sourceId, String targetId, Map<String, ?> properties, TypeDefinition type,
            VersioningState versioningState, List<Policy> policies, List<Ace> addACEs, List<Ace> removeACEs,
            OperationContext context) {
        DocumentModel doc = getDocumentModel(sourceId);
        DocumentModel folder = getDocumentModel(targetId);
        try {
            DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
            NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc, context);
            if (properties != null && !properties.isEmpty()) {
                updateProperties(copy, null, properties, type, false);
                copy.doc = coreSession.saveDocument(copyDoc);
            }
            coreSession.save();
            setInitialVersioningState(copy, versioningState);
            return copy;
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void deleteContentStream(String repositoryId, Holder<String> objectIdHolder,
            Holder<String> changeTokenHolder, ExtensionsData extension) {
        setContentStream(repositoryId, objectIdHolder, Boolean.TRUE, changeTokenHolder, null, extension);
    }

    @Override
    public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
            UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
        if (unfileObjects == UnfileObject.UNFILE) {
            throw new CmisConstraintException("Unfiling not supported");
        }
        if (repository.getRootFolderId().equals(folderId)) {
            throw new CmisInvalidArgumentException("Cannot delete root");
        }
        try {
            DocumentModel doc = getDocumentModel(folderId);
            if (!doc.isFolder()) {
                throw new CmisInvalidArgumentException("Not a folder: " + folderId);
            }
            coreSession.removeDocument(new IdRef(folderId));
            coreSession.save();
            // TODO returning null fails in opencmis 0.1.0 due to
            // org.apache.chemistry.opencmis.client.runtime.PersistentFolderImpl.deleteTree
            return new FailedToDeleteDataImpl();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
        DocumentModel doc = getDocumentModel(objectId);
        return NuxeoObjectData.getAllowableActions(doc, false);
    }

    @Override
    public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
            BigInteger length, ExtensionsData extension) {
        // TODO offset, length
        if (NuxeoObjectData.STREAM_ICON.equals(streamId)) {
            try {
                DocumentModel doc = getDocumentModel(objectId);
                String iconPath;
                try {
                    iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
                } catch (PropertyException e) {
                    iconPath = null;
                }
                InputStream is = NuxeoObjectData.getIconStream(iconPath, callContext);
                if (is == null) {
                    throw new CmisConstraintException("No icon content stream: " + objectId);
                }

                int slash = iconPath.lastIndexOf('/');
                String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1);
                long len;
                try {
                    len = NuxeoObjectData.getStreamLength(is);
                } catch (IOException e) {
                    throw new CmisRuntimeException(e.toString(), e);
                }
                // refetch now-consumed stream
                is = NuxeoObjectData.getIconStream(iconPath, callContext);
                return new ContentStreamImpl(filename, BigInteger.valueOf(len),
                        NuxeoObjectData.getIconMimeType(iconPath), is);
            } catch (ClientException e) {
                throw new CmisRuntimeException(e.toString(), e);
            }
        } else if (streamId == null) {
            DocumentModel doc = getDocumentModel(objectId);
            ContentStream cs = NuxeoPropertyData.getContentStream(doc);
            if (cs != null) {
                return cs;
            }
            throw new CmisConstraintException("No content stream: " + objectId);
        }
        throw new CmisInvalidArgumentException("Invalid stream id: " + streamId);
    }

    @Override
    public List<RenditionData> getRenditions(String repositoryId, String objectId, String renditionFilter,
            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
        DocumentModel doc = getDocumentModel(objectId);
        return NuxeoObjectData.getRenditions(doc, maxItems, skipCount, callContext);
    }

    @Override
    public ObjectData getObjectByPath(String repositoryId, String path, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
        DocumentModel doc;
        try {
            DocumentRef pathRef = new PathRef(path);
            if (coreSession.exists(pathRef)) {
                doc = coreSession.getDocument(pathRef);
                if (isFilteredOut(doc)) {
                    throw new CmisObjectNotFoundException(path);
                }
            } else {
                // Adobe Drive 2 confuses cmis:name and path segment
                // try using sequence of titles
                doc = getObjectByPathOfNames(path);
            }
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
        ObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePolicyIds, includeAcl, extension);
        collectObjectInfo(repositoryId, data);
        return data;
    }

    /**
     * Gets a document given a path built out of dc:title components.
     * <p>
     * Filtered out docs are ignored.
     */
    protected DocumentModel getObjectByPathOfNames(String path)
            throws ClientException, CmisObjectNotFoundException {
        DocumentModel doc = coreSession.getRootDocument();
        for (String name : new Path(path).segments()) {
            String query = String.format("SELECT * FROM Document WHERE ecm:parentId = %s AND dc:title = %s",
                    escapeStringForNXQL(doc.getId()), escapeStringForNXQL(name));
            DocumentModelList docs = coreSession.query(query);
            if (docs.isEmpty()) {
                throw new CmisObjectNotFoundException(path);
            }
            doc = null;
            for (DocumentModel d : docs) {
                if (isFilteredOut(d)) {
                    continue;
                }
                if (doc == null) {
                    doc = d;
                } else {
                    log.warn(String.format("Path '%s' returns several documents for '%s'", path, name));
                    break;
                }
            }
            if (doc == null) {
                throw new CmisObjectNotFoundException(path);
            }
        }
        return doc;
    }

    protected static String REPLACE_QUOTE = Matcher.quoteReplacement("\\'");

    protected static String escapeStringForNXQL(String s) {
        return "'" + s.replaceAll("'", REPLACE_QUOTE) + "'";
    }

    @Override
    public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
        DocumentModel doc = getDocumentModel(objectId);
        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, null, null, null, null, null, null);
        return data.getProperties();
    }

    protected boolean collectObjectInfos = true;

    protected Map<String, ObjectInfo> objectInfos;

    // part of CMIS API and of ObjectInfoHandler
    @Override
    public ObjectInfo getObjectInfo(String repositoryId, String objectId) {
        ObjectInfo info = getObjectInfos().get(objectId);
        if (info != null) {
            return info;
        }
        DocumentModel doc = getDocumentModel(objectId);
        NuxeoObjectData data = new NuxeoObjectData(this, doc, null, Boolean.TRUE, IncludeRelationships.BOTH, null,
                Boolean.TRUE, Boolean.FALSE, null);
        return getObjectInfo(repositoryId, data);
    }

    // AbstractCmisService helper
    @Override
    protected ObjectInfo getObjectInfo(String repositoryId, ObjectData data) {
        ObjectInfo info = getObjectInfos().get(data.getId());
        if (info != null) {
            return info;
        }
        try {
            collectObjectInfos = false;
            info = getObjectInfoIntern(repositoryId, data);
            getObjectInfos().put(info.getId(), info);
        } catch (Exception e) {
            log.error(e.toString(), e);
        } finally {
            collectObjectInfos = true;
        }
        return info;
    }

    protected Map<String, ObjectInfo> getObjectInfos() {
        if (objectInfos == null) {
            objectInfos = new HashMap<String, ObjectInfo>();
        }
        return objectInfos;
    }

    @Override
    public void clearObjectInfos() {
        objectInfos = null;
    }

    protected void collectObjectInfo(String repositoryId, String objectId) {
        if (collectObjectInfos && callContext.isObjectInfoRequired()) {
            getObjectInfo(repositoryId, objectId);
        }
    }

    protected void collectObjectInfo(String repositoryId, ObjectData data) {
        if (collectObjectInfos && callContext.isObjectInfoRequired()) {
            getObjectInfo(repositoryId, data);
        }
    }

    @Override
    public void addObjectInfo(ObjectInfo info) {
        // ObjectInfoHandler, unused here
        throw new UnsupportedOperationException();
    }

    @Override
    public void moveObject(String repositoryId, Holder<String> objectIdHolder, String targetFolderId,
            String sourceFolderId, ExtensionsData extension) {
        String objectId;
        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
            throw new CmisInvalidArgumentException("Missing object ID");
        }
        if (repository.getRootFolderId().equals(objectId)) {
            throw new CmisConstraintException("Cannot move root");
        }
        if (targetFolderId == null) {
            throw new CmisInvalidArgumentException("Missing target folder ID");
        }
        try {
            getDocumentModel(objectId); // check exists and not deleted
            DocumentRef docRef = new IdRef(objectId);
            DocumentModel parent = coreSession.getParentDocument(docRef);
            if (isFilteredOut(parent)) {
                throw new CmisObjectNotFoundException("No parent: " + objectId);
            }
            if (sourceFolderId == null) {
                sourceFolderId = parent.getId();
            } else {
                // check it's the actual parent
                if (!parent.getId().equals(sourceFolderId)) {
                    throw new CmisInvalidArgumentException(
                            "Object " + objectId + " is not filed in " + sourceFolderId);
                }
            }
            DocumentModel target = getDocumentModel(targetFolderId);
            if (!target.isFolder()) {
                throw new CmisInvalidArgumentException("Target is not a folder: " + targetFolderId);
            }
            coreSession.move(docRef, new IdRef(targetFolderId), null);
            coreSession.save();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void setContentStream(String repositoryId, Holder<String> objectIdHolder, Boolean overwriteFlag,
            Holder<String> changeTokenHolder, ContentStream contentStream, ExtensionsData extension) {
        String objectId;
        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
            throw new CmisInvalidArgumentException("Missing object ID");
        }

        DocumentModel doc = getDocumentModel(objectId);
        // TODO test doc checkout state
        try {
            NuxeoPropertyData.setContentStream(doc, contentStream, !Boolean.FALSE.equals(overwriteFlag));
            coreSession.saveDocument(doc);
            coreSession.save();
        } catch (CmisBaseException e) {
            throw e;
        } catch (Exception e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void updateProperties(String repositoryId, Holder<String> objectIdHolder,
            Holder<String> changeTokenHolder, Properties properties, ExtensionsData extension) {
        String objectId;
        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
            throw new CmisInvalidArgumentException("Missing object ID");
        }
        DocumentModel doc = getDocumentModel(objectId);
        NuxeoObjectData object = new NuxeoObjectData(this, doc);
        String changeToken = changeTokenHolder == null ? null : changeTokenHolder.getValue();
        updateProperties(object, changeToken, properties, false);
        try {
            coreSession.saveDocument(doc);
            coreSession.save();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public Acl applyAcl(String repositoryId, String objectId, Acl addAces, Acl removeAces,
            AclPropagation aclPropagation, ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public Acl applyAcl(String repositoryId, String objectId, Acl aces, AclPropagation aclPropagation) {
        throw new CmisNotSupportedException();
    }

    @Override
    public Acl getAcl(String repositoryId, String objectId, Boolean onlyBasicPermissions,
            ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public ObjectList getContentChanges(String repositoryId, Holder<String> changeLogTokenHolder,
            Boolean includeProperties, String filter, Boolean includePolicyIds, Boolean includeAcl,
            BigInteger maxItems, ExtensionsData extension) {
        if (changeLogTokenHolder == null) {
            throw new CmisInvalidArgumentException("Missing change log token holder");

        }
        String changeLogToken = changeLogTokenHolder.getValue();
        long minDate;
        if (changeLogToken == null) {
            minDate = 0;
        } else {
            try {
                minDate = Long.parseLong(changeLogToken);
            } catch (NumberFormatException e) {
                throw new CmisInvalidArgumentException("Invalid change log token");
            }
        }
        try {
            AuditReader reader = Framework.getService(AuditReader.class);
            if (reader == null) {
                throw new CmisRuntimeException("Cannot find audit service");
            }
            int max = maxItems == null ? -1 : maxItems.intValue();
            if (max < 0) {
                max = DEFAULT_CHANGE_LOG_SIZE;
            }
            // TODO XXX repositoryId as well
            Map<String, Object> params = new HashMap<String, Object>();
            String query = "FROM LogEntry log" //
                    + " WHERE log.eventDate >= :minDate" //
                    + "   AND log.eventId IN (:evCreated, :evModified, :evRemoved)" //
                    + " ORDER BY log.eventDate";
            params.put("minDate", new Date(minDate));
            params.put("evCreated", DocumentEventTypes.DOCUMENT_CREATED);
            params.put("evModified", DocumentEventTypes.DOCUMENT_UPDATED);
            params.put("evRemoved", DocumentEventTypes.DOCUMENT_REMOVED);
            List<?> entries = reader.nativeQuery(query, params, 1, max + 1);
            ObjectListImpl ol = new ObjectListImpl();
            boolean hasMoreItems = entries.size() > max;
            ol.setHasMoreItems(Boolean.valueOf(hasMoreItems));
            if (hasMoreItems) {
                entries = entries.subList(0, max);
            }
            List<ObjectData> ods = new ArrayList<ObjectData>(entries.size());
            Date date = null;
            for (Object entry : entries) {
                LogEntry logEntry = (LogEntry) entry;
                ObjectDataImpl od = new ObjectDataImpl();
                ChangeEventInfoDataImpl cei = new ChangeEventInfoDataImpl();
                // change type
                String eventId = logEntry.getEventId();
                ChangeType changeType;
                if (DocumentEventTypes.DOCUMENT_CREATED.equals(eventId)) {
                    changeType = ChangeType.CREATED;
                } else if (DocumentEventTypes.DOCUMENT_UPDATED.equals(eventId)) {
                    changeType = ChangeType.UPDATED;
                } else if (DocumentEventTypes.DOCUMENT_REMOVED.equals(eventId)) {
                    changeType = ChangeType.DELETED;
                } else {
                    continue;
                }
                cei.setChangeType(changeType);
                // change time
                GregorianCalendar changeTime = (GregorianCalendar) Calendar.getInstance();
                date = logEntry.getEventDate();
                changeTime.setTime(date);
                cei.setChangeTime(changeTime);
                od.setChangeEventInfo(cei);
                // properties: id, doc type
                PropertiesImpl properties = new PropertiesImpl();
                properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID, logEntry.getDocUUID()));
                properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_TYPE_ID, logEntry.getDocType()));
                od.setProperties(properties);
                ods.add(od);
            }
            ol.setObjects(ods);
            ol.setNumItems(BigInteger.valueOf(-1));
            String latestChangeLogToken = date == null ? null : String.valueOf(date.getTime());
            changeLogTokenHolder.setValue(latestChangeLogToken);
            return ol;
        } catch (Exception e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    protected String getLatestChangeLogToken(String repositoryId) {
        try {
            AuditReader reader = Framework.getService(AuditReader.class);
            if (reader == null) {
                log.warn("Audit Service not found. latest change log token will be '0'");
                return "0";
                //throw new CmisRuntimeException("Cannot find audit service");
            }
            // TODO XXX repositoryId as well
            Map<String, Object> params = new HashMap<String, Object>();
            String query = "FROM LogEntry log" //
                    + " WHERE log.eventId IN (:evCreated, :evModified, :evRemoved)" //
                    + " ORDER BY log.eventDate DESC";
            params.put("evCreated", DocumentEventTypes.DOCUMENT_CREATED);
            params.put("evModified", DocumentEventTypes.DOCUMENT_UPDATED);
            params.put("evRemoved", DocumentEventTypes.DOCUMENT_REMOVED);
            List<?> entries = reader.nativeQuery(query, params, 1, 1);
            if (entries.size() == 0) {
                return "0";
            }
            LogEntry logEntry = (LogEntry) entries.get(0);
            return String.valueOf(logEntry.getEventDate().getTime());
        } catch (Exception e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public ObjectList query(String repositoryId, String statement, Boolean searchAllVersions,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
        long skip = skipCount == null ? 0 : skipCount.longValue();
        if (skip < 0) {
            skip = 0;
        }
        long max = maxItems == null ? -1 : maxItems.longValue();
        if (max <= 0) {
            max = DEFAULT_QUERY_SIZE;
        }
        long numItems;
        List<ObjectData> list;
        IterableQueryResult res = null;
        try {
            Map<String, PropertyDefinition<?>> typeInfo = new HashMap<String, PropertyDefinition<?>>();
            res = coreSession.queryAndFetch(statement, CMISQLQueryMaker.TYPE, this, typeInfo);

            // convert from Nuxeo to CMIS format
            list = new ArrayList<ObjectData>();
            if (skip > 0) {
                res.skipTo(skip);
            }
            for (Map<String, Serializable> map : res) {
                ObjectDataImpl od = new ObjectDataImpl();

                // properties (kept in list form)
                PropertiesImpl properties = new PropertiesImpl();
                for (Entry<String, Serializable> en : map.entrySet()) {
                    String queryName = en.getKey();
                    PropertyDefinition<?> pd = typeInfo.get(queryName);
                    if (pd == null) {
                        throw new NullPointerException("Cannot get " + queryName);
                    }
                    PropertyData<?> p = createPropertyData(pd, en.getValue(), queryName);
                    properties.addProperty(p);
                }
                od.setProperties(properties);

                // optional stuff
                if (Boolean.TRUE.equals(includeAllowableActions)) {
                    // od.setAllowableActions(allowableActions);
                }
                if (includeRelationships != null && includeRelationships != IncludeRelationships.NONE) {
                    // od.setRelationships(relationships);
                }
                if (renditionFilter != null && renditionFilter.length() > 0) {
                    // od.setRenditions(renditions);
                }

                list.add(od);
                if (list.size() >= max) {
                    break;
                }
            }
            numItems = res.size();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.getMessage(), e);
        } finally {
            if (res != null) {
                res.close();
            }
        }
        ObjectListImpl objList = new ObjectListImpl();
        objList.setObjects(list);
        objList.setNumItems(BigInteger.valueOf(numItems));
        objList.setHasMoreItems(Boolean.valueOf(numItems > skip + list.size()));
        return objList;
    }

    // TODO extract and move to BindingsObjectFactoryImpl
    @SuppressWarnings("unchecked")
    protected <T> PropertyData<T> createPropertyData(PropertyDefinition<T> pd, Serializable value,
            String queryName) {
        AbstractPropertyData<T> p;
        String id = pd.getId();
        if (pd instanceof PropertyIdDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyIdData(id, (String) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyIdData(id, (List<String>) value);
            }
        } else if (pd instanceof PropertyStringDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyStringData(id, (String) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyStringData(id, (List<String>) value);
            }
        } else if (pd instanceof PropertyDateTimeDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyDateTimeData(id,
                        (GregorianCalendar) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyDateTimeData(id,
                        (List<GregorianCalendar>) value);
            }
        } else if (pd instanceof PropertyBooleanDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyBooleanData(id, (Boolean) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyBooleanData(id, (List<Boolean>) value);
            }
        } else if (pd instanceof PropertyIntegerDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyIntegerData(id, (BigInteger) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyIntegerData(id, (List<BigInteger>) value);
            }
        } else if (pd instanceof PropertyDecimalDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyDecimalData(id, (BigDecimal) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyDecimalData(id, (List<BigDecimal>) value);
            }
        } else if (pd instanceof PropertyHtmlDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyHtmlData(id, (String) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyHtmlData(id, (List<String>) value);
            }
        } else if (pd instanceof PropertyUriDefinition) {
            if (pd.getCardinality() == Cardinality.SINGLE) {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyUriData(id, (String) value);
            } else {
                p = (AbstractPropertyData<T>) objectFactory.createPropertyUriData(id, (List<String>) value);
            }
        } else {
            throw new CmisRuntimeException("Unknown property definition: " + pd);
        }
        p.setLocalName(pd.getLocalName());
        p.setDisplayName(pd.getDisplayName());
        p.setQueryName(queryName);
        return p;
    }

    @Override
    public void addObjectToFolder(String repositoryId, String objectId, String folderId, Boolean allVersions,
            ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public void removeObjectFromFolder(String repositoryId, String objectId, String folderId,
            ExtensionsData extension) {
        if (folderId != null) {
            // check it's the actual parent
            try {
                DocumentModel folder = getDocumentModel(folderId);
                DocumentModel parent = coreSession.getParentDocument(new IdRef(objectId));
                if (!parent.getId().equals(folder.getId())) {
                    throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in  " + folderId);
                }
            } catch (ClientException e) {
                throw new CmisRuntimeException(e.toString(), e);
            }
        }
        deleteObject(repositoryId, objectId, Boolean.FALSE, extension);
    }

    @Override
    public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
        if (folderId == null) {
            throw new CmisInvalidArgumentException("Null folderId");
        }
        return getChildrenInternal(repositoryId, folderId, filter, orderBy, includeAllowableActions,
                includeRelationships, renditionFilter, includePathSegment, maxItems, skipCount, false);
    }

    protected ObjectInFolderList getChildrenInternal(String repositoryId, String folderId, String filter,
            String orderBy, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
            boolean folderOnly) {
        ObjectInFolderListImpl result = new ObjectInFolderListImpl();
        List<ObjectInFolderData> list = new ArrayList<ObjectInFolderData>();
        DocumentModel folder = getDocumentModel(folderId);
        if (!folder.isFolder()) {
            return null;
        }
        DocumentModelList children;
        try {
            children = coreSession.getChildren(folder.getRef());
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
        for (DocumentModel child : children) {
            if (isFilteredOut(child)) {
                continue;
            }
            if (folderOnly && !child.isFolder()) {
                continue;
            }
            NuxeoObjectData data = new NuxeoObjectData(this, child, filter, includeAllowableActions,
                    includeRelationships, renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
            ObjectInFolderDataImpl oifd = new ObjectInFolderDataImpl();
            oifd.setObject(data);
            if (Boolean.TRUE.equals(includePathSegment)) {
                oifd.setPathSegment(child.getName());
            }
            list.add(oifd);
            collectObjectInfo(repositoryId, data);
        }
        // TODO orderBy
        BatchedList<ObjectInFolderData> batch = ListUtils.getBatchedList(list, maxItems, skipCount,
                DEFAULT_MAX_CHILDREN);
        result.setObjects(batch.getList());
        result.setHasMoreItems(batch.getHasMoreItems());
        result.setNumItems(batch.getNumItems());
        collectObjectInfo(repositoryId, folderId);
        return result;
    }

    @Override
    public List<ObjectInFolderContainer> getDescendants(String repositoryId, String folderId, BigInteger depth,
            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
        if (folderId == null) {
            throw new CmisInvalidArgumentException("Null folderId");
        }
        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
        if (levels == 0) {
            throw new CmisInvalidArgumentException("Invalid depth: 0");
        }
        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePathSegment, 0, levels, false);
    }

    @Override
    public List<ObjectInFolderContainer> getFolderTree(String repositoryId, String folderId, BigInteger depth,
            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
        if (folderId == null) {
            throw new CmisInvalidArgumentException("Null folderId");
        }
        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
        if (levels == 0) {
            throw new CmisInvalidArgumentException("Invalid depth: 0");
        }
        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
                renditionFilter, includePathSegment, 0, levels, true);
    }

    protected List<ObjectInFolderContainer> getDescendantsInternal(String repositoryId, String folderId,
            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
            String renditionFilter, Boolean includePathSegments, int level, int maxLevels, boolean folderOnly) {
        if (maxLevels != -1 && level >= maxLevels) {
            return null;
        }
        ObjectInFolderList children = getChildrenInternal(repositoryId, folderId, filter, null,
                includeAllowableActions, includeRelationships, renditionFilter, includePathSegments, null, null,
                folderOnly);
        if (children == null) {
            return Collections.emptyList();
        }
        List<ObjectInFolderContainer> res = new ArrayList<ObjectInFolderContainer>(children.getObjects().size());
        for (ObjectInFolderData child : children.getObjects()) {
            ObjectInFolderContainerImpl oifc = new ObjectInFolderContainerImpl();
            oifc.setObject(child);
            // recurse
            List<ObjectInFolderContainer> subChildren = getDescendantsInternal(repositoryId,
                    child.getObject().getId(), filter, includeAllowableActions, includeRelationships,
                    renditionFilter, includePathSegments, level + 1, maxLevels, folderOnly);
            if (subChildren != null) {
                oifc.setChildren(subChildren);
            }
            res.add(oifc);
        }
        return res;
    }

    @Override
    public ObjectData getFolderParent(String repositoryId, String folderId, String filter,
            ExtensionsData extension) {
        List<ObjectParentData> parents = getObjectParentsInternal(repositoryId, folderId, filter, null, null, null,
                Boolean.TRUE, true);
        return parents.isEmpty() ? null : parents.get(0).getObject();
    }

    @Override
    public List<ObjectParentData> getObjectParents(String repositoryId, String objectId, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includeRelativePathSegment, ExtensionsData extension) {
        return getObjectParentsInternal(repositoryId, objectId, filter, includeAllowableActions,
                includeRelationships, renditionFilter, includeRelativePathSegment, false);
    }

    protected List<ObjectParentData> getObjectParentsInternal(String repositoryId, String objectId, String filter,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            Boolean includeRelativePathSegment, boolean folderOnly) {
        String pathSegment;
        String parentId;
        try {
            DocumentRef docRef = new IdRef(objectId);
            if (!coreSession.exists(docRef)) {
                throw new CmisObjectNotFoundException(objectId);
            }
            DocumentModel doc = coreSession.getDocument(docRef);
            if (isFilteredOut(doc)) {
                throw new CmisObjectNotFoundException(objectId);
            }
            if (folderOnly && !doc.isFolder()) {
                throw new CmisInvalidArgumentException("Not a folder: " + objectId);
            }
            pathSegment = doc.getName();
            if (pathSegment == null) { // root
                return Collections.emptyList();
            }
            DocumentRef parentRef = doc.getParentRef();
            if (parentRef == null) { // placeless
                return Collections.emptyList();
            }
            if (!coreSession.exists(parentRef)) { // non-accessible
                return Collections.emptyList();
            }
            DocumentModel parent = coreSession.getDocument(parentRef);
            if (isFilteredOut(parent)) { // filtered out
                return Collections.emptyList();
            }
            parentId = parent.getId();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }

        ObjectData od = getObject(repositoryId, parentId, filter, includeAllowableActions, includeRelationships,
                renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
        ObjectParentDataImpl opd = new ObjectParentDataImpl(od);
        if (!Boolean.FALSE.equals(includeRelativePathSegment)) {
            opd.setRelativePathSegment(pathSegment);
        }
        return Collections.<ObjectParentData>singletonList(opd);
    }

    @Override
    public void applyPolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public List<ObjectData> getAppliedPolicies(String repositoryId, String objectId, String filter,
            ExtensionsData extension) {
        return Collections.emptyList();
    }

    @Override
    public void removePolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
        throw new CmisNotSupportedException();
    }

    @Override
    public ObjectList getObjectRelationships(String repositoryId, String objectId,
            Boolean includeSubRelationshipTypes, RelationshipDirection relationshipDirection, String typeId,
            String filter, Boolean includeAllowableActions, BigInteger maxItems, BigInteger skipCount,
            ExtensionsData extension) {
        ObjectListImpl res = new ObjectListImpl();
        res.setNumItems(BigInteger.valueOf(0));
        res.setHasMoreItems(Boolean.FALSE);
        res.setObjects(Collections.<ObjectData>emptyList());
        return res;
    }

    @Override
    public void checkIn(String repositoryId, Holder<String> objectIdHolder, Boolean major, Properties properties,
            ContentStream contentStream, String checkinComment, List<String> policies, Acl addAces, Acl removeAces,
            ExtensionsData extension) {
        String objectId;
        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
            throw new CmisInvalidArgumentException("Missing object ID");
        }
        VersioningOption option = Boolean.TRUE.equals(major) ? VersioningOption.MAJOR : VersioningOption.MINOR;

        DocumentModel doc = getDocumentModel(objectId);
        if (doc.isVersion() || doc.isProxy()) {
            throw new CmisInvalidArgumentException("Cannot check in non-PWC: " + doc);
        }

        NuxeoObjectData object = new NuxeoObjectData(this, doc);
        updateProperties(object, null, properties, false);
        try {
            coreSession.saveDocument(doc);
            DocumentRef ver = doc.checkIn(option, checkinComment);
            coreSession.save();
            objectIdHolder.setValue(getIdFromDocumentRef(ver));
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    public String checkIn(String objectId, boolean major, Map<String, ?> properties, ObjectType type,
            ContentStream contentStream, String checkinComment) {
        VersioningOption option = major ? VersioningOption.MAJOR : VersioningOption.MINOR;
        DocumentModel doc = getDocumentModel(objectId);
        if (doc.isVersion() || doc.isProxy()) {
            throw new CmisInvalidArgumentException("Cannot check in non-PWC: " + doc);
        }
        NuxeoObjectData object = new NuxeoObjectData(this, doc);
        updateProperties(object, null, properties, type, false);
        try {
            coreSession.saveDocument(doc);
            DocumentRef ver = doc.checkIn(option, checkinComment);
            coreSession.save();
            return getIdFromDocumentRef(ver);
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void checkOut(String repositoryId, Holder<String> objectIdHolder, ExtensionsData extension,
            Holder<Boolean> contentCopiedHolder) {
        String objectId;
        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
            throw new CmisInvalidArgumentException("Missing object ID");
        }
        String pwcId = checkOut(objectId);
        objectIdHolder.setValue(pwcId);
        if (contentCopiedHolder != null) {
            contentCopiedHolder.setValue(Boolean.TRUE);
        }
    }

    public String checkOut(String objectId) {
        DocumentModel doc = getDocumentModel(objectId);
        try {
            if (doc.isProxy()) {
                throw new CmisInvalidArgumentException("Cannot check out non-version: " + objectId);
            }
            // find pwc
            DocumentModel pwc;
            if (doc.isVersion()) {
                pwc = coreSession.getWorkingCopy(doc.getRef());
                if (pwc == null) {
                    // no live document available
                    // TODO do a restore somewhere
                    throw new CmisObjectNotFoundException(objectId);
                }
            } else {
                pwc = doc;
            }
            if (pwc.isCheckedOut()) {
                throw new CmisConstraintException("Already checked out: " + objectId);
            }
            pwc.checkOut();
            coreSession.save();
            return pwc.getId();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void cancelCheckOut(String repositoryId, String objectId, ExtensionsData extension) {
        cancelCheckOut(objectId);
    }

    public void cancelCheckOut(String objectId) {
        DocumentModel doc = getDocumentModel(objectId);
        try {
            if (doc.isVersion() || doc.isProxy() || !doc.isCheckedOut()) {
                throw new CmisInvalidArgumentException("Cannot cancel check out of non-PWC: " + doc);
            }
            DocumentRef docRef = doc.getRef();
            // find last version
            DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
            // restore and keep checked in
            coreSession.restoreToVersion(docRef, verRef, true, true);
            coreSession.save();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
        // TODO implement using query on non-proxy non-version non-checkedin
        throw new CmisNotSupportedException();
    }

    @Override
    public List<ObjectData> getAllVersions(String repositoryId, String objectId, String versionSeriesId,
            String filter, Boolean includeAllowableActions, ExtensionsData extension) {
        DocumentModel doc;
        if (objectId != null) {
            // atompub passes object id
            doc = getDocumentModel(objectId);
        } else if (versionSeriesId != null) {
            // soap passes version series id
            // version series id is (for now) id of live document
            // TODO deal with removal of live doc
            doc = getDocumentModel(versionSeriesId);
        } else {
            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
        }
        try {
            List<DocumentRef> versions = coreSession.getVersionsRefs(doc.getRef());
            List<ObjectData> list = new ArrayList<ObjectData>(versions.size());
            for (DocumentRef verRef : versions) {
                String verId = getIdFromDocumentRef(verRef);
                ObjectData od = getObject(repositoryId, verId, filter, includeAllowableActions,
                        IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
                list.add(od);
            }
            // CoreSession returns them in creation order,
            // CMIS wants them last first
            Collections.reverse(list);
            return list;
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public NuxeoObjectData getObjectOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
            Boolean major, String filter, Boolean includeAllowableActions,
            IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
            Boolean includeAcl, ExtensionsData extension) {
        DocumentModel doc;
        if (objectId != null) {
            // atompub passes object id
            doc = getDocumentModel(objectId);
        } else if (versionSeriesId != null) {
            // soap passes version series id
            // version series id is (for now) id of live document
            // TODO deal with removal of live doc
            doc = getDocumentModel(versionSeriesId);
        } else {
            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
        }
        try {
            if (Boolean.TRUE.equals(major)) {
                // we must list all versions
                List<DocumentModel> versions = coreSession.getVersions(doc.getRef());
                Collections.reverse(versions);
                for (DocumentModel ver : versions) {
                    if (ver.isMajorVersion()) {
                        return getObject(repositoryId, ver.getId(), filter, includeAllowableActions,
                                includeRelationships, renditionFilter, includePolicyIds, includeAcl, null);
                    }
                }
                return null;
            } else {
                DocumentRef verRef = coreSession.getLastDocumentVersionRef(doc.getRef());
                String verId = getIdFromDocumentRef(verRef);
                return getObject(repositoryId, verId, filter, includeAllowableActions, includeRelationships,
                        renditionFilter, includePolicyIds, includeAcl, null);
            }
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public Properties getPropertiesOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
            Boolean major, String filter, ExtensionsData extension) {
        NuxeoObjectData od = getObjectOfLatestVersion(repositoryId, objectId, versionSeriesId, major, filter,
                Boolean.FALSE, IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
        return od == null ? null : od.getProperties();
    }

    @Override
    public void deleteObject(String repositoryId, String objectId, Boolean allVersions, ExtensionsData extension) {
        try {
            DocumentModel doc = getDocumentModel(objectId);
            if (doc.isFolder()) {
                // check that there are no children left
                DocumentModelList docs = coreSession.getChildren(new IdRef(objectId), null, documentFilter, null);
                if (docs.size() > 0) {
                    throw new CmisConstraintException("Cannot delete non-empty folder: " + objectId);
                }
            }
            coreSession.removeDocument(doc.getRef());
            coreSession.save();
        } catch (ClientException e) {
            throw new CmisRuntimeException(e.toString(), e);
        }
    }

    @Override
    public void deleteObjectOrCancelCheckOut(String repositoryId, String objectId, Boolean allVersions,
            ExtensionsData extension) {
        deleteObject(repositoryId, objectId, allVersions, extension);
    }

}