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