This project has retired. For details please refer to its Attic page.
JcrRepository xref

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.chemistry.opencmis.jcr;
20  
21  import org.apache.chemistry.opencmis.commons.PropertyIds;
22  import org.apache.chemistry.opencmis.commons.data.AllowableActions;
23  import org.apache.chemistry.opencmis.commons.data.ContentStream;
24  import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
25  import org.apache.chemistry.opencmis.commons.data.ObjectData;
26  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
27  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
28  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
29  import org.apache.chemistry.opencmis.commons.data.ObjectList;
30  import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
31  import org.apache.chemistry.opencmis.commons.data.Properties;
32  import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
33  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
34  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
35  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
36  import org.apache.chemistry.opencmis.commons.enums.CapabilityAcl;
37  import org.apache.chemistry.opencmis.commons.enums.CapabilityChanges;
38  import org.apache.chemistry.opencmis.commons.enums.CapabilityContentStreamUpdates;
39  import org.apache.chemistry.opencmis.commons.enums.CapabilityJoin;
40  import org.apache.chemistry.opencmis.commons.enums.CapabilityQuery;
41  import org.apache.chemistry.opencmis.commons.enums.CapabilityRenditions;
42  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
43  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
44  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
45  import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException;
46  import org.apache.chemistry.opencmis.commons.exceptions.CmisNotSupportedException;
47  import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
48  import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
49  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
50  import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
51  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
52  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
53  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
54  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectListImpl;
55  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
56  import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryCapabilitiesImpl;
57  import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl;
58  import org.apache.chemistry.opencmis.commons.server.ObjectInfoHandler;
59  import org.apache.chemistry.opencmis.commons.spi.Holder;
60  import org.apache.chemistry.opencmis.jcr.query.QueryTranslator;
61  import org.apache.chemistry.opencmis.jcr.type.JcrDocumentTypeHandler;
62  import org.apache.chemistry.opencmis.jcr.type.JcrFolderTypeHandler;
63  import org.apache.chemistry.opencmis.jcr.type.JcrTypeHandlerManager;
64  import org.apache.chemistry.opencmis.jcr.util.Util;
65  import org.apache.commons.logging.Log;
66  import org.apache.commons.logging.LogFactory;
67  
68  import javax.jcr.Credentials;
69  import javax.jcr.ItemNotFoundException;
70  import javax.jcr.LoginException;
71  import javax.jcr.NoSuchWorkspaceException;
72  import javax.jcr.Node;
73  import javax.jcr.NodeIterator;
74  import javax.jcr.PathNotFoundException;
75  import javax.jcr.Repository;
76  import javax.jcr.RepositoryException;
77  import javax.jcr.Session;
78  import javax.jcr.query.Query;
79  import javax.jcr.query.QueryManager;
80  import javax.jcr.query.QueryResult;
81  import java.math.BigInteger;
82  import java.util.ArrayList;
83  import java.util.Collections;
84  import java.util.HashSet;
85  import java.util.Iterator;
86  import java.util.List;
87  import java.util.Set;
88  
89  /**
90   * JCR back-end for CMIS server.
91   */
92  public class JcrRepository {
93      private static final Log log = LogFactory.getLog(JcrRepository.class);
94  
95      private final Repository repository;
96      private final JcrTypeManager typeManager;
97      private final PathManager pathManager;
98      private final JcrTypeHandlerManager typeHandlerManager;
99  
100     /**
101      * Create a new <code>JcrRepository</code> instance backed by a JCR repository.
102      *
103      * @param repository  the JCR repository
104      * @param pathManager
105      * @param typeManager  
106      * @param typeHandlerManager
107      */
108     public JcrRepository(Repository repository, PathManager pathManager, JcrTypeManager typeManager, JcrTypeHandlerManager typeHandlerManager) {
109         this.repository = repository;
110         this.typeManager = typeManager;
111         this.typeHandlerManager = typeHandlerManager;
112         this.pathManager = pathManager;
113     }
114 
115     /**
116      * Log into the underlying JCR repository.
117      * 
118      * @param credentials
119      * @param workspaceName
120      * @return
121      * @throws LoginException
122      * @throws NoSuchWorkspaceException
123      * @throws RepositoryException
124      */
125     public Session login(Credentials credentials, String workspaceName) {
126         try {
127             return repository.login(credentials, workspaceName);
128         }
129         catch (LoginException e) {
130             log.debug(e.getMessage(), e);
131             throw new CmisPermissionDeniedException(e.getMessage(), e);
132         }
133         catch (NoSuchWorkspaceException e) {
134             log.debug(e.getMessage(), e);
135             throw new CmisObjectNotFoundException(e.getMessage(), e);
136         }
137         catch (RepositoryException e) {
138             log.debug(e.getMessage(), e);
139             throw new CmisRuntimeException(e.getMessage(), e);
140         }
141     }
142 
143     /**
144      * See CMIS 1.0 section 2.2.2.2 getRepositoryInfo
145      */
146     public RepositoryInfo getRepositoryInfo(Session session) {
147         log.debug("getRepositoryInfo");
148 
149         return compileRepositoryInfo(session.getWorkspace().getName());
150     }
151 
152     /**
153      * See CMIS 1.0 section 2.2.2.2 getRepositoryInfo
154      */
155     public List<RepositoryInfo> getRepositoryInfos(Session session) {
156         try {
157             ArrayList<RepositoryInfo> infos = new ArrayList<RepositoryInfo>();
158             for (String wspName : session.getWorkspace().getAccessibleWorkspaceNames()) {
159                 infos.add(compileRepositoryInfo(wspName));
160             }
161 
162             return infos;
163         }
164         catch (RepositoryException e) {
165             log.debug(e.getMessage(), e);
166             throw new CmisRuntimeException(e.getMessage(), e);
167         }
168     }
169 
170     /**
171      * See CMIS 1.0 section 2.2.2.3 getTypeChildren
172      */
173     public TypeDefinitionList getTypeChildren(Session session, String typeId, boolean includePropertyDefinitions,
174             BigInteger maxItems, BigInteger skipCount) {
175         
176         log.debug("getTypesChildren");
177         return typeManager.getTypeChildren(typeId, includePropertyDefinitions, maxItems, skipCount);
178     }
179 
180     /**
181      * See CMIS 1.0 section 2.2.2.5 getTypeDefinition
182      */
183     public TypeDefinition getTypeDefinition(Session session, String typeId) {
184         log.debug("getTypeDefinition");
185 
186         TypeDefinition type = typeManager.getType(typeId);
187         if (type == null) {
188             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
189         }
190 
191         return JcrTypeManager.copyTypeDefinition(type);
192     }
193 
194     /**
195      * See CMIS 1.0 section 2.2.2.4 getTypeDescendants
196      */
197     public List<TypeDefinitionContainer> getTypesDescendants(Session session, String typeId, BigInteger depth,
198             Boolean includePropertyDefinitions) {
199 
200         log.debug("getTypesDescendants");
201         return typeManager.getTypesDescendants(typeId, depth, includePropertyDefinitions);
202     }
203 
204     /**
205      * See CMIS 1.0 section 2.2.4.1 createDocument
206      */
207     public String createDocument(Session session, Properties properties, String folderId, ContentStream contentStream,
208             VersioningState versioningState) {
209 
210         log.debug("createDocument");
211 
212         // check properties
213         if (properties == null || properties.getProperties() == null) {
214             throw new CmisInvalidArgumentException("Properties must be set!");
215         }
216 
217         // check type
218         String typeId = PropertyHelper.getTypeId(properties);
219         TypeDefinition type = typeManager.getType(typeId);
220         if (type == null) {
221             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
222         }
223 
224         boolean isVersionable = JcrTypeManager.isVersionable(type);
225         if (!isVersionable && versioningState != VersioningState.NONE) {
226             throw new CmisConstraintException("Versioning not supported for " + typeId);
227         }
228 
229         if (isVersionable && versioningState == VersioningState.NONE) {
230             throw new CmisConstraintException("Versioning required for " + typeId);
231         }
232 
233         // check the name
234         String name = PropertyHelper.getStringProperty(properties, PropertyIds.NAME);
235         if (!JcrConverter.isValidJcrName(name)) {
236             throw new CmisNameConstraintViolationException("Name is not valid: " + name);
237         }
238 
239         // get parent Node and create child
240         JcrFolder parent = getJcrNode(session, folderId).asFolder();
241         JcrDocumentTypeHandler typeHandler = typeHandlerManager.getDocumentTypeHandler(typeId);
242         JcrNode jcrNode = typeHandler.createDocument(parent, name, properties, contentStream, versioningState);
243         return jcrNode.getId();
244     }
245 
246     /**
247      * See CMIS 1.0 section 2.2.4.2 createDocumentFromSource
248      */
249     public String createDocumentFromSource(Session session, String sourceId, Properties properties, String folderId,
250             VersioningState versioningState) {
251 
252         log.debug("createDocumentFromSource");
253 
254         // get parent folder Node
255         JcrFolder parent = getJcrNode(session, folderId).asFolder();
256 
257         // get source document Node
258         JcrDocument source = getJcrNode(session, sourceId).asDocument();
259 
260         boolean isVersionable = source.isVersionable();
261         if (!isVersionable && versioningState != VersioningState.NONE) {
262             throw new CmisConstraintException("Versioning not supported for " + sourceId);
263         }
264 
265         if (isVersionable && versioningState == VersioningState.NONE) {
266             throw new CmisConstraintException("Versioning required for " + sourceId);
267         }
268 
269         // create child from source
270         JcrNode jcrNode = parent.addNodeFromSource(source, properties);
271         return jcrNode.getId();
272     }
273 
274     /**
275      * See CMIS 1.0 section 2.2.4.3 createFolder
276      */
277     public String createFolder(Session session, Properties properties, String folderId) {
278         log.debug("createFolder");
279 
280         // check properties
281         if (properties == null || properties.getProperties() == null) {
282             throw new CmisInvalidArgumentException("Properties must be set!");
283         }
284 
285         // check type
286         String typeId = PropertyHelper.getTypeId(properties);
287         TypeDefinition type = typeManager.getType(typeId);
288         if (type == null) {
289             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
290         }
291 
292         // check the name
293         String name = PropertyHelper.getStringProperty(properties, PropertyIds.NAME);
294         if (!JcrConverter.isValidJcrName(name)) {
295             throw new CmisNameConstraintViolationException("Name is not valid: " + name);
296         }
297 
298         // get parent Node
299         JcrFolder parent = getJcrNode(session, folderId).asFolder();
300         JcrFolderTypeHandler typeHandler = typeHandlerManager.getFolderTypeHandler(typeId);
301         JcrNode jcrNode = typeHandler.createFolder(parent, name, properties);
302         return jcrNode.getId();
303     }
304 
305     /**
306      * See CMIS 1.0 section 2.2.4.13 moveObject
307      */
308     public ObjectData moveObject(Session session, Holder<String> objectId, String targetFolderId,
309             ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
310 
311         log.debug("moveObject");
312 
313         if (objectId == null || objectId.getValue() == null) {
314             throw new CmisInvalidArgumentException("Id is not valid!");
315         }
316 
317         // get the node and parent
318         JcrNode jcrNode = getJcrNode(session, objectId.getValue());
319         JcrFolder parent = getJcrNode(session, targetFolderId).asFolder();
320         jcrNode = jcrNode.move(parent);
321         objectId.setValue(jcrNode.getId());
322         return jcrNode.compileObjectType(null, false, objectInfos, requiresObjectInfo);
323     }
324 
325     /**
326      * See CMIS 1.0 section 2.2.4.16 setContentStream
327      */
328     public void setContentStream(Session session, Holder<String> objectId, Boolean overwriteFlag,
329             ContentStream contentStream) {
330 
331         log.debug("setContentStream or deleteContentStream");
332 
333         if (objectId == null || objectId.getValue() == null) {
334             throw new CmisInvalidArgumentException("Id is not valid!");
335         }
336 
337         JcrDocument jcrDocument = getJcrNode(session, objectId.getValue()).asDocument();
338         String id = jcrDocument.setContentStream(contentStream, Boolean.TRUE.equals(overwriteFlag)).getId();
339         objectId.setValue(id);
340     }
341 
342     /**
343      * See CMIS 1.0 section 2.2.4.14 deleteObject
344      */
345     public void deleteObject(Session session, String objectId, Boolean allVersions) {
346         log.debug("deleteObject");
347 
348         // get the node
349         JcrNode jcrNode = getJcrNode(session, objectId);
350         jcrNode.delete(Boolean.TRUE.equals(allVersions), JcrPrivateWorkingCopy.isPwc(objectId)); 
351     }
352 
353     /**
354      * See CMIS 1.0 section 2.2.4.15 deleteTree
355      */
356     public FailedToDeleteData deleteTree(Session session, String folderId) {
357         log.debug("deleteTree");
358 
359         // get the folder
360         JcrFolder jcrFolder = getJcrNode(session, folderId).asFolder();
361         return jcrFolder.deleteTree();
362     }
363 
364     /**
365      * See CMIS 1.0 section 2.2.4.12 updateProperties
366      */
367     public ObjectData updateProperties(Session session, Holder<String> objectId, Properties properties,
368             ObjectInfoHandler objectInfos, boolean objectInfoRequired) {
369 
370         log.debug("updateProperties");
371 
372         if (objectId == null) {
373             throw new CmisInvalidArgumentException("Id is not valid!");
374         }
375 
376         // get the node
377         JcrNode jcrNode = getJcrNode(session, objectId.getValue());
378         String id = jcrNode.updateProperties(properties).getId();
379         objectId.setValue(id);
380         return jcrNode.compileObjectType(null, false, objectInfos, objectInfoRequired);
381     }
382 
383     /**
384      * See CMIS 1.0 section 2.2.4.7 getObject
385      */
386     public ObjectData getObject(Session session, String objectId, String filter, Boolean includeAllowableActions,
387             ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
388 
389         log.debug("getObject");
390 
391         // check id
392         if (objectId == null) {
393             throw new CmisInvalidArgumentException("Object Id must be set.");
394         }
395 
396         // get the node
397         JcrNode jcrNode = getJcrNode(session, objectId);
398 
399         // gather properties
400         return jcrNode.compileObjectType(splitFilter(filter), includeAllowableActions, objectInfos, requiresObjectInfo);
401     }
402 
403     /**
404      * See CMIS 1.0 section 2.2.4.8 getProperties
405      */
406     public Properties getProperties(Session session, String objectId, String filter, Boolean includeAllowableActions,
407             ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
408 
409         ObjectData object = getObject(session, objectId, filter, includeAllowableActions, objectInfos, requiresObjectInfo);
410         return object.getProperties();
411     }
412 
413     /**
414      * See CMIS 1.0 section 2.2.4.6 getAllowableActions
415      */
416     public AllowableActions getAllowableActions(Session session, String objectId) {
417         log.debug("getAllowableActions");
418 
419         JcrNode jcrNode = getJcrNode(session, objectId);
420         return jcrNode.getAllowableActions();
421     }
422 
423     /**
424      * See CMIS 1.0 section 2.2.4.10 getContentStream
425      */
426     public ContentStream getContentStream(Session session, String objectId, BigInteger offset, BigInteger length) {
427         log.debug("getContentStream");
428 
429         if (offset != null || length != null) {
430             throw new CmisInvalidArgumentException("Offset and Length are not supported!");
431         }
432 
433         // get the node
434         JcrDocument jcrDocument = getJcrNode(session, objectId).asDocument();
435         return jcrDocument.getContentStream();        
436     }
437 
438     /**
439      * See CMIS 1.0 section 2.2.3.1 getChildren
440      */
441     public ObjectInFolderList getChildren(Session session, String folderId, String filter,
442             Boolean includeAllowableActions, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
443             ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
444 
445         log.debug("getChildren");
446 
447         // skip and max
448         int skip = skipCount == null ? 0 : skipCount.intValue();
449         if (skip < 0) {
450             skip = 0;
451         }
452 
453         int max = maxItems == null ? Integer.MAX_VALUE : maxItems.intValue();
454         if (max < 0) {
455             max = Integer.MAX_VALUE;
456         }
457 
458         // get the folder
459         JcrFolder jcrFolder = getJcrNode(session, folderId).asFolder();
460 
461         // set object info of the the folder
462         if (requiresObjectInfo) {
463             jcrFolder.compileObjectType(null, false, objectInfos, requiresObjectInfo);
464         }
465 
466         // prepare result
467         ObjectInFolderListImpl result = new ObjectInFolderListImpl();
468         result.setObjects(new ArrayList<ObjectInFolderData>());
469         result.setHasMoreItems(false);
470         int count = 0;
471 
472         // iterate through children
473         Set<String> splitFilter = splitFilter(filter);
474         Iterator<JcrNode> childNodes = jcrFolder.getNodes();
475         while (childNodes.hasNext()) {
476             JcrNode child = childNodes.next();            
477             count++;
478 
479             if (skip > 0) {
480                 skip--;
481                 continue;
482             }
483 
484             if (result.getObjects().size() >= max) {
485                 result.setHasMoreItems(true);
486                 continue;
487             }
488 
489             // build and add child object
490             ObjectInFolderDataImpl objectInFolder = new ObjectInFolderDataImpl();
491             objectInFolder.setObject(child.compileObjectType(splitFilter, includeAllowableActions, objectInfos,
492                     requiresObjectInfo));
493 
494             if (Boolean.TRUE.equals(includePathSegment)) {
495                 objectInFolder.setPathSegment(child.getName());
496             }
497 
498             result.getObjects().add(objectInFolder);
499         }
500 
501         result.setNumItems(BigInteger.valueOf(count));
502         return result;
503     }
504 
505     /**
506      * See CMIS 1.0 section 2.2.3.2 getDescendants
507      */
508     public List<ObjectInFolderContainer> getDescendants(Session session, String folderId, BigInteger depth,
509             String filter, Boolean includeAllowableActions, Boolean includePathSegment, ObjectInfoHandler objectInfos,
510             boolean requiresObjectInfo, boolean foldersOnly) {
511 
512         log.debug("getDescendants or getFolderTree");
513 
514         // check depth
515         int d = depth == null ? 2 : depth.intValue();
516         if (d == 0) {
517             throw new CmisInvalidArgumentException("Depth must not be 0!");
518         }
519         if (d < -1) {
520             d = -1;
521         }
522 
523         // get the folder
524         JcrFolder jcrFolder = getJcrNode(session, folderId).asFolder();
525 
526         // set object info of the the folder
527         if (requiresObjectInfo) {
528             jcrFolder.compileObjectType(null, false, objectInfos, requiresObjectInfo);
529         }
530 
531         // get the tree
532         List<ObjectInFolderContainer> result = new ArrayList<ObjectInFolderContainer>();
533         gatherDescendants(jcrFolder, result, foldersOnly, d, splitFilter(filter), includeAllowableActions,
534                 includePathSegment, objectInfos, requiresObjectInfo);
535 
536         return result;
537     }
538 
539     /**
540      * See CMIS 1.0 section 2.2.3.4 getFolderParent
541      */
542     public ObjectData getFolderParent(Session session, String folderId, String filter, ObjectInfoHandler objectInfos,
543             boolean requiresObjectInfo) {
544 
545         List<ObjectParentData> parents = getObjectParents(session, folderId, filter, false, false, objectInfos,
546                 requiresObjectInfo);
547 
548         if (parents.isEmpty()) {
549             throw new CmisInvalidArgumentException("The root folder has no parent!");
550         }
551 
552         return parents.get(0).getObject();
553     }
554 
555     /**
556      * See CMIS 1.0 section 2.2.3.5 getObjectParents
557      */
558     public List<ObjectParentData> getObjectParents(Session session, String objectId, String filter,
559             Boolean includeAllowableActions, Boolean includeRelativePathSegment, ObjectInfoHandler objectInfos,
560             boolean requiresObjectInfo) {
561 
562         log.debug("getObjectParents");
563 
564         // get the file or folder
565         JcrNode jcrNode = getJcrNode(session, objectId);
566 
567         // don't climb above the root folder
568         if (jcrNode.isRoot()) {
569             return Collections.emptyList();
570         }
571 
572         // set object info of the the object
573         if (requiresObjectInfo) {
574             jcrNode.compileObjectType(null, false, objectInfos, requiresObjectInfo);
575         }
576 
577         // get parent
578         JcrNode parent = jcrNode.getParent();
579         ObjectData object = parent.compileObjectType(splitFilter(filter), includeAllowableActions, objectInfos,
580                 requiresObjectInfo);
581 
582         ObjectParentDataImpl result = new ObjectParentDataImpl();
583         result.setObject(object);
584         if (Boolean.TRUE.equals(includeRelativePathSegment)) {
585             result.setRelativePathSegment(parent.getName());
586         }
587 
588         return Collections.singletonList((ObjectParentData) result);
589     }
590 
591     /**
592      * See CMIS 1.0 section 2.2.4.9 getObjectByPath
593      */
594     public ObjectData getObjectByPath(Session session, String folderPath, String filter, boolean includeAllowableActions,
595             boolean includeACL, ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
596 
597         log.debug("getObjectByPath");
598 
599         // check path 
600         if (folderPath == null || !PathManager.isAbsolute(folderPath)) {
601             throw new CmisInvalidArgumentException("Invalid folder path!");
602         }
603 
604         JcrNode root = getJcrNode(session, PathManager.CMIS_ROOT_ID);
605         JcrNode jcrNode;
606         if (PathManager.isRoot(folderPath)) {
607             jcrNode = root;
608         }
609         else {
610             String path = PathManager.relativize(PathManager.CMIS_ROOT_PATH, folderPath);
611             jcrNode = root.getNode(path);
612         }
613 
614         return jcrNode.compileObjectType(splitFilter(filter), includeAllowableActions, objectInfos, requiresObjectInfo);
615     }
616 
617     /**
618      * See CMIS 1.0 section 2.2.3.6 getCheckedOutDocs
619      */
620     public ObjectList getCheckedOutDocs(Session session, String folderId, String filter, String orderBy,
621             Boolean includeAllowableActions, BigInteger maxItems, BigInteger skipCount) {
622 
623         log.debug("getCheckedOutDocs");
624 
625         // skip and max
626         int skip = skipCount == null ? 0 : skipCount.intValue();
627         if (skip < 0) {
628             skip = 0;
629         }
630 
631         int max = maxItems == null ? Integer.MAX_VALUE : maxItems.intValue();
632         if (max < 0) {
633             max = Integer.MAX_VALUE;
634         }
635 
636         try {
637             // Build xpath query of the form
638             // '//path/to/folderId//*[jcr:isCheckedOut='true' and (not(@jcr:createdBy) or @jcr:createdBy='admin')]'
639             String xPath = "/*[jcr:isCheckedOut='true' " +
640                     "and (not(@jcr:createdBy) or @jcr:createdBy='" + session.getUserID() + "')]";
641             
642             if (folderId != null) {
643                 JcrFolder jcrFolder = getJcrNode(session, folderId).asFolder();
644                 String path = jcrFolder.getNode().getPath();
645                 if ("/".equals(path)) {
646                     path = "";
647                 }
648                 xPath = '/' + Util.escape(path) + xPath;
649             }
650             else {
651                 xPath = '/' + xPath;
652             }
653 
654             // Execute query
655             QueryManager queryManager = session.getWorkspace().getQueryManager();
656             Query query = queryManager.createQuery(xPath, Query.XPATH);
657             QueryResult queryResult = query.execute();
658 
659             // prepare results
660             ObjectListImpl result = new ObjectListImpl();
661             result.setObjects(new ArrayList<ObjectData>());
662             result.setHasMoreItems(false);
663 
664             // iterate through children
665             Set<String> splitFilter = splitFilter(filter);
666             int count = 0;
667             NodeIterator nodes = queryResult.getNodes();
668             while (nodes.hasNext()) {
669                 Node node = nodes.nextNode();
670                 JcrNode jcrNode = typeHandlerManager.create(node);
671                 if (!jcrNode.isVersionable()) {
672                     continue;
673                 }
674 
675                 count++;
676 
677                 if (skip > 0) {
678                     skip--;
679                     continue;
680                 }
681 
682                 if (result.getObjects().size() >= max) {
683                     result.setHasMoreItems(true);
684                     continue;
685                 }
686                 
687                 // build and add child object
688                 JcrPrivateWorkingCopy jcrVersion = jcrNode.asVersion().getPwc();
689                 ObjectData objectData = jcrVersion.compileObjectType(splitFilter, includeAllowableActions, null, false);
690                 result.getObjects().add(objectData);
691             }
692 
693             result.setNumItems(BigInteger.valueOf(count));
694             return result;
695         }
696         catch (RepositoryException e) {
697             log.debug(e.getMessage(), e);
698             throw new CmisRuntimeException(e.getMessage(), e);
699         }
700     }
701 
702     /**
703      * See CMIS 1.0 section 2.2.7.1 checkOut
704      */
705     public void checkOut(Session session, Holder<String> objectId, Holder<Boolean> contentCopied) {
706         log.debug("checkout");
707 
708         // check id 
709         if (objectId == null || objectId.getValue() == null) {
710             throw new CmisInvalidArgumentException("Object Id must be set.");
711         }
712 
713         // get the node
714         JcrNode jcrNode = getJcrNode(session, objectId.getValue());
715         if (!jcrNode.isVersionable()) {
716             throw new CmisUpdateConflictException("Not a version: " + jcrNode);
717         }
718 
719         // checkout
720         JcrPrivateWorkingCopy pwc = jcrNode.asVersion().checkout();
721         objectId.setValue(pwc.getId());
722         if (contentCopied != null) {
723             contentCopied.setValue(true);
724         }
725     }
726 
727     /**
728      * See CMIS 1.0 section 2.2.7.2 cancelCheckout
729      */
730     public void cancelCheckout(Session session, String objectId) {
731         log.debug("cancelCheckout");
732 
733         // check id
734         if (objectId == null) {
735             throw new CmisInvalidArgumentException("Object Id must be set.");
736         }
737 
738         // get the node
739         JcrNode jcrNode = getJcrNode(session, objectId);
740         if (!jcrNode.isVersionable()) {
741             throw new CmisUpdateConflictException("Not a version: " + jcrNode);
742         }
743 
744         // cancelCheckout
745         jcrNode.asVersion().cancelCheckout();
746     }
747 
748     /**
749      * See CMIS 1.0 section 2.2.7.3 checkedIn
750      */
751     public void checkIn(Session session, Holder<String> objectId, Boolean major, Properties properties,
752             ContentStream contentStream, String checkinComment) {
753 
754         log.debug("checkin");
755 
756         // check id
757         if (objectId == null || objectId.getValue() == null) {
758             throw new CmisInvalidArgumentException("Object Id must be set.");
759         }
760 
761         // get the node
762         JcrNode jcrNode;
763         try {
764             jcrNode = getJcrNode(session, objectId.getValue());
765         }
766         catch (CmisObjectNotFoundException e) {
767             throw new CmisUpdateConflictException(e.getCause().getMessage(), e.getCause());
768         }
769         
770         if (!jcrNode.isVersionable()) {
771             throw new CmisUpdateConflictException("Not a version: " + jcrNode);
772         }
773 
774         // checkin
775         JcrVersion checkedIn = jcrNode.asVersion().checkin(properties, contentStream, checkinComment);
776         objectId.setValue(checkedIn.getId());
777     }
778 
779     /**
780      * See CMIS 1.0 section 2.2.7.6 getAllVersions
781      */
782     public List<ObjectData> getAllVersions(Session session, String objectId, String filter,
783             Boolean includeAllowableActions, ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
784 
785         log.debug("getAllVersions");
786 
787         // check id
788         if (objectId == null) {
789             throw new CmisInvalidArgumentException("Object Id must be set.");
790         }
791 
792         Set<String> splitFilter = splitFilter(filter);
793 
794         // get the node
795         JcrNode jcrNode = getJcrNode(session, objectId);
796 
797         // Collect versions
798         if (jcrNode.isVersionable()) {
799             JcrVersionBase jcrVersion = jcrNode.asVersion();
800 
801             Iterator<JcrVersion> versions = jcrVersion.getVersions();
802             if (versions.hasNext()) {
803                 versions.next(); // skip root version
804             }
805 
806             List<ObjectData> allVersions = new ArrayList<ObjectData>();
807             while (versions.hasNext()) {
808                 JcrVersion version = versions.next();
809                 ObjectData objectData = version.compileObjectType(splitFilter, includeAllowableActions, objectInfos,
810                         requiresObjectInfo);
811                 allVersions.add(objectData);
812             }
813 
814             // Add pwc if checked out
815             if (jcrVersion.isDocumentCheckedOut()) {
816                 JcrPrivateWorkingCopy pwc = jcrVersion.getPwc();
817                 ObjectData objectData = pwc.compileObjectType(splitFilter, includeAllowableActions, objectInfos,
818                         requiresObjectInfo);
819 
820                 allVersions.add(objectData);
821             }
822 
823             // CMIS mandates descending order
824             Collections.reverse(allVersions);
825             return allVersions;
826         }
827         else {
828             // Single version
829             ObjectData objectData = jcrNode.compileObjectType(splitFilter, includeAllowableActions, objectInfos,
830                     requiresObjectInfo);
831 
832             return Collections.singletonList(objectData);
833         }
834 
835     }
836 
837     /**
838      * See CMIS 1.0 section 2.2.6.1 query
839      */
840     public ObjectList query(final Session session, String statement, Boolean searchAllVersions,
841             Boolean includeAllowableActions, BigInteger maxItems, BigInteger skipCount) {
842 
843         log.debug("query");
844 
845         if (searchAllVersions) {
846             throw new CmisNotSupportedException("Not supported: query for all versions");
847         }
848 
849         // skip and max
850         int skip = skipCount == null ? 0 : skipCount.intValue();  
851         if (skip < 0) {
852             skip = 0;
853         }
854 
855         int max = maxItems == null ? Integer.MAX_VALUE : maxItems.intValue();
856         if (max < 0) {
857             max = Integer.MAX_VALUE;
858         }
859 
860         QueryTranslator queryTranslator = new QueryTranslator(typeManager) {
861             @Override
862             protected String jcrPathFromId(String id) {
863                 try {
864                     JcrFolder folder = getJcrNode(session, id).asFolder();
865                     String path = folder.getNode().getPath();
866                     return Util.escape(path);                    
867                 }
868                 catch (RepositoryException e) {
869                     log.debug(e.getMessage(), e);
870                     throw new CmisRuntimeException(e.getMessage(), e);
871                 }
872             }
873 
874             @Override
875             protected String jcrPathFromCol(TypeDefinition fromType, String name) {
876                 return typeHandlerManager.getIdentifierMap(fromType.getId()).jcrPathFromCol(name);
877             }
878 
879             @Override
880             protected String jcrTypeName(TypeDefinition fromType) {
881                 return typeHandlerManager.getIdentifierMap(fromType.getId()).jcrTypeName();
882             }
883 
884             @Override
885             protected String jcrTypeCondition(TypeDefinition fromType) {
886                 return typeHandlerManager.getIdentifierMap(fromType.getId()).jcrTypeCondition();
887             }
888         };
889 
890         String xPath = queryTranslator.translateToXPath(statement);
891         try {  
892             // Execute query
893             QueryManager queryManager = session.getWorkspace().getQueryManager();
894             Query query = queryManager.createQuery(xPath, Query.XPATH);
895 
896             if (skip > 0) {
897                 query.setOffset(skip);
898             }
899             if (max < Integer.MAX_VALUE) {
900                 query.setLimit(max + 1);    // One more in order to detect whether there are more items
901             }
902 
903             QueryResult queryResult = query.execute();
904 
905             // prepare results
906             ObjectListImpl result = new ObjectListImpl();
907             result.setObjects(new ArrayList<ObjectData>());
908             result.setHasMoreItems(false);
909 
910             // iterate through children
911             int count = 0;
912             NodeIterator nodes = queryResult.getNodes();
913             while (nodes.hasNext() && result.getObjects().size() < max) {
914                 Node node = nodes.nextNode();
915                 JcrNode jcrNode = typeHandlerManager.create(node);
916                 count++;
917 
918                 // Get pwc if this node is versionable and checked out
919                 if (jcrNode.isVersionable() && jcrNode.asVersion().isCheckedOut()) {
920                     jcrNode = jcrNode.asVersion().getPwc();
921                 }
922 
923                 // build and add child object
924                 ObjectData objectData = jcrNode.compileObjectType(null, includeAllowableActions, null, false);
925                 result.getObjects().add(objectData);
926             }
927 
928             result.setHasMoreItems(nodes.hasNext());
929             result.setNumItems(BigInteger.valueOf(count));
930             return result;
931         }
932         catch (RepositoryException e) {
933             log.debug(e.getMessage(), e);
934             throw new CmisRuntimeException(e.getMessage(), e);
935         }
936     }
937 
938     //------------------------------------------< protected >---
939 
940     protected RepositoryInfo compileRepositoryInfo(String repositoryId) {
941         RepositoryInfoImpl fRepositoryInfo = new RepositoryInfoImpl();
942 
943         fRepositoryInfo.setId(repositoryId);
944         fRepositoryInfo.setName(getRepositoryName());
945         fRepositoryInfo.setDescription(getRepositoryDescription());
946 
947         fRepositoryInfo.setCmisVersionSupported("1.0");
948 
949         fRepositoryInfo.setProductName("OpenCMIS JCR");
950         fRepositoryInfo.setProductVersion("0.3");
951         fRepositoryInfo.setVendorName("OpenCMIS");
952 
953         fRepositoryInfo.setRootFolder(PathManager.CMIS_ROOT_ID);
954         fRepositoryInfo.setThinClientUri("");
955 
956         RepositoryCapabilitiesImpl capabilities = new RepositoryCapabilitiesImpl();
957         capabilities.setCapabilityAcl(CapabilityAcl.NONE);
958         capabilities.setAllVersionsSearchable(false);
959         capabilities.setCapabilityJoin(CapabilityJoin.NONE);
960         capabilities.setSupportsMultifiling(false);
961         capabilities.setSupportsUnfiling(false);
962         capabilities.setSupportsVersionSpecificFiling(false);
963         capabilities.setIsPwcSearchable(false);
964         capabilities.setIsPwcUpdatable(true);
965         capabilities.setCapabilityQuery(CapabilityQuery.BOTHCOMBINED);
966         capabilities.setCapabilityChanges(CapabilityChanges.NONE);
967         capabilities.setCapabilityContentStreamUpdates(CapabilityContentStreamUpdates.ANYTIME);
968         capabilities.setSupportsGetDescendants(true);
969         capabilities.setSupportsGetFolderTree(true);
970         capabilities.setCapabilityRendition(CapabilityRenditions.NONE);
971         fRepositoryInfo.setCapabilities(capabilities);
972 
973         return fRepositoryInfo;
974     }
975 
976     protected String getRepositoryName() {
977         return repository.getDescriptor(Repository.REP_NAME_DESC);
978     }
979 
980     protected String getRepositoryDescription() {
981         StringBuilder description = new StringBuilder();
982 
983         for (String key : repository.getDescriptorKeys()) {
984             description
985                     .append(key)
986                     .append('=')
987                     .append(repository.getDescriptor(key))
988                     .append('\n');
989         }
990 
991         return description.toString();
992     }
993 
994     protected JcrNode getJcrNode(Session session, String id) {
995         try {
996             if (id == null || id.length() == 0) {
997                 throw new CmisInvalidArgumentException("Null or empty id");
998             }
999 
1000             if (id.equals(PathManager.CMIS_ROOT_ID)) {
1001                 return typeHandlerManager.create(getRootNode(session));
1002             }
1003 
1004             int k = id.indexOf('/');
1005             if (k >= 0) {
1006                 String nodeId = id.substring(0, k);
1007                 String versionName = id.substring(k + 1);
1008 
1009                 Node node = session.getNodeByIdentifier(nodeId);
1010 
1011                 JcrNode jcrNode = typeHandlerManager.create(node);
1012                 if (JcrPrivateWorkingCopy.denotesPwc(versionName)) {
1013                     return jcrNode.asVersion().getPwc();
1014                 }
1015                 else {
1016                     return jcrNode.asVersion().getVersion(versionName);
1017                 }
1018             }
1019             else {
1020                 Node node = session.getNodeByIdentifier(id);
1021                 return typeHandlerManager.create(node);
1022             }
1023 
1024         }
1025         catch (ItemNotFoundException e) {
1026             log.debug(e.getMessage(), e);
1027             throw new CmisObjectNotFoundException(e.getMessage(), e);
1028         }
1029         catch (RepositoryException e) {
1030             log.debug(e.getMessage(), e);
1031             throw new CmisRuntimeException(e.getMessage(), e);
1032         }
1033     }
1034 
1035     protected Node getRootNode(Session session) {
1036         try {
1037             return session.getNode(pathManager.getJcrRootPath());
1038         }
1039         catch (PathNotFoundException e) {
1040             log.debug(e.getMessage(), e);
1041             throw new CmisObjectNotFoundException(e.getMessage(), e);
1042         }
1043         catch (ItemNotFoundException e) {
1044             log.debug(e.getMessage(), e);
1045             throw new CmisObjectNotFoundException(e.getMessage(), e);
1046         }
1047         catch (RepositoryException e) {
1048             log.debug(e.getMessage(), e);
1049             throw new CmisRuntimeException(e.getMessage(), e);
1050         }
1051     }
1052 
1053     //------------------------------------------< private >---
1054 
1055     /**
1056      * Transitively gather the children of a node down to a specific depth
1057      */
1058     private static void gatherDescendants(JcrFolder jcrFolder, List<ObjectInFolderContainer> list,
1059             boolean foldersOnly, int depth, Set<String> filter, Boolean includeAllowableActions,
1060             Boolean includePathSegments, ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
1061 
1062         // iterate through children
1063         Iterator<JcrNode> childNodes = jcrFolder.getNodes();
1064         while (childNodes.hasNext()) {
1065             JcrNode child = childNodes.next();
1066 
1067             // folders only?
1068             if (foldersOnly && !child.isFolder()) {
1069                 continue;
1070             }
1071 
1072             // add to list
1073             ObjectInFolderDataImpl objectInFolder = new ObjectInFolderDataImpl();
1074             objectInFolder.setObject(child.compileObjectType(filter, includeAllowableActions, objectInfos,
1075                     requiresObjectInfo));
1076 
1077             if (Boolean.TRUE.equals(includePathSegments)) {
1078                 objectInFolder.setPathSegment(child.getName());
1079             }
1080 
1081             ObjectInFolderContainerImpl container = new ObjectInFolderContainerImpl();
1082             container.setObject(objectInFolder);
1083 
1084             list.add(container);
1085 
1086             // move to next level
1087             if (depth != 1 && child.isFolder()) {
1088                 container.setChildren(new ArrayList<ObjectInFolderContainer>());
1089                 gatherDescendants(child.asFolder(), container.getChildren(), foldersOnly, depth - 1, filter,
1090                         includeAllowableActions, includePathSegments, objectInfos, requiresObjectInfo);
1091             }
1092         }
1093     }
1094 
1095     /**
1096      * Splits a filter statement into a collection of properties.
1097      */
1098     private static Set<String> splitFilter(String filter) {
1099         if (filter == null) {
1100             return null;
1101         }
1102 
1103         if (filter.trim().length() == 0) {
1104             return null;
1105         }
1106 
1107         Set<String> result = new HashSet<String>();
1108         for (String s : filter.split(",")) {
1109             s = s.trim();
1110             if (s.equals("*")) {
1111                 return null;
1112             } else if (s.length() > 0) {
1113                 result.add(s);
1114             }
1115         }
1116 
1117         // set a few base properties
1118         // query name == id (for base type properties)
1119         result.add(PropertyIds.OBJECT_ID);
1120         result.add(PropertyIds.OBJECT_TYPE_ID);
1121         result.add(PropertyIds.BASE_TYPE_ID);
1122 
1123         return result;
1124     }
1125 }