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