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