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