001/*
002 * (C) Copyright 2006-2011 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     Florent Guillaume
018 */
019package org.nuxeo.ecm.core.opencmis.impl.client;
020
021import static org.apache.chemistry.opencmis.commons.impl.Constants.RENDITION_NONE;
022
023import java.math.BigInteger;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Locale;
030import java.util.Map;
031import java.util.Set;
032
033import org.apache.chemistry.opencmis.client.api.ChangeEvent;
034import org.apache.chemistry.opencmis.client.api.ChangeEvents;
035import org.apache.chemistry.opencmis.client.api.CmisObject;
036import org.apache.chemistry.opencmis.client.api.Document;
037import org.apache.chemistry.opencmis.client.api.Folder;
038import org.apache.chemistry.opencmis.client.api.ItemIterable;
039import org.apache.chemistry.opencmis.client.api.ObjectId;
040import org.apache.chemistry.opencmis.client.api.ObjectType;
041import org.apache.chemistry.opencmis.client.api.OperationContext;
042import org.apache.chemistry.opencmis.client.api.Policy;
043import org.apache.chemistry.opencmis.client.api.QueryResult;
044import org.apache.chemistry.opencmis.client.api.QueryStatement;
045import org.apache.chemistry.opencmis.client.api.Relationship;
046import org.apache.chemistry.opencmis.client.api.Session;
047import org.apache.chemistry.opencmis.client.api.Tree;
048import org.apache.chemistry.opencmis.client.runtime.ObjectIdImpl;
049import org.apache.chemistry.opencmis.client.runtime.OperationContextImpl;
050import org.apache.chemistry.opencmis.client.runtime.QueryResultImpl;
051import org.apache.chemistry.opencmis.client.runtime.QueryStatementImpl;
052import org.apache.chemistry.opencmis.client.runtime.util.AbstractPageFetcher;
053import org.apache.chemistry.opencmis.client.runtime.util.CollectionIterable;
054import org.apache.chemistry.opencmis.commons.PropertyIds;
055import org.apache.chemistry.opencmis.commons.data.Ace;
056import org.apache.chemistry.opencmis.commons.data.Acl;
057import org.apache.chemistry.opencmis.commons.data.BulkUpdateObjectIdAndChangeToken;
058import org.apache.chemistry.opencmis.commons.data.ContentStream;
059import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
060import org.apache.chemistry.opencmis.commons.data.ObjectData;
061import org.apache.chemistry.opencmis.commons.data.ObjectList;
062import org.apache.chemistry.opencmis.commons.data.Properties;
063import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
064import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
065import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
066import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
067import org.apache.chemistry.opencmis.commons.enums.IncludeRelationships;
068import org.apache.chemistry.opencmis.commons.enums.RelationshipDirection;
069import org.apache.chemistry.opencmis.commons.enums.UnfileObject;
070import org.apache.chemistry.opencmis.commons.enums.VersioningState;
071import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
072import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
073import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
074import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
075import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
076import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
077import org.apache.chemistry.opencmis.commons.impl.dataobjects.BulkUpdateObjectIdAndChangeTokenImpl;
078import org.apache.chemistry.opencmis.commons.server.CallContext;
079import org.apache.chemistry.opencmis.commons.server.CmisService;
080import org.apache.commons.lang3.StringUtils;
081import org.nuxeo.ecm.core.api.CoreSession;
082import org.nuxeo.ecm.core.api.DocumentModel;
083import org.nuxeo.ecm.core.opencmis.impl.server.NuxeoObjectData;
084
085/**
086 * Nuxeo Persistent Session, having a direct connection to a Nuxeo {@link CoreSession}.
087 */
088public class NuxeoSession implements Session {
089
090    private static final long serialVersionUID = 1L;
091
092    public static final OperationContext DEFAULT_CONTEXT = new OperationContextImpl(null, false, true, false,
093            IncludeRelationships.NONE, null, true, null, true, 10);
094
095    private final CoreSession coreSession;
096
097    private final String repositoryId;
098
099    protected final NuxeoObjectFactory objectFactory;
100
101    private final CmisService service;
102
103    private final NuxeoBinding binding;
104
105    private OperationContext defaultContext = DEFAULT_CONTEXT;
106
107    public NuxeoSession(NuxeoBinding binding, CallContext context) {
108        this.coreSession = binding.getCoreSession();
109        repositoryId = context.getRepositoryId();
110        objectFactory = new NuxeoObjectFactory(this);
111        service = binding.service;
112        this.binding = binding;
113    }
114
115    @Override
116    public NuxeoObjectFactory getObjectFactory() {
117        return objectFactory;
118    }
119
120    @Override
121    public NuxeoBinding getBinding() {
122        return binding;
123    }
124
125    public CmisService getService() {
126        return service;
127    }
128
129    protected CoreSession getCoreSession() {
130        return coreSession;
131    }
132
133    @Override
134    public void clear() {
135    }
136
137    public void save() {
138        coreSession.save();
139    }
140
141    @Override
142    public void setDefaultContext(OperationContext defaultContext) {
143        this.defaultContext = defaultContext;
144    }
145
146    @Override
147    public OperationContext getDefaultContext() {
148        return defaultContext;
149    }
150
151    @Override
152    public Map<String, String> getSessionParameters() {
153        return Collections.emptyMap();
154    }
155
156    protected String getRepositoryId() {
157        return coreSession.getRepositoryName();
158    }
159
160    @Override
161    public ObjectId createObjectId(String id) {
162        return new ObjectIdImpl(id);
163    }
164
165    @Override
166    public ObjectId createDocument(Map<String, ?> properties, ObjectId folderId, ContentStream contentStream,
167            VersioningState versioningState) {
168        return createDocument(properties, folderId, contentStream, versioningState, null, null, null);
169    }
170
171    /** Converts from an untyped map to a {@link Properties} object. */
172    protected Properties convertProperties(Map<String, ?> properties) {
173        if (properties == null) {
174            return null;
175        }
176        // find type
177        String typeId = (String) properties.get(PropertyIds.OBJECT_TYPE_ID);
178        if (typeId == null) {
179            throw new IllegalArgumentException("Missing type");
180        }
181        ObjectType type = getTypeDefinition(typeId);
182        if (type == null) {
183            throw new IllegalArgumentException("Unknown type: " + typeId);
184        }
185        return objectFactory.convertProperties(properties, type, null, null);
186    }
187
188    @Override
189    public ObjectId createDocument(Map<String, ?> properties, ObjectId folderId, ContentStream contentStream,
190            VersioningState versioningState, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
191        String id = service.createDocument(repositoryId, convertProperties(properties), folderId == null ? null
192                : folderId.getId(), contentStream, versioningState, objectFactory.convertPolicies(policies),
193                objectFactory.convertAces(addAces), objectFactory.convertAces(removeAces), null);
194        return createObjectId(id);
195    }
196
197    @Override
198    public ObjectId createFolder(Map<String, ?> properties, ObjectId folderId) {
199        return createFolder(properties, folderId, null, null, null);
200    }
201
202    @Override
203    public ObjectId createFolder(Map<String, ?> properties, ObjectId folderId, List<Policy> policies,
204            List<Ace> addAces, List<Ace> removeAces) {
205        String id = service.createFolder(repositoryId, convertProperties(properties), folderId == null ? null
206                : folderId.getId(), objectFactory.convertPolicies(policies), objectFactory.convertAces(addAces),
207                objectFactory.convertAces(removeAces), null);
208        return createObjectId(id);
209    }
210
211    @Override
212    public OperationContext createOperationContext() {
213        return new OperationContextImpl();
214    }
215
216    @Override
217    public OperationContext createOperationContext(Set<String> filter, boolean includeAcls,
218            boolean includeAllowableActions, boolean includePolicies, IncludeRelationships includeRelationships,
219            Set<String> renditionFilter, boolean includePathSegments, String orderBy, boolean cacheEnabled,
220            int maxItemsPerPage) {
221        // TODO Auto-generated method stub
222        throw new UnsupportedOperationException();
223    }
224
225    @Override
226    public ObjectId createPolicy(Map<String, ?> properties, ObjectId folderId) {
227        return createPolicy(properties, folderId, null, null, null);
228    }
229
230    @Override
231    public ObjectId createPolicy(Map<String, ?> properties, ObjectId folderId, List<Policy> policies,
232            List<Ace> addAces, List<Ace> removeAces) {
233        // TODO Auto-generated method stub
234        throw new UnsupportedOperationException();
235    }
236
237    @Override
238    public ObjectId createRelationship(Map<String, ?> properties) {
239        return createRelationship(properties, null, null, null);
240    }
241
242    @Override
243    public ObjectId createRelationship(Map<String, ?> properties, List<Policy> policies, List<Ace> addAces,
244            List<Ace> removeAces) {
245        String id = service.createRelationship(repositoryId, convertProperties(properties),
246                objectFactory.convertPolicies(policies), objectFactory.convertAces(addAces),
247                objectFactory.convertAces(removeAces), null);
248        return createObjectId(id);
249    }
250
251    @Override
252    public ObjectId createItem(Map<String, ?> properties, ObjectId folderId, List<Policy> policies, List<Ace> addAces,
253            List<Ace> removeAces) {
254        throw new CmisNotSupportedException();
255    }
256
257    @Override
258    public ObjectId createItem(Map<String, ?> properties, ObjectId folderId) {
259        throw new CmisNotSupportedException();
260    }
261
262    @Override
263    public ObjectId createPath(String newPath, Map<String, ?> properties) {
264        return createPath(null, newPath, properties);
265    }
266
267    @Override
268    public ObjectId createPath(ObjectId startFolderId, String newPath, Map<String, ?> properties) {
269        return createPath(startFolderId, newPath, properties, null, null, null);
270    }
271
272    @Override
273    public ObjectId createPath(String newPath, String typeId) {
274        return createPath(null, newPath, typeId);
275    }
276
277    @Override
278    public ObjectId createPath(ObjectId startFolderId, String newPath, String typeId) {
279        Map<String, Object> properties = Collections.singletonMap(PropertyIds.OBJECT_TYPE_ID, typeId);
280        return createPath(startFolderId, newPath, properties, null, null, null);
281    }
282
283    @Override
284    public ObjectId createPath(ObjectId startFolderId, String newPath, Map<String, ?> properties, List<Policy> policies,
285            List<Ace> addAces, List<Ace> removeAces) {
286        checkPath(newPath);
287        if (newPath.length() == 1) {
288            throw new CmisInvalidArgumentException("Cannot create root folder");
289        }
290        if (newPath.endsWith("/")) {
291            throw new CmisInvalidArgumentException("Path cannot end with a slash");
292        }
293        if (properties == null || properties.isEmpty()) {
294            throw new CmisInvalidArgumentException("Properties must not be empty");
295        }
296        if (!(properties.get(PropertyIds.OBJECT_TYPE_ID) instanceof String)) {
297            throw new CmisInvalidArgumentException("Property cmis:objectTypeId not set or invalid");
298        }
299        StringBuilder nextPath = new StringBuilder();
300        String[] segments;
301        ObjectId lastFolderId = null;
302        boolean create = false;
303
304        // check start folder
305        if (startFolderId != null && startFolderId.getId() != null) {
306            if (startFolderId instanceof Folder) {
307                Folder startFolder = (Folder) startFolderId;
308                if (!startFolder.isRootFolder()) {
309                    nextPath.append(startFolder.getPath());
310                    lastFolderId = startFolder;
311                }
312            } else {
313                String filter = PropertyIds.OBJECT_ID + ',' + PropertyIds.BASE_TYPE_ID + ',' + PropertyIds.PATH;
314                ObjectData startFolderData = service.getObject(repositoryId, startFolderId.getId(), filter,
315                        Boolean.FALSE, IncludeRelationships.NONE, RENDITION_NONE, Boolean.FALSE, Boolean.FALSE, null);
316                if (startFolderData.getBaseTypeId() != BaseTypeId.CMIS_FOLDER) {
317                    throw new CmisInvalidArgumentException("Start folder is not a folder");
318                }
319                if (startFolderData.getProperties() == null || startFolderData.getProperties().getProperties() == null
320                        || startFolderData.getProperties().getProperties().get(PropertyIds.PATH) == null) {
321                    throw new CmisInvalidArgumentException("Start folder has no path property");
322                }
323                String startPath = (String) startFolderData.getProperties()
324                                                           .getProperties()
325                                                           .get(PropertyIds.PATH)
326                                                           .getFirstValue();
327                if (!getRepositoryInfo().getRootFolderId().equals(startFolderData.getId())) {
328                    nextPath.append(startPath);
329                    lastFolderId = startFolderId;
330                }
331            }
332            if (!newPath.startsWith(nextPath.toString())) {
333                throw new CmisInvalidArgumentException("Start folder in not in the path");
334            }
335            segments = newPath.substring(nextPath.length()).split("/");
336        } else {
337            segments = newPath.split("/");
338        }
339
340        // create folders
341        for (int i = 1; i < segments.length; i++) {
342            String segment = segments[i];
343            if (create) {
344                lastFolderId = createFolder(propertiesWithName(properties, segment), lastFolderId, policies, addAces,
345                        removeAces);
346            } else {
347                try {
348                    nextPath.append('/');
349                    nextPath.append(segment);
350                    String filter = PropertyIds.OBJECT_ID + ',' + PropertyIds.BASE_TYPE_ID;
351                    ObjectData folderData = service.getObjectByPath(repositoryId, nextPath.toString(), filter,
352                            Boolean.FALSE, IncludeRelationships.NONE, RENDITION_NONE, Boolean.FALSE, Boolean.FALSE,
353                            null);
354                    if (folderData.getBaseTypeId() != BaseTypeId.CMIS_FOLDER) {
355                        throw new CmisConstraintException("Cannot create folder " + segment
356                                + " because there is already an object with this name which is not a folder");
357                    }
358                    lastFolderId = new ObjectIdImpl(folderData.getId());
359                } catch (CmisObjectNotFoundException e) {
360                    if (lastFolderId == null) {
361                        lastFolderId = new ObjectIdImpl(getRepositoryInfo().getRootFolderId());
362                    }
363                    lastFolderId = createFolder(propertiesWithName(properties, segment), lastFolderId, policies,
364                            addAces, removeAces);
365                    create = true;
366                }
367            }
368        }
369        return lastFolderId;
370    }
371
372    protected Map<String, Object> propertiesWithName(Map<String, ?> properties, String name) {
373        Map<String, Object> newProperties = new HashMap<>(properties);
374        newProperties.put(PropertyIds.NAME, name);
375        return newProperties;
376    }
377
378    @Override
379    public ObjectId createDocumentFromSource(ObjectId source, Map<String, ?> properties, ObjectId folderId,
380            VersioningState versioningState) {
381        return createDocumentFromSource(source, properties, folderId, versioningState, null, null, null);
382    }
383
384    @Override
385    public ObjectId createDocumentFromSource(ObjectId source, Map<String, ?> properties, ObjectId folderId,
386            VersioningState versioningState, List<Policy> policies, List<Ace> addAces, List<Ace> removeAces) {
387        // TODO Auto-generated method stub
388        throw new UnsupportedOperationException();
389    }
390
391    @Override
392    public ItemIterable<Document> getCheckedOutDocs() {
393        // TODO Auto-generated method stub
394        throw new UnsupportedOperationException();
395    }
396
397    @Override
398    public ItemIterable<Document> getCheckedOutDocs(OperationContext context) {
399        // TODO Auto-generated method stub
400        throw new UnsupportedOperationException();
401    }
402
403    @Override
404    public ChangeEvents getContentChanges(String changeLogToken, boolean includeProperties, long maxNumItems) {
405        return getContentChanges(changeLogToken, includeProperties, maxNumItems, getDefaultContext());
406    }
407
408    @Override
409    public ChangeEvents getContentChanges(String changeLogToken, boolean includeProperties, long maxNumItems,
410            OperationContext context) {
411        // TODO Auto-generated method stub
412        throw new UnsupportedOperationException();
413    }
414
415    @Override
416    public ItemIterable<ChangeEvent> getContentChanges(String changeLogToken, boolean includeProperties) {
417        return getContentChanges(changeLogToken, includeProperties, getDefaultContext());
418    }
419
420    @Override
421    public ItemIterable<ChangeEvent> getContentChanges(String changeLogToken, boolean includeProperties,
422            OperationContext context) {
423        // TODO Auto-generated method stub
424        throw new UnsupportedOperationException();
425    }
426
427    @Override
428    public Locale getLocale() {
429        // TODO Auto-generated method stub
430        throw new UnsupportedOperationException();
431    }
432
433    @Override
434    public boolean exists(ObjectId objectId) {
435        return exists(objectId.getId());
436    }
437
438    @Override
439    public boolean exists(String objectId) {
440        try {
441            service.getObject(repositoryId, objectId, PropertyIds.OBJECT_ID, Boolean.FALSE, IncludeRelationships.NONE,
442                    RENDITION_NONE, Boolean.FALSE, Boolean.FALSE, null);
443            return true;
444        } catch (CmisObjectNotFoundException e) {
445            return false;
446        }
447    }
448
449    @Override
450    public boolean existsPath(String parentPath, String name) {
451        if (parentPath == null || !parentPath.startsWith("/")) {
452            throw new CmisInvalidArgumentException("Invalid parent path: " + parentPath);
453        }
454        if (StringUtils.isEmpty(name)) {
455            throw new CmisInvalidArgumentException("Invalid empty name: " + name);
456        }
457        StringBuilder path = new StringBuilder(parentPath);
458        if (!parentPath.endsWith("/")) {
459            path.append('/');
460        }
461        path.append(name);
462        return existsPath(path.toString());
463    }
464
465    @Override
466    public boolean existsPath(String path) {
467        try {
468            service.getObjectByPath(repositoryId, path, PropertyIds.OBJECT_ID, Boolean.FALSE, IncludeRelationships.NONE,
469                    RENDITION_NONE, Boolean.FALSE, Boolean.FALSE, null);
470            return true;
471        } catch (CmisObjectNotFoundException e) {
472            return false;
473        }
474    }
475
476    @Override
477    public CmisObject getObject(ObjectId objectId) {
478        return getObject(objectId, getDefaultContext());
479    }
480
481    @Override
482    public CmisObject getObject(String objectId) {
483        return getObject(objectId, getDefaultContext());
484    }
485
486    /** Gets a CMIS object given a Nuxeo {@link DocumentModel}. */
487    public CmisObject getObject(DocumentModel doc, OperationContext context) {
488        ObjectData data = new NuxeoObjectData(service, doc, context);
489        return objectFactory.convertObject(data, context);
490    }
491
492    @Override
493    public CmisObject getObject(ObjectId objectId, OperationContext context) {
494        if (objectId == null) {
495            throw new CmisInvalidArgumentException("Missing object ID");
496        }
497        return getObject(objectId.getId(), context);
498    }
499
500    @Override
501    public CmisObject getObject(String objectId, OperationContext context) {
502        if (objectId == null) {
503            throw new CmisInvalidArgumentException("Missing object ID");
504        }
505        if (context == null) {
506            throw new CmisInvalidArgumentException("Missing operation context");
507        }
508        ObjectData data = service.getObject(repositoryId, objectId, context.getFilterString(),
509                Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
510                context.getRenditionFilterString(), Boolean.valueOf(context.isIncludePolicies()),
511                Boolean.valueOf(context.isIncludeAcls()), null);
512        return objectFactory.convertObject(data, context);
513    }
514
515    @Override
516    public CmisObject getObjectByPath(String path) {
517        return getObjectByPath(path, getDefaultContext());
518    }
519
520    @Override
521    public CmisObject getObjectByPath(String parentPath, String name) {
522        return getObjectByPath(parentPath, name, getDefaultContext());
523    }
524
525    @Override
526    public CmisObject getObjectByPath(String parentPath, String name, OperationContext context) {
527        if (parentPath == null || !parentPath.startsWith("/")) {
528            throw new CmisInvalidArgumentException("Invalid parent path: " + parentPath);
529        }
530        if (StringUtils.isEmpty(name)) {
531            throw new CmisInvalidArgumentException("Invalid empty name: " + name);
532        }
533        StringBuilder path = new StringBuilder(parentPath);
534        if (!parentPath.endsWith("/")) {
535            path.append('/');
536        }
537        path.append(name);
538        return getObjectByPath(path.toString(), context);
539    }
540
541    @Override
542    public CmisObject getObjectByPath(String path, OperationContext context) {
543        if (path == null || !path.startsWith("/")) {
544            throw new CmisInvalidArgumentException("Invalid path: " + path);
545        }
546        if (context == null) {
547            throw new CmisInvalidArgumentException("Missing operation context");
548        }
549        ObjectData data = service.getObjectByPath(repositoryId, path, context.getFilterString(),
550                Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
551                context.getRenditionFilterString(), Boolean.valueOf(context.isIncludePolicies()),
552                Boolean.valueOf(context.isIncludeAcls()), null);
553        return getObjectFactory().convertObject(data, context);
554    }
555
556    protected String getObjectIdByPath(String path) {
557        return service.getObjectByPath(repositoryId, path, PropertyIds.OBJECT_ID, Boolean.FALSE,
558                IncludeRelationships.NONE, RENDITION_NONE, Boolean.FALSE, Boolean.FALSE, null).getId();
559    }
560
561    @Override
562    public RepositoryInfo getRepositoryInfo() {
563        return service.getRepositoryInfo(repositoryId, null);
564    }
565
566    @Override
567    public Folder getRootFolder() {
568        return getRootFolder(getDefaultContext());
569    }
570
571    @Override
572    public Folder getRootFolder(OperationContext context) {
573        String id = getRepositoryInfo().getRootFolderId();
574        CmisObject folder = getObject(createObjectId(id), context);
575        if (!(folder instanceof Folder)) {
576            throw new CmisRuntimeException("Root object is not a Folder but: " + folder.getClass().getName());
577        }
578        return (Folder) folder;
579    }
580
581    @Override
582    public ItemIterable<ObjectType> getTypeChildren(String typeId, boolean includePropertyDefinitions) {
583        // TODO Auto-generated method stub
584        throw new UnsupportedOperationException();
585    }
586
587    @Override
588    public ObjectType getTypeDefinition(String typeId) {
589        TypeDefinition typeDefinition = service.getTypeDefinition(repositoryId, typeId, null);
590        return objectFactory.convertTypeDefinition(typeDefinition);
591    }
592
593    @Override
594    public ObjectType getTypeDefinition(String typeId, boolean useCache) {
595        return getTypeDefinition(typeId);
596    }
597
598    @Override
599    public List<Tree<ObjectType>> getTypeDescendants(String typeId, int depth, boolean includePropertyDefinitions) {
600        // TODO Auto-generated method stub
601        throw new UnsupportedOperationException();
602    }
603
604    @Override
605    public ItemIterable<QueryResult> query(String statement, boolean searchAllVersions) {
606        // TODO Auto-generated method stub
607        throw new UnsupportedOperationException();
608    }
609
610    @Override
611    public ItemIterable<QueryResult> query(final String statement, final boolean searchAllVersions,
612            final OperationContext context) {
613        AbstractPageFetcher<QueryResult> pageFetcher = new AbstractPageFetcher<QueryResult>(
614                context.getMaxItemsPerPage()) {
615            @Override
616            protected Page<QueryResult> fetchPage(long skipCount) {
617                ObjectList results = service.query(repositoryId, statement, Boolean.valueOf(searchAllVersions),
618                        Boolean.valueOf(context.isIncludeAllowableActions()), context.getIncludeRelationships(),
619                        context.getRenditionFilterString(), BigInteger.valueOf(maxNumItems),
620                        BigInteger.valueOf(skipCount), null);
621                // convert objects
622                List<QueryResult> page = new ArrayList<>();
623                if (results.getObjects() != null) {
624                    for (ObjectData data : results.getObjects()) {
625                        page.add(new QueryResultImpl(NuxeoSession.this, data));
626                    }
627                }
628                return new Page<>(page, results.getNumItems(), results.hasMoreItems());
629            }
630        };
631        return new CollectionIterable<>(pageFetcher);
632    }
633
634    @Override
635    public ItemIterable<CmisObject> queryObjects(String typeId, String where, boolean searchAllVersions,
636            OperationContext context) {
637        // TODO Auto-generated method stub
638        throw new UnsupportedOperationException();
639    }
640
641    @Override
642    public QueryStatement createQueryStatement(String statement) {
643        return new QueryStatementImpl(this, statement);
644    }
645
646    @Override
647    public QueryStatement createQueryStatement(Collection<String> selectPropertyIds, Map<String, String> fromTypes,
648            String whereClause, List<String> orderByPropertyIds) {
649        return new QueryStatementImpl(this, selectPropertyIds, fromTypes, whereClause, orderByPropertyIds);
650    }
651
652    @Override
653    public ItemIterable<Relationship> getRelationships(final ObjectId objectId,
654            final boolean includeSubRelationshipTypes, final RelationshipDirection relationshipDirection,
655            final ObjectType type, final OperationContext context) {
656        final String typeId = type == null ? null : type.getId();
657        AbstractPageFetcher<Relationship> pageFetcher = new AbstractPageFetcher<Relationship>(
658                context.getMaxItemsPerPage()) {
659            @Override
660            protected Page<Relationship> fetchPage(long skipCount) {
661                ObjectList relations = service.getObjectRelationships(repositoryId, objectId.getId(),
662                        Boolean.valueOf(includeSubRelationshipTypes), relationshipDirection, typeId, null, null,
663                        BigInteger.valueOf(maxNumItems), BigInteger.valueOf(skipCount), null);
664                // convert objects
665                List<Relationship> page = new ArrayList<>();
666                if (relations.getObjects() != null) {
667                    for (ObjectData data : relations.getObjects()) {
668                        CmisObject ob;
669                        if (data instanceof NuxeoObjectData) {
670                            ob = objectFactory.convertObject(data, context);
671                        } else {
672                            ob = getObject(data.getId(), context);
673                        }
674                        if (!(ob instanceof Relationship)) {
675                            // should not happen...
676                            continue;
677                        }
678                        page.add((Relationship) ob);
679                    }
680                }
681                return new Page<>(page, relations.getNumItems(), relations.hasMoreItems());
682            }
683        };
684        return new CollectionIterable<>(pageFetcher);
685    }
686
687    @Override
688    public Acl getAcl(ObjectId objectId, boolean onlyBasicPermissions) {
689        return service.getAcl(repositoryId, objectId.getId(), Boolean.valueOf(onlyBasicPermissions), null);
690    }
691
692    @Override
693    public Acl setAcl(ObjectId objectId, List<Ace> aces) {
694        return service.applyAcl(repositoryId, objectId.getId(), new AccessControlListImpl(aces), null);
695    }
696
697    @Override
698    public Acl applyAcl(ObjectId objectId, List<Ace> addAces, List<Ace> removeAces, AclPropagation aclPropagation) {
699        return service.applyAcl(repositoryId, objectId.getId(), new AccessControlListImpl(addAces),
700                new AccessControlListImpl(removeAces), aclPropagation, null);
701    }
702
703    @Override
704    public void applyPolicy(ObjectId objectId, ObjectId... policyIds) {
705        throw new CmisNotSupportedException();
706    }
707
708    @Override
709    public void removePolicy(ObjectId objectId, ObjectId... policyIds) {
710        throw new CmisNotSupportedException();
711    }
712
713    @Override
714    public void removeObjectFromCache(ObjectId objectId) {
715    }
716
717    @Override
718    public void removeObjectFromCache(String objectId) {
719    }
720
721    @Override
722    public void delete(ObjectId objectId) {
723        delete(objectId, true);
724    }
725
726    @Override
727    public void delete(ObjectId objectId, boolean allVersions) {
728        service.deleteObject(repositoryId, objectId.getId(), Boolean.valueOf(allVersions), null);
729    }
730
731    @Override
732    public void deleteByPath(String path) {
733        deleteByPath(path, true);
734    }
735
736    @Override
737    public void deleteByPath(String parentPath, String name) {
738        deleteByPath(buildPath(parentPath, name), true);
739    }
740
741    @Override
742    public void deleteByPath(String path, boolean allVersions) {
743        checkPath(path);
744        delete(new ObjectIdImpl(getObjectIdByPath(path)), allVersions);
745    }
746
747    @Override
748    public List<String> deleteTree(ObjectId folderId, boolean allVersions, UnfileObject unfile,
749            boolean continueOnFailure) {
750        FailedToDeleteData res = service.deleteTree(repositoryId, folderId.getId(), Boolean.valueOf(allVersions),
751                unfile, Boolean.valueOf(continueOnFailure), null);
752        return res.getIds();
753    }
754
755    @Override
756    public List<String> deleteTreebyPath(String parentPath, String name, boolean allVersions, UnfileObject unfile,
757            boolean continueOnFailure) {
758        return deleteTreebyPath(buildPath(parentPath, name), allVersions, unfile, continueOnFailure);
759    }
760
761    @Override
762    public List<String> deleteTreebyPath(String path, boolean allVersions, UnfileObject unfile,
763            boolean continueOnFailure) {
764        checkPath(path);
765        return deleteTree(new ObjectIdImpl(getObjectIdByPath(path)), allVersions, unfile, continueOnFailure);
766    }
767
768    /** Checks that the path is valid. */
769    protected final void checkPath(String path) {
770        if (StringUtils.isEmpty(path)) {
771            throw new CmisInvalidArgumentException("Missing path");
772        }
773        if (!path.startsWith("/")) {
774            throw new CmisInvalidArgumentException("Path must start with a slash");
775        }
776    }
777
778    /** Checks that the parent path and name are valid, and builds a full path from them. */
779    protected String buildPath(String parentPath, String name) {
780        checkPath(parentPath);
781        if (StringUtils.isEmpty(name)) {
782            throw new CmisInvalidArgumentException("Missing name");
783        }
784        if (name.startsWith("/")) {
785            throw new CmisInvalidArgumentException("Name must not start with a slash");
786        }
787        StringBuilder path = new StringBuilder(parentPath.length() + name.length() + 1);
788        path.append(parentPath);
789        if (!parentPath.endsWith("/")) {
790            path.append('/');
791        }
792        path.append(name);
793        return path.toString();
794    }
795
796    @Override
797    public ContentStream getContentStream(ObjectId docId) {
798        return getContentStream(docId, null, null, null);
799    }
800
801    @Override
802    public ContentStream getContentStream(ObjectId docId, String streamId, BigInteger offset, BigInteger length) {
803        if (docId == null) {
804            throw new CmisInvalidArgumentException("Missing object ID");
805        }
806        return service.getContentStream(repositoryId, docId.getId(), streamId, offset, length, null);
807    }
808
809    @Override
810    public ContentStream getContentStreamByPath(String path) {
811        return getContentStreamByPath(path, null, null, null);
812    }
813
814    @Override
815    public ContentStream getContentStreamByPath(String path, String streamId, BigInteger offset, BigInteger length) {
816        checkPath(path);
817        return service.getContentStream(repositoryId, getObjectIdByPath(path), streamId, offset, length, null);
818    }
819
820    @Override
821    public ObjectType createType(TypeDefinition type) {
822        throw new CmisNotSupportedException();
823    }
824
825    @Override
826    public ObjectType updateType(TypeDefinition type) {
827        throw new CmisNotSupportedException();
828    }
829
830    @Override
831    public void deleteType(String typeId) {
832        throw new CmisNotSupportedException();
833    }
834
835    @Override
836    public List<BulkUpdateObjectIdAndChangeToken> bulkUpdateProperties(List<CmisObject> objects,
837            Map<String, ?> properties, List<String> addSecondaryTypeIds, List<String> removeSecondaryTypeIds) {
838        List<BulkUpdateObjectIdAndChangeToken> idts = new ArrayList<>(objects.size());
839        for (CmisObject object : objects) {
840            idts.add(new BulkUpdateObjectIdAndChangeTokenImpl(object.getId(), object.getChangeToken()));
841        }
842        return service.bulkUpdateProperties(repositoryId, idts, convertProperties(properties), addSecondaryTypeIds,
843                removeSecondaryTypeIds, null);
844    }
845
846    @Override
847    public Document getLatestDocumentVersion(ObjectId objectId) {
848        return getLatestDocumentVersion(objectId, false, getDefaultContext());
849    }
850
851    @Override
852    public Document getLatestDocumentVersion(String objectId, OperationContext context) {
853        if (objectId == null) {
854            throw new IllegalArgumentException("Object ID must be set!");
855        }
856        return getLatestDocumentVersion(createObjectId(objectId), false, context);
857    }
858
859    @Override
860    public Document getLatestDocumentVersion(String objectId, boolean major, OperationContext context) {
861        if (objectId == null) {
862            throw new IllegalArgumentException("Object ID must be set!");
863        }
864        return getLatestDocumentVersion(createObjectId(objectId), major, context);
865    }
866
867    @Override
868    public Document getLatestDocumentVersion(String objectId) {
869        if (objectId == null) {
870            throw new IllegalArgumentException("Object ID must be set!");
871        }
872        return getLatestDocumentVersion(createObjectId(objectId), false, getDefaultContext());
873    }
874
875    @Override
876    public Document getLatestDocumentVersion(ObjectId objectId, OperationContext context) {
877        return getLatestDocumentVersion(objectId, false, context);
878    }
879
880    @Override
881    /**
882     * @See org.apache.chemistry.opencmis.client.runtime.SessionImpl
883     */
884    public Document getLatestDocumentVersion(ObjectId objectId, boolean major, OperationContext context) {
885        if (objectId == null || objectId.getId() == null) {
886            throw new IllegalArgumentException("Object ID must be set!");
887        }
888
889        if (context == null) {
890            throw new IllegalArgumentException("Operation context must be set!");
891        }
892
893        CmisObject result = null;
894
895        String versionSeriesId = null;
896
897        // first attempt: if we got a Document object, try getting the version
898        // series ID from it
899        if (objectId instanceof Document) {
900            versionSeriesId = ((Document) objectId).getVersionSeriesId();
901        }
902
903        // (the AtomPub and Browser binding don't need the version series ID ->
904        // avoid roundtrip)
905
906        // get the object
907        ObjectData objectData = binding.getVersioningService().getObjectOfLatestVersion(getRepositoryId(),
908                objectId.getId(), versionSeriesId, major, context.getFilterString(),
909                context.isIncludeAllowableActions(), context.getIncludeRelationships(),
910                context.getRenditionFilterString(), context.isIncludePolicies(), context.isIncludeAcls(), null);
911
912        result = getObjectFactory().convertObject(objectData, context);
913
914        // check result
915        if (!(result instanceof Document)) {
916            throw new IllegalArgumentException("Latest version is not a document!");
917        }
918
919        return (Document) result;
920    }
921
922    @Override
923    public String getLatestChangeLogToken() {
924        return getBinding().getRepositoryService().getRepositoryInfo(getRepositoryId(), null).getLatestChangeLogToken();
925    }
926
927}