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