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