001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.opencmis.impl.server;
020
021import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_CREATED;
022import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_REMOVED;
023import static org.nuxeo.ecm.core.api.event.DocumentEventTypes.DOCUMENT_UPDATED;
024import static org.nuxeo.ecm.core.blob.binary.AbstractBinaryManager.MD5_DIGEST;
025import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_DIGEST_ALGORITHM;
026import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.CONTENT_MD5_HEADER_NAME;
027import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoContentStream.DIGEST_HEADER_NAME;
028import static org.nuxeo.ecm.core.opencmis.impl.server.NuxeoObjectData.REND_STREAM_RENDITION_PREFIX;
029
030import java.io.IOException;
031import java.io.InputStream;
032import java.io.Serializable;
033import java.math.BigInteger;
034import java.util.ArrayList;
035import java.util.Arrays;
036import java.util.Calendar;
037import java.util.Collections;
038import java.util.Date;
039import java.util.GregorianCalendar;
040import java.util.HashMap;
041import java.util.HashSet;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045import java.util.Map.Entry;
046import java.util.Set;
047import java.util.regex.Matcher;
048
049import javax.servlet.http.HttpServletRequest;
050import javax.servlet.http.HttpServletResponse;
051
052import org.apache.chemistry.opencmis.client.api.ObjectId;
053import org.apache.chemistry.opencmis.client.api.OperationContext;
054import org.apache.chemistry.opencmis.client.api.Policy;
055import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
056import org.apache.chemistry.opencmis.commons.BasicPermissions;
057import org.apache.chemistry.opencmis.commons.PropertyIds;
058import org.apache.chemistry.opencmis.commons.data.Ace;
059import org.apache.chemistry.opencmis.commons.data.Acl;
060import org.apache.chemistry.opencmis.commons.data.AllowableActions;
061import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
062import org.apache.chemistry.opencmis.commons.data.ContentStream;
063import org.apache.chemistry.opencmis.commons.data.ExtensionsData;
064import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
065import org.apache.chemistry.opencmis.commons.data.ObjectData;
066import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
067import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
068import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
069import org.apache.chemistry.opencmis.commons.data.ObjectList;
070import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
071import org.apache.chemistry.opencmis.commons.data.Properties;
072import org.apache.chemistry.opencmis.commons.data.PropertyData;
073import org.apache.chemistry.opencmis.commons.data.RenditionData;
074import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
075import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
076import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
077import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
078import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
079import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
080import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
081import org.apache.chemistry.opencmis.commons.enums.Cardinality;
082import org.apache.chemistry.opencmis.commons.enums.ChangeType;
083import org.apache.chemistry.opencmis.commons.enums.CmisVersion;
084import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
085import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
086import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
087import org.apache.chemistry.opencmis.commons.enums.Updatability;
088import org.apache.chemistry.opencmis.commons.enums.VersioningState;
089import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
090import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
091import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
092import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
093import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
094import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
095import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
096import org.apache.chemistry.opencmis.commons.exceptions.CmisVersioningException;
097import org.apache.chemistry.opencmis.commons.impl.WSConverter;
098import org.apache.chemistry.opencmis.commons.impl.dataobjects.AbstractPropertyData;
099import org.apache.chemistry.opencmis.commons.impl.dataobjects.BindingsObjectFactoryImpl;
100import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
101import org.apache.chemistry.opencmis.commons.impl.dataobjects.ChangeEventInfoDataImpl;
102import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
103import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
104import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
105import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
106import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
107import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
108import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
109import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
110import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
111import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
112import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
113import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisTypeContainer;
114import org.apache.chemistry.opencmis.commons.impl.server.AbstractCmisService;
115import org.apache.chemistry.opencmis.commons.server.CallContext;
116import org.apache.chemistry.opencmis.commons.server.CmisService;
117import org.apache.chemistry.opencmis.commons.server.ObjectInfo;
118import org.apache.chemistry.opencmis.commons.server.ProgressControlCmisService;
119import org.apache.chemistry.opencmis.commons.spi.BindingsObjectFactory;
120import org.apache.chemistry.opencmis.commons.spi.Holder;
121import org.apache.chemistry.opencmis.server.support.wrapper.AbstractCmisServiceWrapper;
122import org.apache.chemistry.opencmis.server.support.wrapper.CallContextAwareCmisService;
123import org.apache.commons.lang.StringUtils;
124import org.apache.commons.logging.Log;
125import org.apache.commons.logging.LogFactory;
126import org.elasticsearch.action.search.SearchRequestBuilder;
127import org.elasticsearch.action.search.SearchResponse;
128import org.elasticsearch.action.search.SearchType;
129import org.elasticsearch.index.query.BoolQueryBuilder;
130import org.elasticsearch.index.query.QueryBuilders;
131import org.elasticsearch.search.SearchHit;
132import org.elasticsearch.search.sort.SortOrder;
133import org.nuxeo.common.utils.Path;
134import org.nuxeo.ecm.core.api.Blob;
135import org.nuxeo.ecm.core.api.Blobs;
136import org.nuxeo.ecm.core.api.CoreInstance;
137import org.nuxeo.ecm.core.api.CoreSession;
138import org.nuxeo.ecm.core.api.DocumentModel;
139import org.nuxeo.ecm.core.api.DocumentModelList;
140import org.nuxeo.ecm.core.api.DocumentRef;
141import org.nuxeo.ecm.core.api.Filter;
142import org.nuxeo.ecm.core.api.IdRef;
143import org.nuxeo.ecm.core.api.IterableQueryResult;
144import org.nuxeo.ecm.core.api.LifeCycleConstants;
145import org.nuxeo.ecm.core.api.NuxeoException;
146import org.nuxeo.ecm.core.api.PathRef;
147import org.nuxeo.ecm.core.api.PropertyException;
148import org.nuxeo.ecm.core.api.VersioningOption;
149import org.nuxeo.ecm.core.api.impl.CompoundFilter;
150import org.nuxeo.ecm.core.api.impl.FacetFilter;
151import org.nuxeo.ecm.core.api.impl.LifeCycleFilter;
152import org.nuxeo.ecm.core.api.pathsegment.PathSegmentService;
153import org.nuxeo.ecm.core.api.security.ACE;
154import org.nuxeo.ecm.core.api.security.ACL;
155import org.nuxeo.ecm.core.api.security.ACP;
156import org.nuxeo.ecm.core.api.security.SecurityConstants;
157import org.nuxeo.ecm.core.opencmis.impl.server.versioning.CMISVersioningFilter;
158import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils;
159import org.nuxeo.ecm.core.opencmis.impl.util.ListUtils.BatchedList;
160import org.nuxeo.ecm.core.opencmis.impl.util.SimpleImageInfo;
161import org.nuxeo.ecm.core.opencmis.impl.util.TypeManagerImpl;
162import org.nuxeo.ecm.core.query.QueryParseException;
163import org.nuxeo.ecm.core.query.sql.NXQL;
164import org.nuxeo.ecm.core.schema.FacetNames;
165import org.nuxeo.ecm.core.security.SecurityService;
166import org.nuxeo.ecm.core.storage.sql.coremodel.SQLDocumentVersion.VersionNotModifiableException;
167import org.nuxeo.ecm.platform.audit.api.AuditReader;
168import org.nuxeo.ecm.platform.audit.api.LogEntry;
169import org.nuxeo.ecm.platform.audit.service.DefaultAuditBackend;
170import org.nuxeo.ecm.platform.filemanager.api.FileManager;
171import org.nuxeo.ecm.platform.mimetype.MimetypeNotFoundException;
172import org.nuxeo.ecm.platform.mimetype.interfaces.MimetypeRegistry;
173import org.nuxeo.ecm.platform.mimetype.service.MimetypeRegistryService;
174import org.nuxeo.ecm.platform.rendition.Rendition;
175import org.nuxeo.ecm.platform.rendition.service.RenditionService;
176import org.nuxeo.elasticsearch.ElasticSearchConstants;
177import org.nuxeo.elasticsearch.api.ElasticSearchAdmin;
178import org.nuxeo.elasticsearch.api.ElasticSearchService;
179import org.nuxeo.elasticsearch.api.EsIterableQueryResultImpl;
180import org.nuxeo.elasticsearch.audit.ESAuditBackend;
181import org.nuxeo.elasticsearch.audit.io.AuditEntryJSONReader;
182import org.nuxeo.elasticsearch.query.NxQueryBuilder;
183import org.nuxeo.runtime.api.Framework;
184import org.nuxeo.runtime.services.config.ConfigurationService;
185import org.nuxeo.runtime.transaction.TransactionHelper;
186
187/**
188 * Nuxeo implementation of the CMIS Services, on top of a {@link CoreSession}.
189 */
190public class NuxeoCmisService extends AbstractCmisService
191        implements CallContextAwareCmisService, ProgressControlCmisService {
192
193    public static final int DEFAULT_TYPE_LEVELS = 2;
194
195    public static final int DEFAULT_FOLDER_LEVELS = 2;
196
197    public static final int DEFAULT_CHANGE_LOG_SIZE = 100;
198
199    public static final int MAX_CHANGE_LOG_SIZE = 1000 * 1000;
200
201    public static final int DEFAULT_QUERY_SIZE = 100;
202
203    public static final int DEFAULT_MAX_CHILDREN = 100;
204
205    public static final int DEFAULT_MAX_RELATIONSHIPS = 100;
206
207    public static final String PERMISSION_NOTHING = "Nothing";
208
209    /** Synthetic property for change log entries recording the log entry id. */
210    public static final String NX_CHANGE_LOG_ID = "nuxeo:changeLogId";
211
212    public static final String ES_AUDIT_ID = "id";
213
214    public static final String ES_AUDIT_REPOSITORY_ID = "repositoryId";
215
216    public static final String ES_AUDIT_EVENT_ID = "eventId";
217
218    public static final String ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP =
219            "org.nuxeo.cmis.errorOnCancelCheckOutOfDraftVersion";
220
221    private static final Log log = LogFactory.getLog(NuxeoCmisService.class);
222
223    protected final BindingsObjectFactory objectFactory = new BindingsObjectFactoryImpl();
224
225    protected final NuxeoRepository repository;
226
227    /** When false, we don't own the core session and shouldn't close it. */
228    protected final boolean coreSessionOwned;
229
230    protected CoreSession coreSession;
231
232    /* To avoid refetching it several times per session. */
233    protected String cachedChangeLogToken;
234
235    protected CallContext callContext;
236
237    /** Filter that hides HiddenInNavigation and deleted objects. */
238    protected final Filter documentFilter;
239
240    protected final Set<String> readPermissions;
241
242    protected final Set<String> writePermissions;
243
244    protected final boolean errorOnCancelCheckOutOfDraftVersion;
245
246    public static NuxeoCmisService extractFromCmisService(CmisService service) {
247        if (service == null) {
248            throw new NullPointerException();
249        }
250        for (;;) {
251            if (service instanceof NuxeoCmisService) {
252                return (NuxeoCmisService) service;
253            }
254            if (!(service instanceof AbstractCmisServiceWrapper)) {
255                return null;
256            }
257            service = ((AbstractCmisServiceWrapper) service).getWrappedService();
258        }
259    }
260
261    /**
262     * Constructs a Nuxeo CMIS Service from an existing {@link CoreSession}.
263     *
264     * @param coreSession the session
265     * @since 6.0
266     */
267    public NuxeoCmisService(CoreSession coreSession) {
268        this(coreSession, coreSession.getRepositoryName());
269    }
270
271    /**
272     * Constructs a Nuxeo CMIS Service.
273     *
274     * @param repositoryName the repository name
275     * @since 6.0
276     */
277    public NuxeoCmisService(String repositoryName) {
278        this(null, repositoryName);
279    }
280
281    protected NuxeoCmisService(CoreSession coreSession, String repositoryName) {
282        this.coreSession = coreSession;
283        coreSessionOwned = coreSession == null;
284        repository = getNuxeoRepository(repositoryName);
285        documentFilter = getDocumentFilter();
286        SecurityService securityService = Framework.getService(SecurityService.class);
287        readPermissions = new HashSet<>(Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ)));
288        writePermissions = new HashSet<>(
289                Arrays.asList(securityService.getPermissionsToCheck(SecurityConstants.READ_WRITE)));
290        ConfigurationService configurationService = Framework.getService(ConfigurationService.class);
291        errorOnCancelCheckOutOfDraftVersion = configurationService.isBooleanPropertyTrue(
292                ERROR_ON_CANCEL_CHECK_OUT_OF_DRAFT_VERSION_PROP);
293        CMISVersioningFilter.enable();
294    }
295
296    // called in a finally block from dispatcher
297    @Override
298    public void close() {
299        if (coreSessionOwned && coreSession != null) {
300            coreSession.close();
301            coreSession = null;
302        }
303        clearObjectInfos();
304        CMISVersioningFilter.disable();
305    }
306
307    @Override
308    public Progress beforeServiceCall() {
309        return Progress.CONTINUE;
310    }
311
312    @Override
313    public Progress afterServiceCall() {
314        // check if there is a transaction timeout
315        // if yes, abort and return a 503 (Service Unavailable)
316        if (!TransactionHelper.setTransactionRollbackOnlyIfTimedOut()) {
317            return Progress.CONTINUE;
318        }
319        HttpServletResponse response = (HttpServletResponse) getCallContext().get(CallContext.HTTP_SERVLET_RESPONSE);
320        if (response != null) {
321            try {
322                response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Transaction timeout");
323            } catch (IOException e) {
324                throw new CmisRuntimeException("Failed to set timeout status", e);
325            }
326        }
327        return Progress.STOP;
328    }
329
330    protected static NuxeoRepository getNuxeoRepository(String repositoryName) {
331        if (repositoryName == null) {
332            return null;
333        }
334        return Framework.getService(NuxeoRepositories.class).getRepository(repositoryName);
335    }
336
337    protected static CoreSession openCoreSession(String repositoryName, String username) {
338        if (repositoryName == null) {
339            return null;
340        }
341        return CoreInstance.openCoreSession(repositoryName, username);
342    }
343
344    public NuxeoRepository getNuxeoRepository() {
345        return repository;
346    }
347
348    public CoreSession getCoreSession() {
349        return coreSession;
350    }
351
352    public BindingsObjectFactory getObjectFactory() {
353        return objectFactory;
354    }
355
356    @Override
357    public CallContext getCallContext() {
358        return callContext;
359    }
360
361    protected TypeManagerImpl getTypeManager() {
362        CmisVersion cmisVersion = callContext == null ? CmisVersion.CMIS_1_1 : callContext.getCmisVersion();
363        return repository.getTypeManager(cmisVersion);
364    }
365
366    @Override
367    public void setCallContext(CallContext callContext) {
368        close();
369        this.callContext = callContext;
370        if (coreSessionOwned) {
371            // for non-local binding, the principal is found
372            // in the login stack
373            String username = callContext.getBinding().equals(CallContext.BINDING_LOCAL) ? callContext.getUsername()
374                    : null;
375            coreSession = repository == null ? null : openCoreSession(repository.getId(), username);
376            // re-set CMIS automatic versioning filter as it was disabled at close
377            CMISVersioningFilter.enable();
378        }
379    }
380
381    /** Gets the filter that hides HiddenInNavigation and deleted objects. */
382    protected Filter getDocumentFilter() {
383        Filter facetFilter = new FacetFilter(FacetNames.HIDDEN_IN_NAVIGATION, false);
384        Filter lcFilter = new LifeCycleFilter(LifeCycleConstants.DELETED_STATE, false);
385        return new CompoundFilter(facetFilter, lcFilter);
386    }
387
388    protected String getIdFromDocumentRef(DocumentRef ref) {
389        if (ref instanceof IdRef) {
390            return ((IdRef) ref).value;
391        } else {
392            return coreSession.getDocument(ref).getId();
393        }
394    }
395
396    protected void save() {
397        coreSession.save();
398        cachedChangeLogToken = null;
399    }
400
401    /* This is the only method that does not have a repositoryId / coreSession. */
402    @Override
403    public List<RepositoryInfo> getRepositoryInfos(ExtensionsData extension) {
404        List<NuxeoRepository> repos = Framework.getService(NuxeoRepositories.class).getRepositories();
405        List<RepositoryInfo> infos = new ArrayList<>(repos.size());
406        for (NuxeoRepository repo : repos) {
407            String latestChangeLogToken = getLatestChangeLogToken(repo.getId());
408            infos.add(repo.getRepositoryInfo(latestChangeLogToken, callContext));
409        }
410        return infos;
411    }
412
413    @Override
414    public RepositoryInfo getRepositoryInfo(String repositoryId, ExtensionsData extension) {
415        String latestChangeLogToken;
416        if (cachedChangeLogToken != null) {
417            latestChangeLogToken = cachedChangeLogToken;
418        } else {
419            latestChangeLogToken = getLatestChangeLogToken(repositoryId);
420            cachedChangeLogToken = latestChangeLogToken;
421        }
422        NuxeoRepository repository = getNuxeoRepository(repositoryId);
423        return repository.getRepositoryInfo(latestChangeLogToken, callContext);
424    }
425
426    @Override
427    public TypeDefinition getTypeDefinition(String repositoryId, String typeId, ExtensionsData extension) {
428        TypeDefinition type = getTypeManager().getTypeDefinition(typeId);
429        if (type == null) {
430            throw new CmisInvalidArgumentException("No such type: " + typeId);
431        }
432        // TODO copy only when local binding
433        // clone
434        return WSConverter.convert(WSConverter.convert(type));
435
436    }
437
438    @Override
439    public TypeDefinitionList getTypeChildren(String repositoryId, String typeId, Boolean includePropertyDefinitions,
440            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
441        TypeDefinitionList types = getTypeManager().getTypeChildren(typeId, includePropertyDefinitions, maxItems,
442                skipCount);
443        // TODO copy only when local binding
444        // clone
445        return WSConverter.convert(WSConverter.convert(types));
446    }
447
448    @Override
449    public List<TypeDefinitionContainer> getTypeDescendants(String repositoryId, String typeId, BigInteger depth,
450            Boolean includePropertyDefinitions, ExtensionsData extension) {
451        int d = depth == null ? DEFAULT_TYPE_LEVELS : depth.intValue();
452        List<TypeDefinitionContainer> types = getTypeManager().getTypeDescendants(typeId, d,
453                includePropertyDefinitions);
454        // clone
455        // TODO copy only when local binding
456        List<CmisTypeContainer> tmp = new ArrayList<>(types.size());
457        WSConverter.convertTypeContainerList(types, tmp);
458        return WSConverter.convertTypeContainerList(tmp);
459    }
460
461    protected DocumentModel getDocumentModel(String id) {
462        DocumentRef docRef = new IdRef(id);
463        if (!coreSession.exists(docRef)) {
464            throw new CmisObjectNotFoundException(docRef.toString());
465        }
466        DocumentModel doc = coreSession.getDocument(docRef);
467        if (isFilteredOut(doc)) {
468            throw new CmisObjectNotFoundException(docRef.toString());
469        }
470        return doc;
471    }
472
473    @Override
474    public NuxeoObjectData getObject(String repositoryId, String objectId, String filter,
475            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
476            Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
477        DocumentModel doc = getDocumentModel(objectId);
478        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
479                renditionFilter, includePolicyIds, includeAcl, extension);
480        collectObjectInfo(repositoryId, objectId);
481        return data;
482    }
483
484    /**
485     * Checks if the doc should be ignored because it is "invisible" (deleted, hidden in navigation).
486     */
487    public boolean isFilteredOut(DocumentModel doc) {
488        // don't filter out relations even though they may be HiddenInNavigation
489        if (NuxeoTypeHelper.getBaseTypeId(doc).equals(BaseTypeId.CMIS_RELATIONSHIP)) {
490            return false;
491        }
492        return !documentFilter.accept(doc);
493    }
494
495    /** Creates bare unsaved document model. */
496    protected DocumentModel createDocumentModel(ObjectId folder, TypeDefinition type) {
497        DocumentModel doc;
498        String typeId = type.getId();
499        String nuxeoTypeId = type.getLocalName();
500        if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
501            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FILE;
502        } else if (BaseTypeId.CMIS_FOLDER.value().equals(typeId)) {
503            nuxeoTypeId = NuxeoTypeHelper.NUXEO_FOLDER;
504        } else if (BaseTypeId.CMIS_RELATIONSHIP.value().equals(typeId)) {
505            nuxeoTypeId = NuxeoTypeHelper.NUXEO_RELATION_DEFAULT;
506        }
507        doc = coreSession.createDocumentModel(nuxeoTypeId);
508        if (folder != null) {
509            DocumentRef parentRef = new IdRef(folder.getId());
510            if (!coreSession.exists(parentRef)) {
511                throw new CmisInvalidArgumentException(parentRef.toString());
512            }
513            DocumentModel parentDoc = coreSession.getDocument(parentRef);
514            String pathSegment = nuxeoTypeId; // default path segment based on id
515            doc.setPathInfo(parentDoc.getPathAsString(), pathSegment);
516        }
517        return doc;
518    }
519
520    /** Creates and save document model. */
521    protected DocumentModel createDocumentModel(ObjectId folder, ContentStream contentStream, String name) {
522        FileManager fileManager = Framework.getLocalService(FileManager.class);
523        MimetypeRegistryService mtr = (MimetypeRegistryService) Framework.getLocalService(MimetypeRegistry.class);
524        if (fileManager == null || mtr == null || name == null || folder == null) {
525            return null;
526        }
527
528        DocumentModel parent = coreSession.getDocument(new IdRef(folder.getId()));
529        String path = parent.getPathAsString();
530
531        Blob blob;
532        if (contentStream == null) {
533            String mimeType;
534            try {
535                mimeType = mtr.getMimetypeFromFilename(name);
536            } catch (MimetypeNotFoundException e) {
537                mimeType = MimetypeRegistry.DEFAULT_MIMETYPE;
538            }
539            blob = Blobs.createBlob("", mimeType, null, name);
540        } else {
541            try {
542                blob = NuxeoPropertyData.getPersistentBlob(contentStream, null);
543            } catch (IOException e) {
544                throw new CmisRuntimeException(e.toString(), e);
545            }
546        }
547
548        try {
549            return fileManager.createDocumentFromBlob(coreSession, blob, path, false, name);
550        } catch (IOException e) {
551            throw new CmisRuntimeException(e.toString(), e);
552        }
553    }
554
555    // create and save session
556    protected NuxeoObjectData createObject(String repositoryId, Properties properties, ObjectId folder,
557            BaseTypeId baseType, ContentStream contentStream) {
558        String typeId;
559        Map<String, PropertyData<?>> p;
560        PropertyData<?> d;
561        TypeDefinition type = null;
562        if (properties != null //
563                && (p = properties.getProperties()) != null //
564                && (d = p.get(PropertyIds.OBJECT_TYPE_ID)) != null) {
565            typeId = (String) d.getFirstValue();
566            if (baseType == null) {
567                type = getTypeManager().getTypeDefinition(typeId);
568                if (type == null) {
569                    throw new IllegalArgumentException(typeId);
570                }
571                baseType = type.getBaseTypeId();
572            }
573        } else {
574            typeId = null;
575        }
576        if (typeId == null) {
577            switch (baseType) {
578            case CMIS_DOCUMENT:
579                typeId = BaseTypeId.CMIS_DOCUMENT.value();
580                break;
581            case CMIS_FOLDER:
582                typeId = BaseTypeId.CMIS_FOLDER.value();
583                break;
584            case CMIS_POLICY:
585                throw new CmisRuntimeException("Cannot create policy");
586            case CMIS_RELATIONSHIP:
587                throw new CmisRuntimeException("Cannot create relationship");
588            default:
589                throw new CmisRuntimeException("No base type");
590            }
591        }
592        if (type == null) {
593            type = getTypeManager().getTypeDefinition(typeId);
594        }
595        if (type == null || type.getBaseTypeId() != baseType) {
596            throw new CmisInvalidArgumentException(typeId);
597        }
598        if (type.isCreatable() == Boolean.FALSE) {
599            throw new CmisInvalidArgumentException("Not creatable: " + typeId);
600        }
601
602        // name from properties
603        PropertyData<?> npd = properties.getProperties().get(PropertyIds.NAME);
604        String name = npd == null ? null : (String) npd.getFirstValue();
605        if (StringUtils.isBlank(name)) {
606            throw new CmisConstraintException("The mandatory property " + PropertyIds.NAME + " is missing");
607        }
608
609        // content stream filename default
610        if (contentStream != null && StringUtils.isBlank(contentStream.getFileName())) {
611            // infer filename from name property
612            contentStream = new ContentStreamImpl(name, contentStream.getBigLength(),
613                    contentStream.getMimeType().trim(), contentStream.getStream());
614        }
615
616        DocumentModel doc = null;
617        if (BaseTypeId.CMIS_DOCUMENT.value().equals(typeId)) {
618            doc = createDocumentModel(folder, contentStream, name);
619        }
620        boolean created = doc != null;
621        if (!created) {
622            doc = createDocumentModel(folder, type);
623        }
624
625        NuxeoObjectData data = new NuxeoObjectData(this, doc);
626        updateProperties(data, properties, true);
627        boolean setContentStream = !created && contentStream != null;
628        if (setContentStream) {
629            try {
630                NuxeoPropertyData.setContentStream(doc, contentStream, true);
631            } catch (CmisContentAlreadyExistsException e) {
632                // cannot happen, overwrite = true
633            } catch (IOException e) {
634                throw new CmisRuntimeException(e.toString(), e);
635            }
636        }
637        if (!created) {
638            // set path segment from properties (name/title)
639            PathSegmentService pss = Framework.getLocalService(PathSegmentService.class);
640            String pathSegment = pss.generatePathSegment(doc);
641            Path path = doc.getPath();
642            doc.setPathInfo(path == null ? null : path.removeLastSegments(1).toString(), pathSegment);
643            doc = coreSession.createDocument(doc);
644        } else {
645            doc = coreSession.saveDocument(doc);
646        }
647        if (setContentStream) {
648            NuxeoPropertyData.validateBlobDigest(doc, callContext);
649        }
650        data.doc = doc;
651        save();
652        collectObjectInfo(repositoryId, data.getId());
653        return data;
654    }
655
656    protected <T> void updateProperties(NuxeoObjectData object, Properties properties, boolean creation) {
657        List<TypeDefinition> types = object.getTypeDefinitions();
658        Map<String, PropertyData<?>> p;
659        if (properties == null || (p = properties.getProperties()) == null) {
660            return;
661        }
662        for (Entry<String, PropertyData<?>> en : p.entrySet()) {
663            String key = en.getKey();
664            PropertyData<?> d = en.getValue();
665            setObjectProperty(object, key, d, types, creation);
666        }
667    }
668
669    protected <T> void updateProperties(NuxeoObjectData object, Map<String, ?> properties, TypeDefinition type,
670            boolean creation) {
671        if (properties == null) {
672            return;
673        }
674        for (Entry<String, ?> en : properties.entrySet()) {
675            String key = en.getKey();
676            Object value = en.getValue();
677            @SuppressWarnings("unchecked")
678            PropertyDefinition<T> pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
679            if (pd == null) {
680                throw new CmisRuntimeException("Unknown property: " + key);
681            }
682            setObjectProperty(object, key, value, pd, creation);
683        }
684    }
685
686    protected <T> void setObjectProperty(NuxeoObjectData object, String key, PropertyData<T> d,
687            List<TypeDefinition> types, boolean creation) {
688        PropertyDefinition<T> pd = null;
689        for (TypeDefinition type : types) {
690            pd = (PropertyDefinition<T>) type.getPropertyDefinitions().get(key);
691            if (pd != null) {
692                break;
693            }
694        }
695        if (pd == null) {
696            throw new CmisRuntimeException("Unknown property: " + key);
697        }
698        Object value;
699        if (d == null) {
700            value = null;
701        } else if (pd.getCardinality() == Cardinality.SINGLE) {
702            value = d.getFirstValue();
703        } else {
704            value = d.getValues();
705        }
706        setObjectProperty(object, key, value, pd, creation);
707    }
708
709    protected <T> void setObjectProperty(NuxeoObjectData object, String key, Object value, PropertyDefinition<T> pd,
710            boolean creation) {
711        Updatability updatability = pd.getUpdatability();
712        if (updatability == Updatability.READONLY || (updatability == Updatability.ONCREATE && !creation)) {
713            // log.error("Read-only property, ignored: " + key);
714            return;
715        }
716        if (PropertyIds.OBJECT_TYPE_ID.equals(key) || PropertyIds.LAST_MODIFICATION_DATE.equals(key)) {
717            return;
718        }
719        // TODO avoid constructing property object just to set value
720        NuxeoPropertyDataBase<T> np = (NuxeoPropertyDataBase<T>) NuxeoPropertyData.construct(object, pd, callContext);
721        np.setValue(value);
722    }
723
724    /** Sets initial versioning state and returns its id. */
725    protected String setInitialVersioningState(NuxeoObjectData object, VersioningState versioningState) {
726        if (versioningState == null) {
727            // default is MAJOR, per spec
728            versioningState = VersioningState.MAJOR;
729        }
730        String id;
731        DocumentRef ref = null;
732        switch (versioningState) {
733        case NONE: // cannot be made non-versionable in Nuxeo
734        case CHECKEDOUT:
735            object.doc.setLock();
736            save();
737            id = object.getId();
738            break;
739        case MINOR:
740            ref = object.doc.checkIn(VersioningOption.MINOR, null);
741            save();
742            // id = ref.toString();
743            id = object.getId();
744            break;
745        case MAJOR:
746            ref = object.doc.checkIn(VersioningOption.MAJOR, null);
747            save();
748            // id = ref.toString();
749            id = object.getId();
750            break;
751        default:
752            throw new AssertionError(versioningState);
753        }
754        return id;
755    }
756
757    @Override
758    public String create(String repositoryId, Properties properties, String folderId, ContentStream contentStream,
759            VersioningState versioningState, List<String> policies, ExtensionsData extension) {
760        // TODO policies
761        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId), null,
762                contentStream);
763        return setInitialVersioningState(object, versioningState);
764    }
765
766    @Override
767    public String createDocument(String repositoryId, Properties properties, String folderId,
768            ContentStream contentStream, VersioningState versioningState, List<String> policies, Acl addAces,
769            Acl removeAces, ExtensionsData extension) {
770        // TODO policies, addAces, removeAces
771        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
772                BaseTypeId.CMIS_DOCUMENT, contentStream);
773        return setInitialVersioningState(object, versioningState);
774    }
775
776    @Override
777    public String createFolder(String repositoryId, Properties properties, String folderId, List<String> policies,
778            Acl addAces, Acl removeAces, ExtensionsData extension) {
779        // TODO policies, addAces, removeAces
780        NuxeoObjectData object = createObject(repositoryId, properties, new ObjectIdImpl(folderId),
781                BaseTypeId.CMIS_FOLDER, null);
782        return object.getId();
783    }
784
785    @Override
786    public String createPolicy(String repositoryId, Properties properties, String folderId, List<String> policies,
787            Acl addAces, Acl removeAces, ExtensionsData extension) {
788        throw new CmisNotSupportedException();
789    }
790
791    @Override
792    public String createRelationship(String repositoryId, Properties properties, List<String> policies, Acl addAces,
793            Acl removeAces, ExtensionsData extension) {
794        NuxeoObjectData object = createObject(repositoryId, properties, null, BaseTypeId.CMIS_RELATIONSHIP, null);
795        return object.getId();
796    }
797
798    @Override
799    public String createDocumentFromSource(String repositoryId, String sourceId, Properties properties, String folderId,
800            VersioningState versioningState, List<String> policies, Acl addAces, Acl removeAces,
801            ExtensionsData extension) {
802        if (folderId == null) {
803            // no unfileable objects for now
804            throw new CmisInvalidArgumentException("Invalid null folder ID");
805        }
806        DocumentModel doc = getDocumentModel(sourceId);
807        DocumentModel folder = getDocumentModel(folderId);
808        DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
809        NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc);
810        if (properties != null && properties.getPropertyList() != null && !properties.getPropertyList().isEmpty()) {
811            updateProperties(copy, properties, false);
812            copy.doc = coreSession.saveDocument(copyDoc);
813        }
814        save();
815        return setInitialVersioningState(copy, versioningState);
816    }
817
818    public NuxeoObjectData copy(String sourceId, String targetId, Map<String, ?> properties, TypeDefinition type,
819            VersioningState versioningState, List<Policy> policies, List<Ace> addACEs, List<Ace> removeACEs,
820            OperationContext context) {
821        DocumentModel doc = getDocumentModel(sourceId);
822        DocumentModel folder = getDocumentModel(targetId);
823        DocumentModel copyDoc = coreSession.copy(doc.getRef(), folder.getRef(), null);
824        NuxeoObjectData copy = new NuxeoObjectData(this, copyDoc, context);
825        if (properties != null && !properties.isEmpty()) {
826            updateProperties(copy, properties, type, false);
827            copy.doc = coreSession.saveDocument(copyDoc);
828        }
829        save();
830        String id = setInitialVersioningState(copy, versioningState);
831        NuxeoObjectData res;
832        if (id.equals(copy.getId())) {
833            res = copy;
834        } else {
835            // return the version
836            res = new NuxeoObjectData(this, getDocumentModel(id));
837        }
838        return res;
839    }
840
841    @Override
842    public void deleteContentStream(String repositoryId, Holder<String> objectIdHolder,
843            Holder<String> changeTokenHolder, ExtensionsData extension) {
844        setContentStream(repositoryId, objectIdHolder, Boolean.TRUE, changeTokenHolder, null, extension);
845    }
846
847    @Override
848    public FailedToDeleteData deleteTree(String repositoryId, String folderId, Boolean allVersions,
849            UnfileObject unfileObjects, Boolean continueOnFailure, ExtensionsData extension) {
850        if (unfileObjects == UnfileObject.UNFILE) {
851            throw new CmisConstraintException("Unfiling not supported");
852        }
853        if (repository.getRootFolderId().equals(folderId)) {
854            throw new CmisInvalidArgumentException("Cannot delete root");
855        }
856        DocumentModel doc = getDocumentModel(folderId);
857        if (!doc.isFolder()) {
858            throw new CmisInvalidArgumentException("Not a folder: " + folderId);
859        }
860        coreSession.removeDocument(new IdRef(folderId));
861        save();
862        // TODO returning null fails in opencmis 0.1.0 due to
863        // org.apache.chemistry.opencmis.client.runtime.PersistentFolderImpl.deleteTree
864        return new FailedToDeleteDataImpl();
865    }
866
867    @Override
868    public AllowableActions getAllowableActions(String repositoryId, String objectId, ExtensionsData extension) {
869        DocumentModel doc = getDocumentModel(objectId);
870        return NuxeoObjectData.getAllowableActions(doc, false);
871    }
872
873    @Override
874    public ContentStream getContentStream(String repositoryId, String objectId, String streamId, BigInteger offset,
875            BigInteger length, ExtensionsData extension) {
876        // TODO offset, length
877        ContentStream cs = null;
878        HttpServletRequest request = (HttpServletRequest) callContext.get(CallContext.HTTP_SERVLET_REQUEST);
879        if (streamId == null) {
880            DocumentModel doc = getDocumentModel(objectId);
881            cs = NuxeoPropertyData.getContentStream(doc, request);
882            if (cs == null) {
883                throw new CmisConstraintException("No content stream: " + objectId);
884            }
885        } else {
886            String renditionName = streamId.replaceAll("^" + REND_STREAM_RENDITION_PREFIX, "");
887            cs = getRenditionServiceStream(objectId, renditionName);
888            if (cs == null) {
889                throw new CmisInvalidArgumentException("Invalid stream id: " + streamId);
890            }
891        }
892        if (cs instanceof NuxeoContentStream) {
893            NuxeoContentStream ncs = (NuxeoContentStream) cs;
894            Blob blob = ncs.blob;
895            String blobDigestAlgorithm = blob.getDigestAlgorithm();
896            if (MD5_DIGEST.equals(blobDigestAlgorithm)
897                    && NuxeoContentStream.hasWantDigestRequestHeader(request, CONTENT_MD5_DIGEST_ALGORITHM)) {
898                setResponseHeader(CONTENT_MD5_HEADER_NAME, blob, callContext);
899            }
900            if (NuxeoContentStream.hasWantDigestRequestHeader(request, blobDigestAlgorithm)) {
901                setResponseHeader(DIGEST_HEADER_NAME, blob, callContext);
902            }
903        }
904        return cs;
905    }
906
907    protected void setResponseHeader(String headerName, Blob blob, CallContext callContext) {
908        String digest = NuxeoPropertyData.transcodeHexToBase64(blob.getDigest());
909        HttpServletResponse response = (HttpServletResponse) callContext.get(CallContext.HTTP_SERVLET_RESPONSE);
910        if (DIGEST_HEADER_NAME.equalsIgnoreCase(headerName)) {
911            digest = blob.getDigestAlgorithm() + "=" + digest;
912        }
913        response.setHeader(headerName, digest);
914    }
915
916    /**
917     * @deprecated since 7.3. The thumbnail is now a default rendition, see NXP-16662.
918     */
919    @Deprecated
920    protected ContentStream getIconRenditionStream(String objectId) {
921        DocumentModel doc = getDocumentModel(objectId);
922        String iconPath;
923        try {
924            iconPath = (String) doc.getPropertyValue(NuxeoTypeHelper.NX_ICON);
925        } catch (PropertyException e) {
926            iconPath = null;
927        }
928        InputStream is = NuxeoObjectData.getIconStream(iconPath, callContext);
929        if (is == null) {
930            throw new CmisConstraintException("No icon content stream: " + objectId);
931        }
932
933        int slash = iconPath.lastIndexOf('/');
934        String filename = slash == -1 ? iconPath : iconPath.substring(slash + 1);
935
936        SimpleImageInfo info;
937        try {
938            info = new SimpleImageInfo(is);
939        } catch (IOException e) {
940            throw new CmisRuntimeException(e.toString(), e);
941        }
942        // refetch now-consumed stream
943        is = NuxeoObjectData.getIconStream(iconPath, callContext);
944        return new ContentStreamImpl(filename, BigInteger.valueOf(info.getLength()), info.getMimeType(), is);
945    }
946
947    protected ContentStream getRenditionServiceStream(String objectId, String renditionName) {
948        RenditionService renditionService = Framework.getLocalService(RenditionService.class);
949        DocumentModel doc = getDocumentModel(objectId);
950        Rendition rendition = renditionService.getRendition(doc, renditionName);
951        if (rendition == null) {
952            return null;
953        }
954        Blob blob = rendition.getBlob();
955        if (blob == null) {
956            return null;
957        }
958
959        Calendar modificationDate = rendition.getModificationDate();
960        GregorianCalendar lastModified = (modificationDate instanceof GregorianCalendar)
961                ? (GregorianCalendar) modificationDate : null;
962        HttpServletRequest request = (HttpServletRequest) getCallContext().get(CallContext.HTTP_SERVLET_REQUEST);
963        return NuxeoContentStream.create(doc, null, blob, "cmisRendition",
964                Collections.singletonMap("rendition", renditionName), lastModified, request);
965    }
966
967    @Override
968    public List<RenditionData> getRenditions(String repositoryId, String objectId, String renditionFilter,
969            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
970        if (!NuxeoObjectData.needsRenditions(renditionFilter)) {
971            return Collections.emptyList();
972        }
973        DocumentModel doc = getDocumentModel(objectId);
974        return NuxeoObjectData.getRenditions(doc, renditionFilter, maxItems, skipCount, callContext);
975    }
976
977    @Override
978    public ObjectData getObjectByPath(String repositoryId, String path, String filter, Boolean includeAllowableActions,
979            IncludeRelationships includeRelationships, String renditionFilter, Boolean includePolicyIds,
980            Boolean includeAcl, ExtensionsData extension) {
981        DocumentModel doc;
982        DocumentRef pathRef = new PathRef(path);
983        if (coreSession.exists(pathRef)) {
984            doc = coreSession.getDocument(pathRef);
985            if (isFilteredOut(doc)) {
986                throw new CmisObjectNotFoundException(path);
987            }
988        } else {
989            // Adobe Drive 2 confuses cmis:name and path segment
990            // try using sequence of titles
991            doc = getObjectByPathOfNames(path);
992        }
993        ObjectData data = new NuxeoObjectData(this, doc, filter, includeAllowableActions, includeRelationships,
994                renditionFilter, includePolicyIds, includeAcl, extension);
995        collectObjectInfo(repositoryId, data.getId());
996        return data;
997    }
998
999    /**
1000     * Gets a document given a path built out of dc:title components.
1001     * <p>
1002     * Filtered out docs are ignored.
1003     */
1004    protected DocumentModel getObjectByPathOfNames(String path) throws CmisObjectNotFoundException {
1005        DocumentModel doc = coreSession.getRootDocument();
1006        for (String name : new Path(path).segments()) {
1007            String query = String.format("SELECT * FROM Document WHERE " + NXQL.ECM_PARENTID + " = %s AND "
1008                    + NuxeoTypeHelper.NX_DC_TITLE + " = %s", escapeStringForNXQL(doc.getId()),
1009                    escapeStringForNXQL(name));
1010            query = addProxyClause(query);
1011            DocumentModelList docs = coreSession.query(query);
1012            if (docs.isEmpty()) {
1013                throw new CmisObjectNotFoundException(path);
1014            }
1015            doc = null;
1016            for (DocumentModel d : docs) {
1017                if (isFilteredOut(d)) {
1018                    continue;
1019                }
1020                if (doc == null) {
1021                    doc = d;
1022                } else {
1023                    log.warn(String.format("Path '%s' returns several documents for '%s'", path, name));
1024                    break;
1025                }
1026            }
1027            if (doc == null) {
1028                throw new CmisObjectNotFoundException(path);
1029            }
1030        }
1031        return doc;
1032    }
1033
1034    protected static String REPLACE_QUOTE = Matcher.quoteReplacement("\\'");
1035
1036    protected static String escapeStringForNXQL(String s) {
1037        return "'" + s.replaceAll("'", REPLACE_QUOTE) + "'";
1038    }
1039
1040    @Override
1041    public Properties getProperties(String repositoryId, String objectId, String filter, ExtensionsData extension) {
1042        DocumentModel doc = getDocumentModel(objectId);
1043        NuxeoObjectData data = new NuxeoObjectData(this, doc, filter, null, null, null, null, null, null);
1044        return data.getProperties();
1045    }
1046
1047    protected boolean collectObjectInfos = true;
1048
1049    protected Map<String, ObjectInfo> objectInfos;
1050
1051    // part of CMIS API and of ObjectInfoHandler
1052    @Override
1053    public ObjectInfo getObjectInfo(String repositoryId, String objectId) {
1054        ObjectInfo info = getObjectInfo().get(objectId);
1055        if (info != null) {
1056            return info;
1057        }
1058        DocumentModel doc = getDocumentModel(objectId);
1059        NuxeoObjectData data = new NuxeoObjectData(this, doc, null, Boolean.TRUE, IncludeRelationships.BOTH, "*",
1060                Boolean.TRUE, Boolean.TRUE, null);
1061        return getObjectInfo(repositoryId, data);
1062    }
1063
1064    // AbstractCmisService helper
1065    protected ObjectInfo getObjectInfo(String repositoryId, ObjectData data) {
1066        ObjectInfo info = getObjectInfo().get(data.getId());
1067        if (info != null) {
1068            return info;
1069        }
1070        try {
1071            collectObjectInfos = false;
1072            info = getObjectInfoIntern(repositoryId, data);
1073            getObjectInfo().put(info.getId(), info);
1074        } finally {
1075            collectObjectInfos = true;
1076        }
1077        return info;
1078    }
1079
1080    protected Map<String, ObjectInfo> getObjectInfo() {
1081        if (objectInfos == null) {
1082            objectInfos = new HashMap<>();
1083        }
1084        return objectInfos;
1085    }
1086
1087    @Override
1088    public void clearObjectInfos() {
1089        objectInfos = null;
1090    }
1091
1092    protected void collectObjectInfo(String repositoryId, String objectId) {
1093        if (collectObjectInfos && callContext.isObjectInfoRequired()) {
1094            getObjectInfo(repositoryId, objectId);
1095        }
1096    }
1097
1098    @Override
1099    public void addObjectInfo(ObjectInfo info) {
1100        // ObjectInfoHandler, unused here
1101        throw new UnsupportedOperationException();
1102    }
1103
1104    @Override
1105    public void moveObject(String repositoryId, Holder<String> objectIdHolder, String targetFolderId,
1106            String sourceFolderId, ExtensionsData extension) {
1107        String objectId;
1108        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1109            throw new CmisInvalidArgumentException("Missing object ID");
1110        }
1111        if (repository.getRootFolderId().equals(objectId)) {
1112            throw new CmisConstraintException("Cannot move root");
1113        }
1114        if (targetFolderId == null) {
1115            throw new CmisInvalidArgumentException("Missing target folder ID");
1116        }
1117        getDocumentModel(objectId); // check exists and not deleted
1118        DocumentRef docRef = new IdRef(objectId);
1119        DocumentModel parent = coreSession.getParentDocument(docRef);
1120        if (isFilteredOut(parent)) {
1121            throw new CmisObjectNotFoundException("No parent: " + objectId);
1122        }
1123        if (sourceFolderId == null) {
1124            sourceFolderId = parent.getId();
1125        } else {
1126            // check it's the actual parent
1127            if (!parent.getId().equals(sourceFolderId)) {
1128                throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in " + sourceFolderId);
1129            }
1130        }
1131        DocumentModel target = getDocumentModel(targetFolderId);
1132        if (!target.isFolder()) {
1133            throw new CmisInvalidArgumentException("Target is not a folder: " + targetFolderId);
1134        }
1135        coreSession.move(docRef, new IdRef(targetFolderId), null);
1136        save();
1137    }
1138
1139    @Override
1140    public void setContentStream(String repositoryId, Holder<String> objectIdHolder, Boolean overwriteFlag,
1141            Holder<String> changeTokenHolder, ContentStream contentStream, ExtensionsData extension) {
1142        String objectId;
1143        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1144            throw new CmisInvalidArgumentException("Missing object ID");
1145        }
1146
1147        DocumentModel doc = getDocumentModel(objectId);
1148        String operation = contentStream == null ? "deleteContentStream" : "setContentStream";
1149        verifyChangeToken(doc, changeTokenHolder, operation);
1150
1151        // TODO test doc checkout state
1152        try {
1153            NuxeoPropertyData.setContentStream(doc, contentStream, !Boolean.FALSE.equals(overwriteFlag));
1154            doc = coreSession.saveDocument(doc);
1155            NuxeoPropertyData.validateBlobDigest(doc, callContext);
1156            save();
1157        } catch (IOException e) {
1158            throw new CmisRuntimeException(e.toString(), e);
1159        }
1160    }
1161
1162    protected void verifyChangeToken(DocumentModel doc, Holder<String> changeTokenHolder, String operation) {
1163        if (doc == null) {
1164            return;
1165        }
1166        String docChangeToken = doc.getChangeToken();
1167        if (StringUtils.isBlank(docChangeToken)) {
1168            return;
1169        }
1170        String reqChangeToken = changeTokenHolder == null ? null : changeTokenHolder.getValue();
1171        if (!docChangeToken.equals(reqChangeToken)) {
1172            throw new CmisUpdateConflictException(String.format(
1173                    "Request %s failed because supplied changeToken: '%s' does not match existing changeToken: '%s'",
1174                    operation, reqChangeToken, docChangeToken));
1175        }
1176    }
1177
1178    @Override
1179    public void updateProperties(String repositoryId, Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
1180            Properties properties, ExtensionsData extension) {
1181        updateProperties(objectIdHolder, changeTokenHolder, properties);
1182        save();
1183    }
1184
1185    /* does not save the session */
1186    protected void updateProperties(Holder<String> objectIdHolder, Holder<String> changeTokenHolder,
1187            Properties properties) {
1188        String objectId;
1189        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1190            throw new CmisInvalidArgumentException("Missing object ID");
1191        }
1192        DocumentModel doc = getDocumentModel(objectId);
1193        verifyChangeToken(doc, changeTokenHolder, "updateProperties");
1194        NuxeoObjectData object = new NuxeoObjectData(this, doc);
1195        updateProperties(object, properties, false);
1196        coreSession.saveDocument(doc);
1197    }
1198
1199    @Override
1200    public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(String repositoryId,
1201            List<BulkUpdateObjectIdAndChangeToken> objectIdAndChangeToken, Properties properties,
1202            List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds, ExtensionsData extension) {
1203        List<BulkUpdateObjectIdAndChangeToken> list = new ArrayList<>(objectIdAndChangeToken.size());
1204        for (BulkUpdateObjectIdAndChangeToken idt : objectIdAndChangeToken) {
1205            String id = idt.getId();
1206            Holder<String> objectIdHolder = new Holder<>(id);
1207            Holder<String> changeTokenHolder = new Holder<>(idt.getChangeToken());
1208            updateProperties(objectIdHolder, changeTokenHolder, properties);
1209            list.add(new BulkUpdateObjectIdAndChangeTokenImpl(id, objectIdHolder.getValue(),
1210                    changeTokenHolder.getValue()));
1211        }
1212        save();
1213        return list;
1214    }
1215
1216    @Override
1217    public Acl applyAcl(String repositoryId, String objectId, Acl addAces, Acl removeAces,
1218            AclPropagation aclPropagation, ExtensionsData extension) {
1219        return applyAcl(objectId, addAces, removeAces, false, aclPropagation);
1220    }
1221
1222    @Override
1223    public Acl applyAcl(String repositoryId, String objectId, Acl aces, AclPropagation aclPropagation) {
1224        return applyAcl(objectId, aces, null, true, aclPropagation);
1225    }
1226
1227    protected Acl applyAcl(String objectId, Acl addAces, Acl removeAces, boolean clearFirst,
1228            AclPropagation aclPropagation) {
1229        DocumentModel doc = getDocumentModel(objectId); // does filtering
1230        if (aclPropagation == null) {
1231            aclPropagation = AclPropagation.REPOSITORYDETERMINED;
1232        }
1233        if (aclPropagation == AclPropagation.OBJECTONLY && doc.getDocumentType().isFolder()) {
1234            throw new CmisInvalidArgumentException("Cannot use ACLPropagation=objectonly on Folder");
1235        }
1236        DocumentRef docRef = new IdRef(objectId);
1237
1238        ACP acp = coreSession.getACP(docRef);
1239
1240        ACL acl = acp.getOrCreateACL(ACL.LOCAL_ACL);
1241        if (clearFirst) {
1242            acl.clear();
1243        }
1244
1245        if (addAces != null) {
1246            for (Ace ace : addAces.getAces()) {
1247                String principalId = ace.getPrincipalId();
1248                for (String permission : ace.getPermissions()) {
1249                    String perm = permissionToNuxeo(permission);
1250                    if (PERMISSION_NOTHING.equals(perm)) {
1251                        // block everything
1252                        acl.add(new ACE(SecurityConstants.EVERYONE, SecurityConstants.EVERYTHING, false));
1253                    } else {
1254                        acl.add(new ACE(principalId, perm, true));
1255                    }
1256                }
1257            }
1258        }
1259
1260        if (removeAces != null) {
1261            for (Iterator<ACE> it = acl.iterator(); it.hasNext();) {
1262                ACE ace = it.next();
1263                String username = ace.getUsername();
1264                String perm = ace.getPermission();
1265                if (ace.isDenied()) {
1266                    if (SecurityConstants.EVERYONE.equals(username) && SecurityConstants.EVERYTHING.equals(perm)) {
1267                        perm = PERMISSION_NOTHING;
1268                    } else {
1269                        continue;
1270                    }
1271                }
1272                String permission = permissionFromNuxeo(perm);
1273                for (Ace race : removeAces.getAces()) {
1274                    String principalId = race.getPrincipalId();
1275                    if (!username.equals(principalId)) {
1276                        continue;
1277                    }
1278                    if (race.getPermissions().contains(permission)) {
1279                        it.remove();
1280                        break;
1281                    }
1282                }
1283            }
1284        }
1285        coreSession.setACP(docRef, acp, true);
1286        return NuxeoObjectData.getAcl(acp, false, this);
1287    }
1288
1289    protected static String permissionToNuxeo(String permission) {
1290        switch (permission) {
1291        case BasicPermissions.READ:
1292            return SecurityConstants.READ;
1293        case BasicPermissions.WRITE:
1294            return SecurityConstants.READ_WRITE;
1295        case BasicPermissions.ALL:
1296            return SecurityConstants.EVERYTHING;
1297        default:
1298            return permission;
1299        }
1300    }
1301
1302    protected static String permissionFromNuxeo(String permission) {
1303        switch (permission) {
1304        case SecurityConstants.READ:
1305            return BasicPermissions.READ;
1306        case SecurityConstants.READ_WRITE:
1307            return BasicPermissions.WRITE;
1308        case SecurityConstants.EVERYTHING:
1309            return BasicPermissions.ALL;
1310        default:
1311            return permission;
1312        }
1313    }
1314
1315    @Override
1316    public Acl getAcl(String repositoryId, String objectId, Boolean onlyBasicPermissions, ExtensionsData extension) {
1317        boolean basic = !Boolean.FALSE.equals(onlyBasicPermissions);
1318        getDocumentModel(objectId); // does filtering
1319        ACP acp = coreSession.getACP(new IdRef(objectId));
1320        return NuxeoObjectData.getAcl(acp, basic, this);
1321    }
1322
1323    @Override
1324    public ObjectList getContentChanges(String repositoryId, Holder<String> changeLogTokenHolder,
1325            Boolean includeProperties, String filter, Boolean includePolicyIds, Boolean includeAcl, BigInteger maxItems,
1326            ExtensionsData extension) {
1327        if (changeLogTokenHolder == null) {
1328            throw new CmisInvalidArgumentException("Missing change log token holder");
1329        }
1330        String changeLogToken = changeLogTokenHolder.getValue();
1331        long minId;
1332        if (changeLogToken == null) {
1333            minId = 0;
1334        } else {
1335            try {
1336                minId = Long.parseLong(changeLogToken);
1337            } catch (NumberFormatException e) {
1338                throw new CmisInvalidArgumentException("Invalid change log token");
1339            }
1340        }
1341        int max = maxItems == null ? -1 : maxItems.intValue();
1342        if (max <= 0) {
1343            max = DEFAULT_CHANGE_LOG_SIZE;
1344        }
1345        if (max > MAX_CHANGE_LOG_SIZE) {
1346            max = MAX_CHANGE_LOG_SIZE;
1347        }
1348        List<ObjectData> ods = null;
1349        // retry with increasingly larger page size if some items are
1350        // skipped
1351        for (int scale = 1; scale < 128; scale *= 2) {
1352            int pageSize = max * scale + 1;
1353            if (pageSize < 0) { // overflow
1354                pageSize = Integer.MAX_VALUE;
1355            }
1356            ods = readAuditLog(repositoryId, minId, max, pageSize);
1357            if (ods != null) {
1358                break;
1359            }
1360            if (pageSize == Integer.MAX_VALUE) {
1361                break;
1362            }
1363        }
1364        if (ods == null) {
1365            // couldn't find enough, too many items were skipped
1366            ods = Collections.emptyList();
1367
1368        }
1369        boolean hasMoreItems = ods.size() > max;
1370        if (hasMoreItems) {
1371            ods = ods.subList(0, max);
1372        }
1373        String latestChangeLogToken;
1374        if (ods.size() == 0) {
1375            latestChangeLogToken = null;
1376        } else {
1377            ObjectData last = ods.get(ods.size() - 1);
1378            latestChangeLogToken = (String) last.getProperties().getProperties().get(NX_CHANGE_LOG_ID).getFirstValue();
1379        }
1380        ObjectListImpl ol = new ObjectListImpl();
1381        ol.setHasMoreItems(Boolean.valueOf(hasMoreItems));
1382        ol.setObjects(ods);
1383        ol.setNumItems(BigInteger.valueOf(-1));
1384        changeLogTokenHolder.setValue(latestChangeLogToken);
1385        return ol;
1386    }
1387
1388    protected SearchRequestBuilder getElasticsearchBuilder() {
1389        ElasticSearchAdmin esa = Framework.getService(ElasticSearchAdmin.class);
1390        String indexName = esa.getIndexNameForType(ElasticSearchConstants.ENTRY_TYPE);
1391        return esa.getClient().prepareSearch(indexName).setTypes(ElasticSearchConstants.ENTRY_TYPE).setSearchType(
1392                SearchType.DFS_QUERY_THEN_FETCH);
1393    }
1394
1395    /**
1396     * Reads at most max+1 entries from the audit log.
1397     *
1398     * @return null if not enough elements found with the current page size
1399     */
1400    protected List<ObjectData> readAuditLog(String repositoryId, long minId, int max, int pageSize) {
1401        AuditReader reader = Framework.getLocalService(AuditReader.class);
1402        if (reader == null) {
1403            throw new CmisRuntimeException("Cannot find audit service");
1404        }
1405        List<LogEntry> entries;
1406        if (reader instanceof DefaultAuditBackend) {
1407            String query = "FROM LogEntry log" //
1408                    + " WHERE log.id >= :minId" //
1409                    + "   AND log.eventId IN (:evCreated, :evModified, :evRemoved)" //
1410                    + "   AND log.repositoryId = :repoId" //
1411                    + " ORDER BY log.id";
1412            Map<String, Object> params = new HashMap<>();
1413            params.put("minId", Long.valueOf(minId));
1414            params.put("evCreated", DOCUMENT_CREATED);
1415            params.put("evModified", DOCUMENT_UPDATED);
1416            params.put("evRemoved", DOCUMENT_REMOVED);
1417            params.put("repoId", repositoryId);
1418            entries = (List<LogEntry>) reader.nativeQuery(query, params, 1, pageSize);
1419        } else if (reader instanceof ESAuditBackend) {
1420            SearchRequestBuilder builder = getElasticsearchBuilder();
1421            BoolQueryBuilder query = QueryBuilders.boolQuery()
1422                                                  .must(QueryBuilders.matchAllQuery())
1423                                                  .filter(QueryBuilders.termQuery(ES_AUDIT_REPOSITORY_ID, repositoryId))
1424                                                  .filter(QueryBuilders.termsQuery(ES_AUDIT_EVENT_ID, DOCUMENT_CREATED,
1425                                                          DOCUMENT_UPDATED, DOCUMENT_REMOVED))
1426                                                  .filter(QueryBuilders.rangeQuery(ES_AUDIT_ID).gte(minId));
1427            builder.setQuery(query);
1428            builder.addSort(ES_AUDIT_ID, SortOrder.ASC);
1429            entries = new ArrayList<>();
1430            SearchResponse searchResponse = builder.setSize(pageSize).execute().actionGet();
1431            for (SearchHit hit : searchResponse.getHits()) {
1432                try {
1433                    entries.add(AuditEntryJSONReader.read(hit.getSourceAsString()));
1434                } catch (IOException e) {
1435                    throw new CmisRuntimeException("Failed to parse audit entry: " + hit, e);
1436                }
1437            }
1438        } else {
1439            throw new CmisRuntimeException("Unknown audit backend: " + reader.getClass().getName());
1440        }
1441        List<ObjectData> ods = new ArrayList<>();
1442        for (LogEntry entry : entries) {
1443            ObjectData od = getLogEntryObjectData(entry);
1444            if (od != null) {
1445                ods.add(od);
1446                if (ods.size() > max) {
1447                    // enough collected
1448                    return ods;
1449                }
1450            }
1451        }
1452        if (entries.size() < pageSize) {
1453            // end of audit log
1454            return ods;
1455        }
1456        return null;
1457    }
1458
1459    /**
1460     * Gets object data for a log entry, or null if skipped.
1461     */
1462    protected ObjectData getLogEntryObjectData(LogEntry logEntry) {
1463        String docType = logEntry.getDocType();
1464        if (!getTypeManager().hasType(docType)) {
1465            // ignore types present in the log but not exposed through CMIS
1466            return null;
1467        }
1468        // change type
1469        String eventId = logEntry.getEventId();
1470        ChangeType changeType;
1471        if (DOCUMENT_CREATED.equals(eventId)) {
1472            changeType = ChangeType.CREATED;
1473        } else if (DOCUMENT_UPDATED.equals(eventId)) {
1474            changeType = ChangeType.UPDATED;
1475        } else if (DOCUMENT_REMOVED.equals(eventId)) {
1476            changeType = ChangeType.DELETED;
1477        } else {
1478            return null;
1479        }
1480        ChangeEventInfoDataImpl cei = new ChangeEventInfoDataImpl();
1481        cei.setChangeType(changeType);
1482        // change time
1483        GregorianCalendar changeTime = (GregorianCalendar) Calendar.getInstance();
1484        Date date = logEntry.getEventDate();
1485        changeTime.setTime(date);
1486        cei.setChangeTime(changeTime);
1487        ObjectDataImpl od = new ObjectDataImpl();
1488        od.setChangeEventInfo(cei);
1489        // properties: id, doc type, change log id
1490        PropertiesImpl properties = new PropertiesImpl();
1491        properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_ID, logEntry.getDocUUID()));
1492        properties.addProperty(new PropertyIdImpl(PropertyIds.OBJECT_TYPE_ID, docType));
1493        properties.addProperty(new PropertyStringImpl(NX_CHANGE_LOG_ID, String.valueOf(logEntry.getId())));
1494        od.setProperties(properties);
1495        return od;
1496    }
1497
1498    protected String getLatestChangeLogToken(String repositoryId) {
1499        AuditReader reader = Framework.getService(AuditReader.class);
1500        if (reader == null) {
1501            log.warn("Audit Service not found. latest change log token will be '0'");
1502            return "0";
1503            // throw new CmisRuntimeException("Cannot find audit service");
1504        }
1505        long id;
1506        if (reader instanceof DefaultAuditBackend) {
1507            String query = "FROM LogEntry log" //
1508                    + " WHERE log.eventId IN (:evCreated, :evModified, :evRemoved)" //
1509                    + "   AND log.repositoryId = :repoId" //
1510                    + " ORDER BY log.id DESC";
1511            Map<String, Object> params = new HashMap<>();
1512            params.put("evCreated", DOCUMENT_CREATED);
1513            params.put("evModified", DOCUMENT_UPDATED);
1514            params.put("evRemoved", DOCUMENT_REMOVED);
1515            params.put("repoId", repositoryId);
1516            @SuppressWarnings("unchecked")
1517            List<LogEntry> entries = (List<LogEntry>) reader.nativeQuery(query, params, 1, 1);
1518            id = entries.isEmpty() ? 0 : entries.get(0).getId();
1519        } else if (reader instanceof ESAuditBackend) {
1520            SearchRequestBuilder builder = getElasticsearchBuilder();
1521            BoolQueryBuilder query = QueryBuilders.boolQuery()
1522                                                  .must(QueryBuilders.matchAllQuery())
1523                                                  .filter(QueryBuilders.termQuery(ES_AUDIT_REPOSITORY_ID, repositoryId))
1524                                                  .filter(QueryBuilders.termsQuery(ES_AUDIT_EVENT_ID, DOCUMENT_CREATED,
1525                                                          DOCUMENT_UPDATED, DOCUMENT_REMOVED));
1526            builder.setQuery(query);
1527            builder.addSort(ES_AUDIT_ID, SortOrder.DESC);
1528            builder.setSize(1);
1529            // TODO refactor this to use max clause
1530            SearchResponse searchResponse = builder.execute().actionGet();
1531            SearchHit[] hits = searchResponse.getHits().hits();
1532            if (hits.length == 0) {
1533                id = 0;
1534            } else {
1535                String hit = hits[0].getSourceAsString();
1536                try {
1537                    id = AuditEntryJSONReader.read(hit).getId();
1538                } catch (IOException e) {
1539                    throw new CmisRuntimeException("Failed to parse audit entry: " + hit, e);
1540                }
1541            }
1542        } else {
1543            throw new CmisRuntimeException("Unknown audit backend: " + reader.getClass().getName());
1544        }
1545        return String.valueOf(id);
1546    }
1547
1548    protected String addProxyClause(String query) {
1549        if (!repository.supportsProxies()) {
1550            query += " AND " + NXQL.ECM_ISPROXY + " = 0";
1551        }
1552        return query;
1553    }
1554
1555    @Override
1556    public ObjectList query(String repositoryId, String statement, Boolean searchAllVersions,
1557            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1558            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1559        long skip = skipCount == null ? 0 : skipCount.longValue();
1560        if (skip < 0) {
1561            skip = 0;
1562        }
1563        long max = maxItems == null ? -1 : maxItems.longValue();
1564        if (max <= 0) {
1565            max = DEFAULT_QUERY_SIZE;
1566        }
1567        long numItems;
1568        List<ObjectData> list;
1569        IterableQueryResult res = null;
1570        try {
1571            Map<String, PropertyDefinition<?>> typeInfo = new HashMap<>();
1572            // searchAllVersions defaults to false, spec 2.2.6.1.1
1573            res = queryAndFetch(statement, Boolean.TRUE.equals(searchAllVersions), typeInfo);
1574
1575            // convert from Nuxeo to CMIS format
1576            list = new ArrayList<>();
1577            if (skip > 0) {
1578                res.skipTo(skip);
1579            }
1580            for (Map<String, Serializable> map : res) {
1581                ObjectDataImpl od = makeObjectData(map, typeInfo);
1582
1583                // optional stuff
1584                String id = od.getId();
1585                if (id != null) { // null if JOIN in original query
1586                    DocumentModel doc = null;
1587                    if (Boolean.TRUE.equals(includeAllowableActions)) {
1588                        doc = getDocumentModel(id);
1589                        AllowableActions allowableActions = NuxeoObjectData.getAllowableActions(doc, false);
1590                        od.setAllowableActions(allowableActions);
1591                    }
1592                    if (includeRelationships != null && includeRelationships != IncludeRelationships.NONE) {
1593                        // TODO get relationships using a JOIN
1594                        // added to the original query
1595                        List<ObjectData> relationships = NuxeoObjectData.getRelationships(id, includeRelationships,
1596                                this);
1597                        od.setRelationships(relationships);
1598                    }
1599                    if (NuxeoObjectData.needsRenditions(renditionFilter)) {
1600                        if (doc == null) {
1601                            doc = getDocumentModel(id);
1602                        }
1603                        List<RenditionData> renditions = NuxeoObjectData.getRenditions(doc, renditionFilter, null, null,
1604                                callContext);
1605                        od.setRenditions(renditions);
1606                    }
1607                }
1608
1609                list.add(od);
1610                if (list.size() >= max) {
1611                    break;
1612                }
1613            }
1614            numItems = res.size();
1615        } finally {
1616            if (res != null) {
1617                res.close();
1618            }
1619        }
1620        ObjectListImpl objList = new ObjectListImpl();
1621        objList.setObjects(list);
1622        objList.setNumItems(BigInteger.valueOf(numItems));
1623        objList.setHasMoreItems(Boolean.valueOf(numItems > skip + list.size()));
1624        return objList;
1625    }
1626
1627    /**
1628     * Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
1629     * {@code finally} block.
1630     *
1631     * @param query the CMISQL query
1632     * @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
1633     *            ), for versionable types
1634     * @param typeInfo a map filled with type information for each returned property, or {@code null} if no such info is
1635     *            needed
1636     * @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
1637     * @throws CmisInvalidArgumentException if the query cannot be parsed or is invalid
1638     * @since 6.0
1639     */
1640    public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions,
1641            Map<String, PropertyDefinition<?>> typeInfo) {
1642        if (repository.supportsJoins()) {
1643            if (repository.supportsProxies()) {
1644                throw new CmisRuntimeException(
1645                        "Server configuration error: cannot supports joins and proxies at the same time");
1646            }
1647            // straight to CoreSession as CMISQL, relies on proper QueryMaker
1648            return coreSession.queryAndFetch(query, CMISQLQueryMaker.TYPE, this, typeInfo,
1649                    Boolean.valueOf(searchAllVersions));
1650        } else {
1651            // convert to NXQL for evaluation
1652            CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
1653            String nxql;
1654            try {
1655                nxql = converter.getNXQL(query, this, typeInfo, searchAllVersions);
1656            } catch (QueryParseException e) {
1657                throw new CmisInvalidArgumentException(e.getMessage(), e);
1658            }
1659
1660            IterableQueryResult it;
1661            try {
1662                if (repository.useElasticsearch()) {
1663                    ElasticSearchService ess = Framework.getService(ElasticSearchService.class);
1664                    NxQueryBuilder qb = new NxQueryBuilder(coreSession).nxql(nxql)
1665                                                                       .limit(1000)
1666                                                                       .onlyElasticsearchResponse();
1667                    it = new EsIterableQueryResultImpl(ess, ess.scroll(qb, 1000));
1668                } else {
1669                    // distinct documents
1670                    it = coreSession.queryAndFetch(nxql, NXQL.NXQL, true, new Object[0]);
1671                }
1672            } catch (QueryParseException e) {
1673                e.addInfo("Invalid query: CMISQL: " + query);
1674                throw e;
1675            }
1676            // wrap result
1677            return converter.getIterableQueryResult(it, this);
1678        }
1679    }
1680
1681    /**
1682     * Makes a CMISQL query to the repository and returns an {@link IterableQueryResult}, which MUST be closed in a
1683     * {@code finally} block.
1684     *
1685     * @param query the CMISQL query
1686     * @param searchAllVersions whether to search all versions ({@code true}) or only the latest version ({@code false}
1687     *            ), for versionable types
1688     * @return an {@link IterableQueryResult}, which MUST be closed in a {@code finally} block
1689     * @throws CmisRuntimeException if the query cannot be parsed or is invalid
1690     * @since 6.0
1691     */
1692    public IterableQueryResult queryAndFetch(String query, boolean searchAllVersions) {
1693        return queryAndFetch(query, searchAllVersions, null);
1694    }
1695
1696    protected ObjectDataImpl makeObjectData(Map<String, Serializable> map,
1697            Map<String, PropertyDefinition<?>> typeInfo) {
1698        ObjectDataImpl od = new ObjectDataImpl();
1699        PropertiesImpl properties = new PropertiesImpl();
1700        for (Entry<String, Serializable> en : map.entrySet()) {
1701            String queryName = en.getKey();
1702            PropertyDefinition<?> pd = typeInfo.get(queryName);
1703            if (pd == null) {
1704                throw new NullPointerException("Cannot get " + queryName);
1705            }
1706            AbstractPropertyData<?> p = (AbstractPropertyData<?>) objectFactory.createPropertyData(pd, en.getValue());
1707            p.setLocalName(pd.getLocalName());
1708            p.setDisplayName(pd.getDisplayName());
1709            // queryName and pd.getQueryName() may be different
1710            // for qualified properties
1711            p.setQueryName(queryName);
1712            properties.addProperty(p);
1713        }
1714        od.setProperties(properties);
1715        return od;
1716    }
1717
1718    @Override
1719    public void addObjectToFolder(String repositoryId, String objectId, String folderId, Boolean allVersions,
1720            ExtensionsData extension) {
1721        throw new CmisNotSupportedException();
1722    }
1723
1724    @Override
1725    public void removeObjectFromFolder(String repositoryId, String objectId, String folderId,
1726            ExtensionsData extension) {
1727        if (folderId != null) {
1728            // check it's the actual parent
1729            DocumentModel folder = getDocumentModel(folderId);
1730            DocumentModel parent = coreSession.getParentDocument(new IdRef(objectId));
1731            if (!parent.getId().equals(folder.getId())) {
1732                throw new CmisInvalidArgumentException("Object " + objectId + " is not filed in  " + folderId);
1733            }
1734        }
1735        deleteObject(repositoryId, objectId, Boolean.FALSE, extension);
1736    }
1737
1738    @Override
1739    public ObjectInFolderList getChildren(String repositoryId, String folderId, String filter, String orderBy,
1740            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1741            Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1742        if (folderId == null) {
1743            throw new CmisInvalidArgumentException("Null folderId");
1744        }
1745        return getChildrenInternal(repositoryId, folderId, filter, orderBy, includeAllowableActions,
1746                includeRelationships, renditionFilter, includePathSegment, maxItems, skipCount, false);
1747    }
1748
1749    protected ObjectInFolderList getChildrenInternal(String repositoryId, String folderId, String filter,
1750            String orderBy, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1751            String renditionFilter, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
1752            boolean folderOnly) {
1753        ObjectInFolderListImpl result = new ObjectInFolderListImpl();
1754        List<ObjectInFolderData> list = new ArrayList<>();
1755        DocumentModel folder = getDocumentModel(folderId);
1756        if (!folder.isFolder()) {
1757            return null;
1758        }
1759
1760        String query = String.format(
1761                "SELECT * FROM %s WHERE " // Folder/Document
1762                        + "%s = '%s' AND " // ecm:parentId = 'folderId'
1763                        + "%s <> '%s' AND " // ecm:mixinType <> 'HiddenInNavigation'
1764                        + "%s <> '%s'", // ecm:currentLifeCycleState <> 'deleted'
1765                folderOnly ? "Folder" : "Document", //
1766                NXQL.ECM_PARENTID, folderId, //
1767                NXQL.ECM_MIXINTYPE, FacetNames.HIDDEN_IN_NAVIGATION, //
1768                NXQL.ECM_LIFECYCLESTATE, LifeCycleConstants.DELETED_STATE);
1769        query = addProxyClause(query);
1770        if (!StringUtils.isBlank(orderBy)) {
1771            CMISQLtoNXQL converter = new CMISQLtoNXQL(repository.supportsProxies());
1772            query += " ORDER BY " + converter.convertOrderBy(orderBy, getTypeManager());
1773        }
1774
1775        long limit = maxItems == null ? 0 : maxItems.longValue();
1776        if (limit < 0) {
1777            limit = 0;
1778        }
1779        long offset = skipCount == null ? 0 : skipCount.longValue();
1780        if (offset < 0) {
1781            offset = 0;
1782        }
1783
1784        DocumentModelList children = coreSession.query(query, null, limit, offset, true);
1785
1786        for (DocumentModel child : children) {
1787            NuxeoObjectData data = new NuxeoObjectData(this, child, filter, includeAllowableActions,
1788                    includeRelationships, renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
1789            ObjectInFolderDataImpl oifd = new ObjectInFolderDataImpl();
1790            oifd.setObject(data);
1791            if (Boolean.TRUE.equals(includePathSegment)) {
1792                oifd.setPathSegment(child.getName());
1793            }
1794            list.add(oifd);
1795            collectObjectInfo(repositoryId, data.getId());
1796        }
1797
1798        Boolean hasMoreItems;
1799        if (limit == 0) {
1800            hasMoreItems = Boolean.FALSE;
1801        } else {
1802            hasMoreItems = Boolean.valueOf(children.totalSize() > offset + limit);
1803        }
1804        result.setObjects(list);
1805        result.setHasMoreItems(hasMoreItems);
1806        result.setNumItems(BigInteger.valueOf(children.totalSize()));
1807        collectObjectInfo(repositoryId, folderId);
1808        return result;
1809    }
1810
1811    @Override
1812    public List<ObjectInFolderContainer> getDescendants(String repositoryId, String folderId, BigInteger depth,
1813            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1814            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
1815        if (folderId == null) {
1816            throw new CmisInvalidArgumentException("Null folderId");
1817        }
1818        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
1819        if (levels == 0) {
1820            throw new CmisInvalidArgumentException("Invalid depth: 0");
1821        }
1822        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
1823                renditionFilter, includePathSegment, 0, levels, false);
1824    }
1825
1826    @Override
1827    public List<ObjectInFolderContainer> getFolderTree(String repositoryId, String folderId, BigInteger depth,
1828            String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
1829            String renditionFilter, Boolean includePathSegment, ExtensionsData extension) {
1830        if (folderId == null) {
1831            throw new CmisInvalidArgumentException("Null folderId");
1832        }
1833        int levels = depth == null ? DEFAULT_FOLDER_LEVELS : depth.intValue();
1834        if (levels == 0) {
1835            throw new CmisInvalidArgumentException("Invalid depth: 0");
1836        }
1837        return getDescendantsInternal(repositoryId, folderId, filter, includeAllowableActions, includeRelationships,
1838                renditionFilter, includePathSegment, 0, levels, true);
1839    }
1840
1841    protected List<ObjectInFolderContainer> getDescendantsInternal(String repositoryId, String folderId, String filter,
1842            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1843            Boolean includePathSegments, int level, int maxLevels, boolean folderOnly) {
1844        if (maxLevels != -1 && level >= maxLevels) {
1845            return null;
1846        }
1847        ObjectInFolderList children = getChildrenInternal(repositoryId, folderId, filter, null, includeAllowableActions,
1848                includeRelationships, renditionFilter, includePathSegments, null, null, folderOnly);
1849        if (children == null) {
1850            return Collections.emptyList();
1851        }
1852        List<ObjectInFolderContainer> res = new ArrayList<>(children.getObjects().size());
1853        for (ObjectInFolderData child : children.getObjects()) {
1854            ObjectInFolderContainerImpl oifc = new ObjectInFolderContainerImpl();
1855            oifc.setObject(child);
1856            // recurse
1857            List<ObjectInFolderContainer> subChildren = getDescendantsInternal(repositoryId, child.getObject().getId(),
1858                    filter, includeAllowableActions, includeRelationships, renditionFilter, includePathSegments,
1859                    level + 1, maxLevels, folderOnly);
1860            if (subChildren != null) {
1861                oifc.setChildren(subChildren);
1862            }
1863            res.add(oifc);
1864        }
1865        return res;
1866    }
1867
1868    @Override
1869    public ObjectData getFolderParent(String repositoryId, String folderId, String filter, ExtensionsData extension) {
1870        List<ObjectParentData> parents = getObjectParentsInternal(repositoryId, folderId, filter, null, null, null,
1871                Boolean.TRUE, true);
1872        return parents.isEmpty() ? null : parents.get(0).getObject();
1873    }
1874
1875    @Override
1876    public List<ObjectParentData> getObjectParents(String repositoryId, String objectId, String filter,
1877            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1878            Boolean includeRelativePathSegment, ExtensionsData extension) {
1879        return getObjectParentsInternal(repositoryId, objectId, filter, includeAllowableActions, includeRelationships,
1880                renditionFilter, includeRelativePathSegment, false);
1881    }
1882
1883    protected List<ObjectParentData> getObjectParentsInternal(String repositoryId, String objectId, String filter,
1884            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
1885            Boolean includeRelativePathSegment, boolean folderOnly) {
1886        DocumentRef docRef = new IdRef(objectId);
1887        if (!coreSession.exists(docRef)) {
1888            throw new CmisObjectNotFoundException(objectId);
1889        }
1890        DocumentModel doc = coreSession.getDocument(docRef);
1891        if (isFilteredOut(doc)) {
1892            throw new CmisObjectNotFoundException(objectId);
1893        }
1894        if (folderOnly && !doc.isFolder()) {
1895            throw new CmisInvalidArgumentException("Not a folder: " + objectId);
1896        }
1897        String pathSegment = doc.getName();
1898        if (pathSegment == null) { // root
1899            return Collections.emptyList();
1900        }
1901        DocumentRef parentRef = doc.getParentRef();
1902        if (parentRef == null) { // placeless
1903            return Collections.emptyList();
1904        }
1905        if (!coreSession.exists(parentRef)) { // non-accessible
1906            return Collections.emptyList();
1907        }
1908        DocumentModel parent = coreSession.getDocument(parentRef);
1909        if (isFilteredOut(parent)) { // filtered out
1910            return Collections.emptyList();
1911        }
1912        String parentId = parent.getId();
1913
1914        ObjectData od = getObject(repositoryId, parentId, filter, includeAllowableActions, includeRelationships,
1915                renditionFilter, Boolean.FALSE, Boolean.FALSE, null);
1916        ObjectParentDataImpl opd = new ObjectParentDataImpl(od);
1917        if (!Boolean.FALSE.equals(includeRelativePathSegment)) {
1918            opd.setRelativePathSegment(pathSegment);
1919        }
1920        return Collections.<ObjectParentData> singletonList(opd);
1921    }
1922
1923    @Override
1924    public void applyPolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
1925        throw new CmisNotSupportedException();
1926    }
1927
1928    @Override
1929    public List<ObjectData> getAppliedPolicies(String repositoryId, String objectId, String filter,
1930            ExtensionsData extension) {
1931        return Collections.emptyList();
1932    }
1933
1934    @Override
1935    public void removePolicy(String repositoryId, String policyId, String objectId, ExtensionsData extension) {
1936        throw new CmisNotSupportedException();
1937    }
1938
1939    @Override
1940    public ObjectList getObjectRelationships(String repositoryId, String objectId, Boolean includeSubRelationshipTypes,
1941            RelationshipDirection relationshipDirection, String typeId, String filter, Boolean includeAllowableActions,
1942            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
1943        IncludeRelationships includeRelationships;
1944        if (relationshipDirection == null || relationshipDirection == RelationshipDirection.SOURCE) {
1945            includeRelationships = IncludeRelationships.SOURCE;
1946        } else if (relationshipDirection == RelationshipDirection.TARGET) {
1947            includeRelationships = IncludeRelationships.TARGET;
1948        } else { // RelationshipDirection.EITHER
1949            includeRelationships = IncludeRelationships.BOTH;
1950        }
1951        List<ObjectData> rels = NuxeoObjectData.getRelationships(objectId, includeRelationships, this);
1952        BatchedList<ObjectData> batch = ListUtils.getBatchedList(rels, maxItems, skipCount, DEFAULT_MAX_RELATIONSHIPS);
1953        ObjectListImpl res = new ObjectListImpl();
1954        res.setObjects(batch.getList());
1955        res.setNumItems(batch.getNumItems());
1956        res.setHasMoreItems(batch.getHasMoreItems());
1957        for (ObjectData data : res.getObjects()) {
1958            collectObjectInfo(repositoryId, data.getId());
1959        }
1960        return res;
1961    }
1962
1963    @Override
1964    public void checkIn(String repositoryId, Holder<String> objectIdHolder, Boolean major, Properties properties,
1965            ContentStream contentStream, String checkinComment, List<String> policies, Acl addAces, Acl removeAces,
1966            ExtensionsData extension) {
1967        String objectId;
1968        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
1969            throw new CmisInvalidArgumentException("Missing object ID");
1970        }
1971        VersioningOption option = Boolean.TRUE.equals(major) ? VersioningOption.MAJOR : VersioningOption.MINOR;
1972
1973        DocumentModel doc = getDocumentModel(objectId);
1974
1975        NuxeoObjectData object = new NuxeoObjectData(this, doc);
1976        updateProperties(object, properties, false);
1977        boolean setContentStream = contentStream != null;
1978        if (setContentStream) {
1979            try {
1980                NuxeoPropertyData.setContentStream(doc, contentStream, true);
1981            } catch (IOException e) {
1982                throw new CmisRuntimeException(e.toString(), e);
1983            }
1984        }
1985        // comment for save event
1986        doc.putContextData("comment", checkinComment);
1987        doc = coreSession.saveDocument(doc);
1988        if (setContentStream) {
1989            NuxeoPropertyData.validateBlobDigest(doc, callContext);
1990        }
1991        DocumentRef ver;
1992        try {
1993            ver = doc.checkIn(option, checkinComment);
1994        } catch (VersionNotModifiableException e) {
1995            throw new CmisInvalidArgumentException("Cannot check in non-PWC: " + doc);
1996        }
1997        doc.removeLock();
1998        save();
1999        objectIdHolder.setValue(getIdFromDocumentRef(ver));
2000    }
2001
2002    @Override
2003    public void checkOut(String repositoryId, Holder<String> objectIdHolder, ExtensionsData extension,
2004            Holder<Boolean> contentCopiedHolder) {
2005        String objectId;
2006        if (objectIdHolder == null || (objectId = objectIdHolder.getValue()) == null) {
2007            throw new CmisInvalidArgumentException("Missing object ID");
2008        }
2009        String pwcId = checkOut(objectId);
2010        objectIdHolder.setValue(pwcId);
2011        if (contentCopiedHolder != null) {
2012            contentCopiedHolder.setValue(Boolean.TRUE);
2013        }
2014    }
2015
2016    public String checkOut(String objectId) {
2017        DocumentModel doc = getDocumentModel(objectId);
2018        try {
2019            // find pwc
2020            DocumentModel pwc;
2021            if (doc.isVersion()) {
2022                pwc = coreSession.getWorkingCopy(doc.getRef());
2023                if (pwc == null) {
2024                    // no live document available
2025                    // TODO do a restore somewhere
2026                    throw new CmisObjectNotFoundException(objectId);
2027                }
2028            } else {
2029                pwc = doc;
2030            }
2031            if (pwc.isCheckedOut()) {
2032                throw new CmisConstraintException("Already checked out: " + objectId);
2033            }
2034            if (pwc.isLocked()) {
2035                throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
2036            }
2037            pwc.setLock();
2038            pwc.checkOut();
2039            save();
2040            return pwc.getId();
2041        } catch (VersionNotModifiableException e) {
2042            throw new CmisInvalidArgumentException("Cannot check out non-version: " + objectId);
2043        } catch (NuxeoException e) { // TODO use a core LockException
2044            String message = e.getMessage();
2045            if (message != null && message.startsWith("Document already locked")) {
2046                throw new CmisConstraintException("Cannot check out since currently locked: " + objectId);
2047            }
2048            throw new CmisRuntimeException(e.toString(), e);
2049        }
2050    }
2051
2052    @Override
2053    public void cancelCheckOut(String repositoryId, String objectId, ExtensionsData extension) {
2054        cancelCheckOut(objectId);
2055    }
2056
2057    public void cancelCheckOut(String objectId) {
2058        DocumentModel doc = getDocumentModel(objectId);
2059        if (!doc.isCheckedOut()) {
2060            throw new CmisInvalidArgumentException("Cannot cancel check out of non-PWC: " + doc);
2061        }
2062        DocumentRef docRef = doc.getRef();
2063        // find last version
2064        DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
2065        if (verRef == null) {
2066            if (errorOnCancelCheckOutOfDraftVersion && "0.0".equals(doc.getVersionLabel())) {
2067                throw new CmisVersioningException("Cannot cancelCheckOut of draft version due to configuration");
2068            }
2069            // delete
2070            coreSession.removeDocument(docRef);
2071        } else {
2072            // restore and keep checked in
2073            coreSession.restoreToVersion(docRef, verRef, true, true);
2074            doc.removeLock();
2075        }
2076        save();
2077    }
2078
2079    @Override
2080    public ObjectList getCheckedOutDocs(String repositoryId, String folderId, String filter, String orderBy,
2081            Boolean includeAllowableActions, IncludeRelationships includeRelationships, String renditionFilter,
2082            BigInteger maxItems, BigInteger skipCount, ExtensionsData extension) {
2083        // columns from filter
2084        List<String> props;
2085        if (StringUtils.isBlank(filter)) {
2086            props = Arrays.asList(PropertyIds.OBJECT_ID, PropertyIds.OBJECT_TYPE_ID, PropertyIds.BASE_TYPE_ID);
2087        } else {
2088            props = NuxeoObjectData.getPropertyIdsFromFilter(filter);
2089            // same as query names
2090        }
2091        // clause from folderId
2092        List<String> clauses = new ArrayList<>(3);
2093        clauses.add(NuxeoTypeHelper.NX_ISVERSION + " = false");
2094        clauses.add(NuxeoTypeHelper.NX_ISCHECKEDIN + " = false");
2095        if (folderId != null) {
2096            String qid = "'" + folderId.replace("'", "''") + "'";
2097            clauses.add("IN_FOLDER(" + qid + ")");
2098        }
2099        // orderBy
2100        String order;
2101        if (StringUtils.isBlank(orderBy)) {
2102            order = "";
2103        } else {
2104            order = " ORDER BY " + orderBy;
2105        }
2106        String statement = "SELECT " + StringUtils.join(props, ", ") + " FROM " + BaseTypeId.CMIS_DOCUMENT.value()
2107                + " WHERE " + StringUtils.join(clauses, " AND ") + order;
2108        Boolean searchAllVersions = Boolean.TRUE;
2109        return query(repositoryId, statement, searchAllVersions, includeAllowableActions, includeRelationships,
2110                renditionFilter, maxItems, skipCount, extension);
2111    }
2112
2113    @Override
2114    public List<ObjectData> getAllVersions(String repositoryId, String objectId, String versionSeriesId, String filter,
2115            Boolean includeAllowableActions, ExtensionsData extension) {
2116        DocumentModel doc;
2117        if (objectId != null) {
2118            // atompub passes object id
2119            doc = getDocumentModel(objectId);
2120        } else if (versionSeriesId != null) {
2121            // soap passes version series id
2122            // version series id is (for now) id of live document
2123            // TODO deal with removal of live doc
2124            doc = getDocumentModel(versionSeriesId);
2125        } else {
2126            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
2127        }
2128        List<DocumentRef> versions = coreSession.getVersionsRefs(doc.getRef());
2129        List<ObjectData> list = new ArrayList<>(versions.size());
2130        for (DocumentRef verRef : versions) {
2131            String verId = getIdFromDocumentRef(verRef);
2132            ObjectData od = getObject(repositoryId, verId, filter, includeAllowableActions, IncludeRelationships.NONE,
2133                    null, Boolean.FALSE, Boolean.FALSE, null);
2134            list.add(od);
2135        }
2136        // PWC last
2137        DocumentModel pwc = doc.isVersion() ? coreSession.getWorkingCopy(doc.getRef()) : doc;
2138        if (pwc != null && pwc.isCheckedOut()) {
2139            NuxeoObjectData od = new NuxeoObjectData(this, pwc, filter, includeAllowableActions,
2140                    IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, extension);
2141            list.add(od);
2142        }
2143        // CoreSession returns them in creation order,
2144        // CMIS wants them last first
2145        Collections.reverse(list);
2146        return list;
2147    }
2148
2149    @Override
2150    public NuxeoObjectData getObjectOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
2151            Boolean major, String filter, Boolean includeAllowableActions, IncludeRelationships includeRelationships,
2152            String renditionFilter, Boolean includePolicyIds, Boolean includeAcl, ExtensionsData extension) {
2153        DocumentModel doc;
2154        if (objectId != null) {
2155            // atompub passes object id
2156            doc = getDocumentModel(objectId);
2157        } else if (versionSeriesId != null) {
2158            // soap passes version series id
2159            // version series id is (for now) id of live document
2160            // TODO deal with removal of live doc
2161            doc = getDocumentModel(versionSeriesId);
2162        } else {
2163            throw new CmisInvalidArgumentException("Missing object ID or version series ID");
2164        }
2165        if (Boolean.TRUE.equals(major)) {
2166            // we must list all versions
2167            List<DocumentModel> versions = coreSession.getVersions(doc.getRef());
2168            Collections.reverse(versions);
2169            for (DocumentModel ver : versions) {
2170                if (ver.isMajorVersion()) {
2171                    return getObject(repositoryId, ver.getId(), filter, includeAllowableActions, includeRelationships,
2172                            renditionFilter, includePolicyIds, includeAcl, null);
2173                }
2174            }
2175            return null;
2176        } else {
2177            DocumentRef verRef = coreSession.getLastDocumentVersionRef(doc.getRef());
2178            String verId = getIdFromDocumentRef(verRef);
2179            return getObject(repositoryId, verId, filter, includeAllowableActions, includeRelationships,
2180                    renditionFilter, includePolicyIds, includeAcl, null);
2181        }
2182    }
2183
2184    @Override
2185    public Properties getPropertiesOfLatestVersion(String repositoryId, String objectId, String versionSeriesId,
2186            Boolean major, String filter, ExtensionsData extension) {
2187        NuxeoObjectData od = getObjectOfLatestVersion(repositoryId, objectId, versionSeriesId, major, filter,
2188                Boolean.FALSE, IncludeRelationships.NONE, null, Boolean.FALSE, Boolean.FALSE, null);
2189        return od == null ? null : od.getProperties();
2190    }
2191
2192    @Override
2193    public void deleteObject(String repositoryId, String objectId, Boolean allVersions, ExtensionsData extension) {
2194        DocumentModel doc = getDocumentModel(objectId);
2195        if (doc.isFolder()) {
2196            // check that there are no children left
2197            DocumentModelList docs = coreSession.getChildren(new IdRef(objectId), null, documentFilter, null);
2198            if (docs.size() > 0) {
2199                throw new CmisConstraintException("Cannot delete non-empty folder: " + objectId);
2200            }
2201        }
2202        coreSession.removeDocument(doc.getRef());
2203        save();
2204    }
2205
2206    @Override
2207    public void deleteObjectOrCancelCheckOut(String repositoryId, String objectId, Boolean allVersions,
2208            ExtensionsData extension) {
2209        DocumentModel doc = getDocumentModel(objectId);
2210        DocumentRef docRef = doc.getRef();
2211        // find last version
2212        DocumentRef verRef = coreSession.getLastDocumentVersionRef(docRef);
2213        // If doc has versions, is locked, and is checkedOut, then it was
2214        // likely
2215        // explicitly checkedOut so invoke cancelCheckOut not delete
2216        if (verRef != null && doc.isLocked() && doc.isCheckedOut()) {
2217            cancelCheckOut(repositoryId, objectId, extension);
2218        } else {
2219            deleteObject(repositoryId, objectId, allVersions, extension);
2220        }
2221    }
2222
2223}