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