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