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