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.client;
013
014import java.math.BigInteger;
015import java.util.ArrayList;
016import java.util.Collection;
017import java.util.Collections;
018import java.util.List;
019import java.util.Locale;
020import java.util.Map;
021import java.util.Set;
022
023import org.apache.chemistry.opencmis.client.api.ChangeEvent;
024import org.apache.chemistry.opencmis.client.api.ChangeEvents;
025import org.apache.chemistry.opencmis.client.api.CmisObject;
026import org.apache.chemistry.opencmis.client.api.Document;
027import org.apache.chemistry.opencmis.client.api.Folder;
028import org.apache.chemistry.opencmis.client.api.ItemIterable;
029import org.apache.chemistry.opencmis.client.api.ObjectId;
030import org.apache.chemistry.opencmis.client.api.ObjectType;
031import org.apache.chemistry.opencmis.client.api.OperationContext;
032import org.apache.chemistry.opencmis.client.api.Policy;
033import org.apache.chemistry.opencmis.client.api.QueryResult;
034import org.apache.chemistry.opencmis.client.api.QueryStatement;
035import org.apache.chemistry.opencmis.client.api.Relationship;
036import org.apache.chemistry.opencmis.client.api.Session;
037import org.apache.chemistry.opencmis.client.api.Tree;
038import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
039import org.apache.chemistry.opencmis.client.runtime.OperationContextImpl;
040import org.apache.chemistry.opencmis.client.runtime.QueryResultImpl;
041import org.apache.chemistry.opencmis.client.runtime.QueryStatementImpl;
042import org.apache.chemistry.opencmis.client.runtime.util.AbstractPageFetcher;
043import org.apache.chemistry.opencmis.client.runtime.util.CollectionIterable;
044import org.apache.chemistry.opencmis.commons.PropertyIds;
045import org.apache.chemistry.opencmis.commons.data.Ace;
046import org.apache.chemistry.opencmis.commons.data.Acl;
047import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
048import org.apache.chemistry.opencmis.commons.data.ContentStream;
049import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
050import org.apache.chemistry.opencmis.commons.data.ObjectData;
051import org.apache.chemistry.opencmis.commons.data.ObjectList;
052import org.apache.chemistry.opencmis.commons.data.Properties;
053import org.apache.chemistry.opencmis.commons.data.PropertyData;
054import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
055import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
056import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
057import org.apache.chemistry.opencmis.commons.enums.BindingType;
058import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
059import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
060import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
061import org.apache.chemistry.opencmis.commons.enums.VersioningState;
062import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
063import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
064import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
065import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
066import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
067import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
068import org.apache.chemistry.opencmis.commons.server.CallContext;
069import org.apache.chemistry.opencmis.commons.server.CmisService;
070import org.apache.commons.lang.StringUtils;
071import org.nuxeo.ecm.core.api.CoreSession;
072import org.nuxeo.ecm.core.api.DocumentModel;
073import org.nuxeo.ecm.core.opencmis.impl.server.NuxeoObjectData;
074
075/**
076 * Nuxeo Persistent Session, having a direct connection to a Nuxeo {@link CoreSession}.
077 */
078public class NuxeoSession implements Session {
079
080    private static final long serialVersionUID = 1L;
081
082    public static final OperationContext DEFAULT_CONTEXT = new OperationContextImpl(null, false, true, false,
083            IncludeRelationships.NONE, null, true, null, true, 10);
084
085    private final CoreSession coreSession;
086
087    private final String repositoryId;
088
089    protected final NuxeoObjectFactory objectFactory;
090
091    private final CmisService service;
092
093    private final NuxeoBinding binding;
094
095    private OperationContext defaultContext = DEFAULT_CONTEXT;
096
097    public NuxeoSession(NuxeoBinding binding, CallContext context) {
098        this.coreSession = binding.getCoreSession();
099        repositoryId = context.getRepositoryId();
100        objectFactory = new NuxeoObjectFactory(this);
101        service = binding.service;
102        this.binding = binding;
103    }
104
105    @Override
106    public NuxeoObjectFactory getObjectFactory() {
107        return objectFactory;
108    }
109
110    @Override
111    public NuxeoBinding getBinding() {
112        return binding;
113    }
114
115    public CmisService getService() {
116        return service;
117    }
118
119    protected CoreSession getCoreSession() {
120        return coreSession;
121    }
122
123    @Override
124    public void clear() {
125    }
126
127    public void save() {
128        coreSession.save();
129    }
130
131    @Override
132    public void setDefaultContext(OperationContext defaultContext) {
133        this.defaultContext = defaultContext;
134    }
135
136    @Override
137    public OperationContext getDefaultContext() {
138        return defaultContext;
139    }
140
141    @Override
142    public Map<String, String> getSessionParameters() {
143        return Collections.emptyMap();
144    }
145
146    protected String getRepositoryId() {
147        return coreSession.getRepositoryName();
148    }
149
150    @Override
151    public ObjectId createObjectId(String id) {
152        return new ObjectIdImpl(id);
153    }
154
155    @Override
156    public ObjectId createDocument(Map<String, ?> properties, ObjectId folderId, ContentStream contentStream,
157            VersioningState versioningState) {
158        return createDocument(properties, folderId, contentStream, versioningState, null, null, null);
159    }
160
161    /** Converts from an untyped map to a {@link Properties} object. */
162    protected Properties convertProperties(Map<String, ?> properties) {
163        if (properties == null) {
164            return null;
165        }
166        // find type
167        String typeId = (String) properties.get(PropertyIds.OBJECT_TYPE_ID);
168        if (typeId == null) {
169            throw new IllegalArgumentException("Missing type");
170        }
171        ObjectType type = getTypeDefinition(typeId);
172        if (type == null) {
173            throw new IllegalArgumentException("Unknown type: " + typeId);
174        }
175        return objectFactory.convertProperties(properties, type, null, null);
176    }
177
178    @Override
179    public ObjectId createDocument(Map<String, ?> properties, ObjectId folderId, ContentStream contentStream,
180            VersioningState versioningState, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
181        String id = service.createDocument(repositoryId, convertProperties(properties), folderId == null ? null
182                : folderId.getId(), contentStream, versioningState, objectFactory.convertPolicies(policies),
183                objectFactory.convertAces(addAces), objectFactory.convertAces(removeAces), null);
184        return createObjectId(id);
185    }
186
187    @Override
188    public ObjectId createFolder(Map<String, ?> properties, ObjectId folderId) {
189        return createFolder(properties, folderId, null, null, null);
190    }
191
192    @Override
193    public ObjectId createFolder(Map<String, ?> properties, ObjectId folderId, List<Policy> policies,
194            List<Ace> addAces, List<Ace> removeAces) {
195        String id = service.createFolder(repositoryId, convertProperties(properties), folderId == null ? null
196                : folderId.getId(), objectFactory.convertPolicies(policies), objectFactory.convertAces(addAces),
197                objectFactory.convertAces(removeAces), null);
198        return createObjectId(id);
199    }
200
201    @Override
202    public OperationContext createOperationContext() {
203        return new OperationContextImpl();
204    }
205
206    @Override
207    public OperationContext createOperationContext(Set<String> filter, boolean includeAcls,
208            boolean includeAllowableActions, boolean includePolicies, IncludeRelationships includeRelationships,
209            Set<String> renditionFilter, boolean includePathSegments, String orderBy, boolean cacheEnabled,
210            int maxItemsPerPage) {
211        // TODO Auto-generated method stub
212        throw new UnsupportedOperationException();
213    }
214
215    @Override
216    public ObjectId createPolicy(Map<String, ?> properties, ObjectId folderId) {
217        return createPolicy(properties, folderId, null, null, null);
218    }
219
220    @Override
221    public ObjectId createPolicy(Map<String, ?> properties, ObjectId folderId, List<Policy> policies,
222            List<Ace> addAces, List<Ace> removeAces) {
223        // TODO Auto-generated method stub
224        throw new UnsupportedOperationException();
225    }
226
227    @Override
228    public ObjectId createRelationship(Map<String, ?> properties) {
229        return createRelationship(properties, null, null, null);
230    }
231
232    @Override
233    public ObjectId createRelationship(Map<String, ?> properties, List<Policy> policies, List<Ace> addAces,
234            List<Ace> removeAces) {
235        String id = service.createRelationship(repositoryId, convertProperties(properties),
236                objectFactory.convertPolicies(policies), objectFactory.convertAces(addAces),
237                objectFactory.convertAces(removeAces), null);
238        return createObjectId(id);
239    }
240
241    @Override
242    public ObjectId createItem(Map<String, ?> properties, ObjectId folderId, List<Policy> policies, List<Ace> addAces,
243            List<Ace> removeAces) {
244        throw new CmisNotSupportedException();
245    }
246
247    @Override
248    public ObjectId createItem(Map<String, ?> properties, ObjectId folderId) {
249        throw new CmisNotSupportedException();
250    }
251
252    @Override
253    public ObjectId createDocumentFromSource(ObjectId source, Map<String, ?> properties, ObjectId folderId,
254            VersioningState versioningState) {
255        return createDocumentFromSource(source, properties, folderId, versioningState, null, null, null);
256    }
257
258    @Override
259    public ObjectId createDocumentFromSource(ObjectId source, Map<String, ?> properties, ObjectId folderId,
260            VersioningState versioningState, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
261        // TODO Auto-generated method stub
262        throw new UnsupportedOperationException();
263    }
264
265    @Override
266    public ItemIterable<Document> getCheckedOutDocs() {
267        // TODO Auto-generated method stub
268        throw new UnsupportedOperationException();
269    }
270
271    @Override
272    public ItemIterable<Document> getCheckedOutDocs(OperationContext context) {
273        // TODO Auto-generated method stub
274        throw new UnsupportedOperationException();
275    }
276
277    @Override
278    public ChangeEvents getContentChanges(String changeLogToken, boolean includeProperties, long maxNumItems) {
279        return getContentChanges(changeLogToken, includeProperties, maxNumItems, getDefaultContext());
280    }
281
282    @Override
283    public ChangeEvents getContentChanges(String changeLogToken, boolean includeProperties, long maxNumItems,
284            OperationContext context) {
285        // TODO Auto-generated method stub
286        throw new UnsupportedOperationException();
287    }
288
289    @Override
290    public ItemIterable<ChangeEvent> getContentChanges(String changeLogToken, boolean includeProperties) {
291        return getContentChanges(changeLogToken, includeProperties, getDefaultContext());
292    };
293
294    @Override
295    public ItemIterable<ChangeEvent> getContentChanges(String changeLogToken, boolean includeProperties,
296            OperationContext context) {
297        // TODO Auto-generated method stub
298        throw new UnsupportedOperationException();
299    };
300
301    @Override
302    public Locale getLocale() {
303        // TODO Auto-generated method stub
304        throw new UnsupportedOperationException();
305    }
306
307    @Override
308    public boolean exists(ObjectId objectId) {
309        return exists(objectId.getId());
310    }
311
312    @Override
313    public boolean exists(String objectId) {
314        try {
315            service.getObject(repositoryId, objectId, PropertyIds.OBJECT_ID, Boolean.FALSE, IncludeRelationships.NONE,
316                    "cmis:none", Boolean.FALSE, Boolean.FALSE, null);
317            return true;
318        } catch (CmisObjectNotFoundException e) {
319            return false;
320        }
321    }
322
323    @Override
324    public boolean existsPath(String parentPath, String name) {
325        if (parentPath == null || !parentPath.startsWith("/")) {
326            throw new CmisInvalidArgumentException("Invalid parent path: " + parentPath);
327        }
328        if (StringUtils.isEmpty(name)) {
329            throw new CmisInvalidArgumentException("Invalid empty name: " + name);
330        }
331        StringBuilder path = new StringBuilder(parentPath);
332        if (!parentPath.endsWith("/")) {
333            path.append('/');
334        }
335        path.append(name);
336        return existsPath(path.toString());
337    }
338
339    @Override
340    public boolean existsPath(String path) {
341        try {
342            service.getObjectByPath(repositoryId, path, PropertyIds.OBJECT_ID, Boolean.FALSE, IncludeRelationships.NONE,
343                    "cmis:none", Boolean.FALSE, Boolean.FALSE, null);
344            return true;
345        } catch (CmisObjectNotFoundException e) {
346            return false;
347        }
348    }
349
350    @Override
351    public CmisObject getObject(ObjectId objectId) {
352        return getObject(objectId, getDefaultContext());
353    }
354
355    @Override
356    public CmisObject getObject(String objectId) {
357        return getObject(objectId, getDefaultContext());
358    }
359
360    /** Gets a CMIS object given a Nuxeo {@link DocumentModel}. */
361    public CmisObject getObject(DocumentModel doc, OperationContext context) {
362        ObjectData data = new NuxeoObjectData(service, doc, context);
363        return objectFactory.convertObject(data, context);
364    }
365
366    @Override
367    public CmisObject getObject(ObjectId objectId, OperationContext context) {
368        if (objectId == null) {
369            throw new CmisInvalidArgumentException("Missing object ID");
370        }
371        return getObject(objectId.getId(), context);
372    }
373
374    @Override
375    public CmisObject getObject(String objectId, OperationContext context) {
376        if (objectId == null) {
377            throw new CmisInvalidArgumentException("Missing object ID");
378        }
379        if (context == null) {
380            throw new CmisInvalidArgumentException("Missing operation context");
381        }
382        ObjectData data = service.getObject(repositoryId, objectId, context.getFilterString(),
383                Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
384                context.getRenditionFilterString(), Boolean.valueOf(context.isIncludePolicies()),
385                Boolean.valueOf(context.isIncludeAcls()), null);
386        return objectFactory.convertObject(data, context);
387    }
388
389    @Override
390    public CmisObject getObjectByPath(String path) {
391        return getObjectByPath(path, getDefaultContext());
392    }
393
394    @Override
395    public CmisObject getObjectByPath(String parentPath, String name) {
396        return getObjectByPath(parentPath, name, getDefaultContext());
397    }
398
399    @Override
400    public CmisObject getObjectByPath(String parentPath, String name, OperationContext context) {
401        if (parentPath == null || !parentPath.startsWith("/")) {
402            throw new CmisInvalidArgumentException("Invalid parent path: " + parentPath);
403        }
404        if (StringUtils.isEmpty(name)) {
405            throw new CmisInvalidArgumentException("Invalid empty name: " + name);
406        }
407        StringBuilder path = new StringBuilder(parentPath);
408        if (!parentPath.endsWith("/")) {
409            path.append('/');
410        }
411        path.append(name);
412        return getObjectByPath(path.toString(), context);
413    }
414
415    @Override
416    public CmisObject getObjectByPath(String path, OperationContext context) {
417        if (path == null || !path.startsWith("/")) {
418            throw new CmisInvalidArgumentException("Invalid path: " + path);
419        }
420        if (context == null) {
421            throw new CmisInvalidArgumentException("Missing operation context");
422        }
423        ObjectData data = service.getObjectByPath(repositoryId, path, context.getFilterString(),
424                Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
425                context.getRenditionFilterString(), Boolean.valueOf(context.isIncludePolicies()),
426                Boolean.valueOf(context.isIncludeAcls()), null);
427        return getObjectFactory().convertObject(data, context);
428    }
429
430    @Override
431    public RepositoryInfo getRepositoryInfo() {
432        return service.getRepositoryInfo(repositoryId, null);
433    }
434
435    @Override
436    public Folder getRootFolder() {
437        return getRootFolder(getDefaultContext());
438    }
439
440    @Override
441    public Folder getRootFolder(OperationContext context) {
442        String id = getRepositoryInfo().getRootFolderId();
443        CmisObject folder = getObject(createObjectId(id), context);
444        if (!(folder instanceof Folder)) {
445            throw new CmisRuntimeException("Root object is not a Folder but: " + folder.getClass().getName());
446        }
447        return (Folder) folder;
448    }
449
450    @Override
451    public ItemIterable<ObjectType> getTypeChildren(String typeId, boolean includePropertyDefinitions) {
452        // TODO Auto-generated method stub
453        throw new UnsupportedOperationException();
454    }
455
456    @Override
457    public ObjectType getTypeDefinition(String typeId) {
458        TypeDefinition typeDefinition = service.getTypeDefinition(repositoryId, typeId, null);
459        return objectFactory.convertTypeDefinition(typeDefinition);
460    }
461
462    @Override
463    public ObjectType getTypeDefinition(String typeId, boolean useCache) {
464        return getTypeDefinition(typeId);
465    }
466
467    @Override
468    public List<Tree<ObjectType>> getTypeDescendants(String typeId, int depth, boolean includePropertyDefinitions) {
469        // TODO Auto-generated method stub
470        throw new UnsupportedOperationException();
471    }
472
473    @Override
474    public ItemIterable<QueryResult> query(String statement, boolean searchAllVersions) {
475        // TODO Auto-generated method stub
476        throw new UnsupportedOperationException();
477    }
478
479    @Override
480    public ItemIterable<QueryResult> query(final String statement, final boolean searchAllVersions,
481            final OperationContext context) {
482        AbstractPageFetcher<QueryResult> pageFetcher = new AbstractPageFetcher<QueryResult>(
483                context.getMaxItemsPerPage()) {
484            @Override
485            protected Page<QueryResult> fetchPage(long skipCount) {
486                ObjectList results = service.query(repositoryId, statement, Boolean.valueOf(searchAllVersions),
487                        Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
488                        context.getRenditionFilterString(), BigInteger.valueOf(maxNumItems),
489                        BigInteger.valueOf(skipCount), null);
490                // convert objects
491                List<QueryResult> page = new ArrayList<QueryResult>();
492                if (results.getObjects() != null) {
493                    for (ObjectData data : results.getObjects()) {
494                        page.add(new QueryResultImpl(NuxeoSession.this, data));
495                    }
496                }
497                return new Page<QueryResult>(page, results.getNumItems(), results.hasMoreItems());
498            }
499        };
500        return new CollectionIterable<QueryResult>(pageFetcher);
501    }
502
503    @Override
504    public ItemIterable<CmisObject> queryObjects(String typeId, String where, boolean searchAllVersions,
505            OperationContext context) {
506        // TODO Auto-generated method stub
507        throw new UnsupportedOperationException();
508    }
509
510    @Override
511    public QueryStatement createQueryStatement(String statement) {
512        return new QueryStatementImpl(this, statement);
513    }
514
515    @Override
516    public QueryStatement createQueryStatement(Collection<String> selectPropertyIds, Map<String, String> fromTypes,
517            String whereClause, List<String> orderByPropertyIds) {
518        return new QueryStatementImpl(this, selectPropertyIds, fromTypes, whereClause, orderByPropertyIds);
519    }
520
521    @Override
522    public ItemIterable<Relationship> getRelationships(final ObjectId objectId,
523            final boolean includeSubRelationshipTypes, final RelationshipDirection relationshipDirection,
524            final ObjectType type, final OperationContext context) {
525        final String typeId = type == null ? null : type.getId();
526        AbstractPageFetcher<Relationship> pageFetcher = new AbstractPageFetcher<Relationship>(
527                context.getMaxItemsPerPage()) {
528            @Override
529            protected Page<Relationship> fetchPage(long skipCount) {
530                ObjectList relations = service.getObjectRelationships(repositoryId, objectId.getId(),
531                        Boolean.valueOf(includeSubRelationshipTypes), relationshipDirection, typeId, null, null,
532                        BigInteger.valueOf(maxNumItems), BigInteger.valueOf(skipCount), null);
533                // convert objects
534                List<Relationship> page = new ArrayList<Relationship>();
535                if (relations.getObjects() != null) {
536                    for (ObjectData data : relations.getObjects()) {
537                        CmisObject ob;
538                        if (data instanceof NuxeoObjectData) {
539                            ob = objectFactory.convertObject(data, context);
540                        } else {
541                            ob = getObject(data.getId(), context);
542                        }
543                        if (!(ob instanceof Relationship)) {
544                            // should not happen...
545                            continue;
546                        }
547                        page.add((Relationship) ob);
548                    }
549                }
550                return new Page<Relationship>(page, relations.getNumItems(), relations.hasMoreItems());
551            }
552        };
553        return new CollectionIterable<Relationship>(pageFetcher);
554    }
555
556    @Override
557    public Acl getAcl(ObjectId objectId, boolean onlyBasicPermissions) {
558        return service.getAcl(repositoryId, objectId.getId(), Boolean.valueOf(onlyBasicPermissions), null);
559    }
560
561    @Override
562    public Acl setAcl(ObjectId objectId, List<Ace> aces) {
563        return service.applyAcl(repositoryId, objectId.getId(), new AccessControlListImpl(aces), null);
564    }
565
566    @Override
567    public Acl applyAcl(ObjectId objectId, List<Ace> addAces, List<Ace> removeAces, AclPropagation aclPropagation) {
568        return service.applyAcl(repositoryId, objectId.getId(), new AccessControlListImpl(addAces),
569                new AccessControlListImpl(removeAces), aclPropagation, null);
570    }
571
572    @Override
573    public void applyPolicy(ObjectId objectId, ObjectId... policyIds) {
574        throw new CmisNotSupportedException();
575    }
576
577    @Override
578    public void removePolicy(ObjectId objectId, ObjectId... policyIds) {
579        throw new CmisNotSupportedException();
580    }
581
582    @Override
583    public void removeObjectFromCache(ObjectId objectId) {
584    }
585
586    @Override
587    public void removeObjectFromCache(String objectId) {
588    }
589
590    @Override
591    public void delete(ObjectId objectId) {
592        delete(objectId, true);
593    }
594
595    @Override
596    public void delete(ObjectId objectId, boolean allVersions) {
597        service.deleteObject(repositoryId, objectId.getId(), Boolean.valueOf(allVersions), null);
598    }
599
600    @Override
601    public List<String> deleteTree(ObjectId folderId, boolean allVersions, UnfileObject unfile,
602            boolean continueOnFailure) {
603        FailedToDeleteData res = service.deleteTree(repositoryId, folderId.getId(), Boolean.valueOf(allVersions),
604                unfile, Boolean.valueOf(continueOnFailure), null);
605        return res.getIds();
606    }
607
608    @Override
609    public ContentStream getContentStream(ObjectId docId) {
610        return getContentStream(docId, null, null, null);
611    }
612
613    @Override
614    public ContentStream getContentStream(ObjectId docId, String streamId, BigInteger offset, BigInteger length) {
615        if (docId == null) {
616            throw new CmisInvalidArgumentException("Missing object ID");
617        }
618        return service.getContentStream(repositoryId, docId.getId(), streamId, offset, length, null);
619    }
620
621    @Override
622    public ObjectType createType(TypeDefinition type) {
623        throw new CmisNotSupportedException();
624    }
625
626    @Override
627    public ObjectType updateType(TypeDefinition type) {
628        throw new CmisNotSupportedException();
629    }
630
631    @Override
632    public void deleteType(String typeId) {
633        throw new CmisNotSupportedException();
634    }
635
636    @Override
637    public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(List<CmisObject> objects,
638            Map<String, ?> properties, List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds) {
639        List<BulkUpdateObjectIdAndChangeToken> idts = new ArrayList<BulkUpdateObjectIdAndChangeToken>(objects.size());
640        for (CmisObject object : objects) {
641            idts.add(new BulkUpdateObjectIdAndChangeTokenImpl(object.getId(), object.getChangeToken()));
642        }
643        return service.bulkUpdateProperties(repositoryId, idts, convertProperties(properties), addSecondaryTypeIds,
644                removeSecondaryTypeIds, null);
645    }
646
647    @Override
648    public Document getLatestDocumentVersion(ObjectId objectId) {
649        return getLatestDocumentVersion(objectId, false, getDefaultContext());
650    }
651
652    @Override
653    public Document getLatestDocumentVersion(String objectId, OperationContext context) {
654        if (objectId == null) {
655            throw new IllegalArgumentException("Object ID must be set!");
656        }
657        return getLatestDocumentVersion(createObjectId(objectId), false, context);
658    }
659
660    @Override
661    public Document getLatestDocumentVersion(String objectId, boolean major, OperationContext context) {
662        if (objectId == null) {
663            throw new IllegalArgumentException("Object ID must be set!");
664        }
665        return getLatestDocumentVersion(createObjectId(objectId), major, context);
666    }
667
668    @Override
669    public Document getLatestDocumentVersion(String objectId) {
670        if (objectId == null) {
671            throw new IllegalArgumentException("Object ID must be set!");
672        }
673        return getLatestDocumentVersion(createObjectId(objectId), false, getDefaultContext());
674    }
675
676    @Override
677    public Document getLatestDocumentVersion(ObjectId objectId, OperationContext context) {
678        return getLatestDocumentVersion(objectId, false, context);
679    }
680
681    @Override
682    /**
683     * @See org.apache.chemistry.opencmis.client.runtime.SessionImpl
684     */
685    public Document getLatestDocumentVersion(ObjectId objectId, boolean major, OperationContext context) {
686        if (objectId == null || objectId.getId() == null) {
687            throw new IllegalArgumentException("Object ID must be set!");
688        }
689
690        if (context == null) {
691            throw new IllegalArgumentException("Operation context must be set!");
692        }
693
694        CmisObject result = null;
695
696        String versionSeriesId = null;
697
698        // first attempt: if we got a Document object, try getting the version
699        // series ID from it
700        if (objectId instanceof Document) {
701            versionSeriesId = ((Document) objectId).getVersionSeriesId();
702        }
703
704        // third attempt (Web Services only): get the version series ID from the
705        // repository
706        // (the AtomPub and Browser binding don't need the version series ID ->
707        // avoid roundtrip)
708        if (versionSeriesId == null) {
709            BindingType bindingType = getBinding().getBindingType();
710            if (bindingType == BindingType.WEBSERVICES || bindingType == BindingType.CUSTOM) {
711
712                // get the document to find the version series ID
713                ObjectData sourceObjectData = binding.getObjectService().getObject(getRepositoryId(), objectId.getId(),
714                        PropertyIds.OBJECT_ID + "," + PropertyIds.VERSION_SERIES_ID, false, IncludeRelationships.NONE,
715                        "cmis:none", false, false, null);
716
717                if (sourceObjectData.getProperties() != null
718                        && sourceObjectData.getProperties().getProperties() != null) {
719                    PropertyData<?> verionsSeriesIdProp = sourceObjectData.getProperties().getProperties().get(
720                            PropertyIds.VERSION_SERIES_ID);
721                    if (verionsSeriesIdProp != null && verionsSeriesIdProp.getFirstValue() instanceof String) {
722                        versionSeriesId = (String) verionsSeriesIdProp.getFirstValue();
723                    }
724                }
725
726                // the Web Services binding needs the version series ID -> fail
727                if (versionSeriesId == null) {
728                    throw new IllegalArgumentException("Object is not a document or not versionable!");
729                }
730            }
731        }
732
733        // get the object
734        ObjectData objectData = binding.getVersioningService().getObjectOfLatestVersion(getRepositoryId(),
735                objectId.getId(), versionSeriesId, major, context.getFilterString(),
736                context.isIncludeAllowableActions(), context.getIncludeRelationships(),
737                context.getRenditionFilterString(), context.isIncludePolicies(), context.isIncludeAcls(), null);
738
739        result = getObjectFactory().convertObject(objectData, context);
740
741        // check result
742        if (!(result instanceof Document)) {
743            throw new IllegalArgumentException("Latest version is not a document!");
744        }
745
746        return (Document) result;
747    }
748
749    @Override
750    public String getLatestChangeLogToken() {
751        return getBinding().getRepositoryService().getRepositoryInfo(getRepositoryId(), null).getLatestChangeLogToken();
752    }
753
754}