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