This project has retired. For details please refer to its Attic page.
FileShareRepository 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.fileshare;
20  
21  import java.io.BufferedInputStream;
22  import java.io.BufferedOutputStream;
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.math.BigDecimal;
31  import java.math.BigInteger;
32  import java.util.ArrayList;
33  import java.util.Collections;
34  import java.util.GregorianCalendar;
35  import java.util.HashMap;
36  import java.util.HashSet;
37  import java.util.LinkedHashMap;
38  import java.util.List;
39  import java.util.Map;
40  import java.util.Set;
41  import java.util.TimeZone;
42  
43  import javax.xml.bind.JAXBElement;
44  import javax.xml.bind.Marshaller;
45  import javax.xml.bind.Unmarshaller;
46  
47  import org.apache.chemistry.opencmis.commons.PropertyIds;
48  import org.apache.chemistry.opencmis.commons.data.Ace;
49  import org.apache.chemistry.opencmis.commons.data.Acl;
50  import org.apache.chemistry.opencmis.commons.data.AllowableActions;
51  import org.apache.chemistry.opencmis.commons.data.ContentStream;
52  import org.apache.chemistry.opencmis.commons.data.FailedToDeleteData;
53  import org.apache.chemistry.opencmis.commons.data.ObjectData;
54  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderContainer;
55  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderData;
56  import org.apache.chemistry.opencmis.commons.data.ObjectInFolderList;
57  import org.apache.chemistry.opencmis.commons.data.ObjectParentData;
58  import org.apache.chemistry.opencmis.commons.data.PermissionMapping;
59  import org.apache.chemistry.opencmis.commons.data.Properties;
60  import org.apache.chemistry.opencmis.commons.data.PropertyData;
61  import org.apache.chemistry.opencmis.commons.data.PropertyDateTime;
62  import org.apache.chemistry.opencmis.commons.data.PropertyId;
63  import org.apache.chemistry.opencmis.commons.data.PropertyString;
64  import org.apache.chemistry.opencmis.commons.data.RepositoryInfo;
65  import org.apache.chemistry.opencmis.commons.definitions.PermissionDefinition;
66  import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
67  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
68  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionContainer;
69  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinitionList;
70  import org.apache.chemistry.opencmis.commons.enums.AclPropagation;
71  import org.apache.chemistry.opencmis.commons.enums.Action;
72  import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
73  import org.apache.chemistry.opencmis.commons.enums.CapabilityAcl;
74  import org.apache.chemistry.opencmis.commons.enums.CapabilityChanges;
75  import org.apache.chemistry.opencmis.commons.enums.CapabilityContentStreamUpdates;
76  import org.apache.chemistry.opencmis.commons.enums.CapabilityJoin;
77  import org.apache.chemistry.opencmis.commons.enums.CapabilityQuery;
78  import org.apache.chemistry.opencmis.commons.enums.CapabilityRenditions;
79  import org.apache.chemistry.opencmis.commons.enums.SupportedPermissions;
80  import org.apache.chemistry.opencmis.commons.enums.Updatability;
81  import org.apache.chemistry.opencmis.commons.enums.VersioningState;
82  import org.apache.chemistry.opencmis.commons.exceptions.CmisBaseException;
83  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
84  import org.apache.chemistry.opencmis.commons.exceptions.CmisContentAlreadyExistsException;
85  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
86  import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException;
87  import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
88  import org.apache.chemistry.opencmis.commons.exceptions.CmisPermissionDeniedException;
89  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
90  import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException;
91  import org.apache.chemistry.opencmis.commons.exceptions.CmisStreamNotSupportedException;
92  import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
93  import org.apache.chemistry.opencmis.commons.impl.Converter;
94  import org.apache.chemistry.opencmis.commons.impl.JaxBHelper;
95  import org.apache.chemistry.opencmis.commons.impl.MimeTypes;
96  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlEntryImpl;
97  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlListImpl;
98  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AccessControlPrincipalDataImpl;
99  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AclCapabilitiesDataImpl;
100 import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
101 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ContentStreamImpl;
102 import org.apache.chemistry.opencmis.commons.impl.dataobjects.FailedToDeleteDataImpl;
103 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
104 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderContainerImpl;
105 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderDataImpl;
106 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectInFolderListImpl;
107 import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectParentDataImpl;
108 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionDefinitionDataImpl;
109 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PermissionMappingDataImpl;
110 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
111 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyBooleanImpl;
112 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDateTimeImpl;
113 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDecimalImpl;
114 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyHtmlImpl;
115 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
116 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIntegerImpl;
117 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
118 import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyUriImpl;
119 import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryCapabilitiesImpl;
120 import org.apache.chemistry.opencmis.commons.impl.dataobjects.RepositoryInfoImpl;
121 import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisObjectType;
122 import org.apache.chemistry.opencmis.commons.impl.jaxb.CmisProperty;
123 import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
124 import org.apache.chemistry.opencmis.commons.server.CallContext;
125 import org.apache.chemistry.opencmis.commons.server.ObjectInfoHandler;
126 import org.apache.chemistry.opencmis.commons.spi.Holder;
127 import org.apache.commons.codec.binary.Base64;
128 import org.apache.commons.logging.Log;
129 import org.apache.commons.logging.LogFactory;
130 
131 /**
132  * File system back-end for CMIS server.
133  */
134 public class FileShareRepository {
135 
136     private static final String ROOT_ID = "@root@";
137     private static final String SHADOW_EXT = ".cmis.xml";
138     private static final String SHADOW_FOLDER = "cmis.xml";
139 
140     private static final String USER_UNKNOWN = "<unknown>";
141 
142     private static final String CMIS_READ = "cmis:read";
143     private static final String CMIS_WRITE = "cmis:write";
144     private static final String CMIS_ALL = "cmis:all";
145 
146     private static final int BUFFER_SIZE = 64 * 1024;
147 
148     private static final Log log = LogFactory.getLog(FileShareRepository.class);
149 
150     /** Repository id */
151     private final String repositoryId;
152     /** Root directory */
153     private final File root;
154     /** Types */
155     private final TypeManager types;
156     /** User table */
157     private final Map<String, Boolean> userMap;
158     /** Repository info */
159     private final RepositoryInfoImpl repositoryInfo;
160 
161     /**
162      * Constructor.
163      * 
164      * @param repId
165      *            CMIS repository id
166      * @param rootPath
167      *            root folder
168      * @param types
169      *            type manager object
170      */
171     public FileShareRepository(String repId, String rootPath, TypeManager types) {
172         // check repository id
173         if ((repId == null) || (repId.trim().length() == 0)) {
174             throw new IllegalArgumentException("Invalid repository id!");
175         }
176 
177         repositoryId = repId;
178 
179         // check root folder
180         if ((rootPath == null) || (rootPath.trim().length() == 0)) {
181             throw new IllegalArgumentException("Invalid root folder!");
182         }
183 
184         root = new File(rootPath);
185         if (!root.isDirectory()) {
186             throw new IllegalArgumentException("Root is not a directory!");
187         }
188 
189         // set types
190         this.types = types;
191 
192         // set up user table
193         userMap = new HashMap<String, Boolean>();
194 
195         // compile repository info
196         repositoryInfo = new RepositoryInfoImpl();
197 
198         repositoryInfo.setId(repositoryId);
199         repositoryInfo.setName(repositoryId);
200         repositoryInfo.setDescription(repositoryId);
201 
202         repositoryInfo.setCmisVersionSupported("1.0");
203 
204         repositoryInfo.setProductName("OpenCMIS FileShare");
205         repositoryInfo.setProductVersion("0.1");
206         repositoryInfo.setVendorName("OpenCMIS");
207 
208         repositoryInfo.setRootFolder(ROOT_ID);
209 
210         repositoryInfo.setThinClientUri("");
211 
212         RepositoryCapabilitiesImpl capabilities = new RepositoryCapabilitiesImpl();
213         capabilities.setCapabilityAcl(CapabilityAcl.DISCOVER);
214         capabilities.setAllVersionsSearchable(false);
215         capabilities.setCapabilityJoin(CapabilityJoin.NONE);
216         capabilities.setSupportsMultifiling(false);
217         capabilities.setSupportsUnfiling(false);
218         capabilities.setSupportsVersionSpecificFiling(false);
219         capabilities.setIsPwcSearchable(false);
220         capabilities.setIsPwcUpdatable(false);
221         capabilities.setCapabilityQuery(CapabilityQuery.NONE);
222         capabilities.setCapabilityChanges(CapabilityChanges.NONE);
223         capabilities.setCapabilityContentStreamUpdates(CapabilityContentStreamUpdates.ANYTIME);
224         capabilities.setSupportsGetDescendants(true);
225         capabilities.setSupportsGetFolderTree(true);
226         capabilities.setCapabilityRendition(CapabilityRenditions.NONE);
227 
228         repositoryInfo.setCapabilities(capabilities);
229 
230         AclCapabilitiesDataImpl aclCapability = new AclCapabilitiesDataImpl();
231         aclCapability.setSupportedPermissions(SupportedPermissions.BASIC);
232         aclCapability.setAclPropagation(AclPropagation.OBJECTONLY);
233 
234         // permissions
235         List<PermissionDefinition> permissions = new ArrayList<PermissionDefinition>();
236         permissions.add(createPermission(CMIS_READ, "Read"));
237         permissions.add(createPermission(CMIS_WRITE, "Write"));
238         permissions.add(createPermission(CMIS_ALL, "All"));
239         aclCapability.setPermissionDefinitionData(permissions);
240 
241         // mapping
242         List<PermissionMapping> list = new ArrayList<PermissionMapping>();
243         list.add(createMapping(PermissionMapping.CAN_CREATE_DOCUMENT_FOLDER, CMIS_READ));
244         list.add(createMapping(PermissionMapping.CAN_CREATE_FOLDER_FOLDER, CMIS_READ));
245         list.add(createMapping(PermissionMapping.CAN_DELETE_CONTENT_DOCUMENT, CMIS_WRITE));
246         list.add(createMapping(PermissionMapping.CAN_DELETE_OBJECT, CMIS_ALL));
247         list.add(createMapping(PermissionMapping.CAN_DELETE_TREE_FOLDER, CMIS_ALL));
248         list.add(createMapping(PermissionMapping.CAN_GET_ACL_OBJECT, CMIS_READ));
249         list.add(createMapping(PermissionMapping.CAN_GET_ALL_VERSIONS_VERSION_SERIES, CMIS_READ));
250         list.add(createMapping(PermissionMapping.CAN_GET_CHILDREN_FOLDER, CMIS_READ));
251         list.add(createMapping(PermissionMapping.CAN_GET_DESCENDENTS_FOLDER, CMIS_READ));
252         list.add(createMapping(PermissionMapping.CAN_GET_FOLDER_PARENT_OBJECT, CMIS_READ));
253         list.add(createMapping(PermissionMapping.CAN_GET_PARENTS_FOLDER, CMIS_READ));
254         list.add(createMapping(PermissionMapping.CAN_GET_PROPERTIES_OBJECT, CMIS_READ));
255         list.add(createMapping(PermissionMapping.CAN_MOVE_OBJECT, CMIS_WRITE));
256         list.add(createMapping(PermissionMapping.CAN_MOVE_SOURCE, CMIS_READ));
257         list.add(createMapping(PermissionMapping.CAN_MOVE_TARGET, CMIS_WRITE));
258         list.add(createMapping(PermissionMapping.CAN_SET_CONTENT_DOCUMENT, CMIS_WRITE));
259         list.add(createMapping(PermissionMapping.CAN_UPDATE_PROPERTIES_OBJECT, CMIS_WRITE));
260         list.add(createMapping(PermissionMapping.CAN_VIEW_CONTENT_OBJECT, CMIS_READ));
261         Map<String, PermissionMapping> map = new LinkedHashMap<String, PermissionMapping>();
262         for (PermissionMapping pm : list) {
263             map.put(pm.getKey(), pm);
264         }
265         aclCapability.setPermissionMappingData(map);
266 
267         repositoryInfo.setAclCapabilities(aclCapability);
268     }
269 
270     private static PermissionDefinition createPermission(String permission, String description) {
271         PermissionDefinitionDataImpl pd = new PermissionDefinitionDataImpl();
272         pd.setPermission(permission);
273         pd.setDescription(description);
274 
275         return pd;
276     }
277 
278     private static PermissionMapping createMapping(String key, String permission) {
279         PermissionMappingDataImpl pm = new PermissionMappingDataImpl();
280         pm.setKey(key);
281         pm.setPermissions(Collections.singletonList(permission));
282 
283         return pm;
284     }
285 
286     /**
287      * Adds a user to the repository.
288      */
289     public void addUser(String user, boolean readOnly) {
290         if ((user == null) || (user.length() == 0)) {
291             return;
292         }
293 
294         userMap.put(user, readOnly);
295     }
296 
297     // --- the public stuff ---
298 
299     /**
300      * Returns the repository id.
301      */
302     public String getRepositoryId() {
303         return repositoryId;
304     }
305 
306     /**
307      * CMIS getRepositoryInfo.
308      */
309     public RepositoryInfo getRepositoryInfo(CallContext context) {
310         debug("getRepositoryInfo");
311         checkUser(context, false);
312 
313         return repositoryInfo;
314     }
315 
316     /**
317      * CMIS getTypesChildren.
318      */
319     public TypeDefinitionList getTypesChildren(CallContext context, String typeId, boolean includePropertyDefinitions,
320             BigInteger maxItems, BigInteger skipCount) {
321         debug("getTypesChildren");
322         checkUser(context, false);
323 
324         return types.getTypesChildren(context, typeId, includePropertyDefinitions, maxItems, skipCount);
325     }
326 
327     /**
328      * CMIS getTypeDefinition.
329      */
330     public TypeDefinition getTypeDefinition(CallContext context, String typeId) {
331         debug("getTypeDefinition");
332         checkUser(context, false);
333 
334         return types.getTypeDefinition(context, typeId);
335     }
336 
337     /**
338      * CMIS getTypesDescendants.
339      */
340     public List<TypeDefinitionContainer> getTypesDescendants(CallContext context, String typeId, BigInteger depth,
341             Boolean includePropertyDefinitions) {
342         debug("getTypesDescendants");
343         checkUser(context, false);
344 
345         return types.getTypesDescendants(context, typeId, depth, includePropertyDefinitions);
346     }
347 
348     /**
349      * Create* dispatch for AtomPub.
350      */
351     public ObjectData create(CallContext context, Properties properties, String folderId, ContentStream contentStream,
352             VersioningState versioningState, ObjectInfoHandler objectInfos) {
353         debug("create");
354         boolean userReadOnly = checkUser(context, true);
355 
356         String typeId = getTypeId(properties);
357         TypeDefinition type = types.getType(typeId);
358         if (type == null) {
359             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
360         }
361 
362         String objectId = null;
363         if (type.getBaseTypeId() == BaseTypeId.CMIS_DOCUMENT) {
364             objectId = createDocument(context, properties, folderId, contentStream, versioningState);
365         } else if (type.getBaseTypeId() == BaseTypeId.CMIS_FOLDER) {
366             objectId = createFolder(context, properties, folderId);
367         } else {
368             throw new CmisObjectNotFoundException("Cannot create object of type '" + typeId + "'!");
369         }
370 
371         return compileObjectType(context, getFile(objectId), null, false, false, userReadOnly, objectInfos);
372     }
373 
374     /**
375      * CMIS createDocument.
376      */
377     public String createDocument(CallContext context, Properties properties, String folderId,
378             ContentStream contentStream, VersioningState versioningState) {
379         debug("createDocument");
380         checkUser(context, true);
381 
382         // check properties
383         if ((properties == null) || (properties.getProperties() == null)) {
384             throw new CmisInvalidArgumentException("Properties must be set!");
385         }
386 
387         // check versioning state
388         if (VersioningState.NONE != versioningState) {
389             throw new CmisConstraintException("Versioning not supported!");
390         }
391 
392         // check type
393         String typeId = getTypeId(properties);
394         TypeDefinition type = types.getType(typeId);
395         if (type == null) {
396             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
397         }
398 
399         // compile the properties
400         Properties props = compileProperties(typeId, context.getUsername(),
401                 millisToCalendar(System.currentTimeMillis()), context.getUsername(), properties);
402 
403         // check the name
404         String name = getStringProperty(properties, PropertyIds.NAME);
405         if (!isValidName(name)) {
406             throw new CmisNameConstraintViolationException("Name is not valid!");
407         }
408 
409         // get parent File
410         File parent = getFile(folderId);
411         if (!parent.isDirectory()) {
412             throw new CmisObjectNotFoundException("Parent is not a folder!");
413         }
414 
415         // check the file
416         File newFile = new File(parent, name);
417         if (newFile.exists()) {
418             throw new CmisNameConstraintViolationException("Document already exists!");
419         }
420 
421         // create the file
422         try {
423             newFile.createNewFile();
424         } catch (IOException e) {
425             throw new CmisStorageException("Could not create file: " + e.getMessage());
426         }
427 
428         // write content, if available
429         if ((contentStream != null) && (contentStream.getStream() != null)) {
430             try {
431                 OutputStream out = new BufferedOutputStream(new FileOutputStream(newFile), BUFFER_SIZE);
432                 InputStream in = new BufferedInputStream(contentStream.getStream(), BUFFER_SIZE);
433 
434                 byte[] buffer = new byte[BUFFER_SIZE];
435                 int b;
436                 while ((b = in.read(buffer)) > -1) {
437                     out.write(buffer, 0, b);
438                 }
439 
440                 out.flush();
441                 out.close();
442                 in.close();
443             } catch (Exception e) {
444                 throw new CmisStorageException("Could not write content: " + e.getMessage(), e);
445             }
446         }
447 
448         // write properties
449         writePropertiesFile(newFile, props);
450 
451         return getId(newFile);
452     }
453 
454     /**
455      * CMIS createDocumentFromSource.
456      */
457     public String createDocumentFromSource(CallContext context, String sourceId, Properties properties,
458             String folderId, VersioningState versioningState) {
459 
460         // check versioning state
461         if (VersioningState.NONE != versioningState) {
462             throw new CmisConstraintException("Versioning not supported!");
463         }
464 
465         // get parent File
466         File parent = getFile(folderId);
467         if (!parent.isDirectory()) {
468             throw new CmisObjectNotFoundException("Parent is not a folder!");
469         }
470 
471         // get source File
472         File source = getFile(sourceId);
473         if (!source.isFile()) {
474             throw new CmisObjectNotFoundException("Source is not a document!");
475         }
476 
477         // file name
478         String name = source.getName();
479 
480         // get properties
481         PropertiesImpl sourceProperties = new PropertiesImpl();
482         readCustomProperties(source, sourceProperties, null, new ObjectInfoImpl());
483 
484         // get the type id
485         String typeId = getIdProperty(sourceProperties, PropertyIds.OBJECT_TYPE_ID);
486         if (typeId == null) {
487             typeId = TypeManager.DOCUMENT_TYPE_ID;
488         }
489 
490         // copy properties
491         PropertiesImpl newProperties = new PropertiesImpl();
492         for (PropertyData<?> prop : sourceProperties.getProperties().values()) {
493             if ((prop.getId().equals(PropertyIds.OBJECT_TYPE_ID)) || (prop.getId().equals(PropertyIds.CREATED_BY))
494                     || (prop.getId().equals(PropertyIds.CREATION_DATE))
495                     || (prop.getId().equals(PropertyIds.LAST_MODIFIED_BY))) {
496                 continue;
497             }
498 
499             newProperties.addProperty(prop);
500         }
501 
502         // replace properties
503         if (properties != null) {
504             // find new name
505             String newName = getStringProperty(properties, PropertyIds.NAME);
506             if (newName != null) {
507                 if (!isValidName(newName)) {
508                     throw new CmisNameConstraintViolationException("Name is not valid!");
509                 }
510                 name = newName;
511             }
512 
513             // get the property definitions
514             TypeDefinition type = types.getType(typeId);
515             if (type == null) {
516                 throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
517             }
518 
519             // replace with new values
520             for (PropertyData<?> prop : properties.getProperties().values()) {
521                 PropertyDefinition<?> propType = type.getPropertyDefinitions().get(prop.getId());
522 
523                 // do we know that property?
524                 if (propType == null) {
525                     throw new CmisConstraintException("Property '" + prop.getId() + "' is unknown!");
526                 }
527 
528                 // can it be set?
529                 if ((propType.getUpdatability() != Updatability.READWRITE)) {
530                     throw new CmisConstraintException("Property '" + prop.getId() + "' cannot be updated!");
531                 }
532 
533                 // empty properties are invalid
534                 if (isEmptyProperty(prop)) {
535                     throw new CmisConstraintException("Property '" + prop.getId() + "' must not be empty!");
536                 }
537 
538                 newProperties.addProperty(prop);
539             }
540         }
541 
542         addPropertyId(newProperties, typeId, null, PropertyIds.OBJECT_TYPE_ID, typeId);
543         addPropertyString(newProperties, typeId, null, PropertyIds.CREATED_BY, context.getUsername());
544         addPropertyDateTime(newProperties, typeId, null, PropertyIds.CREATION_DATE,
545                 millisToCalendar(System.currentTimeMillis()));
546         addPropertyString(newProperties, typeId, null, PropertyIds.LAST_MODIFIED_BY, context.getUsername());
547 
548         // check the file
549         File newFile = new File(parent, name);
550         if (newFile.exists()) {
551             throw new CmisNameConstraintViolationException("Document already exists.");
552         }
553 
554         // create the file
555         try {
556             newFile.createNewFile();
557         } catch (IOException e) {
558             throw new CmisStorageException("Could not create file: " + e.getMessage(), e);
559         }
560 
561         // copy content
562         try {
563             OutputStream out = new BufferedOutputStream(new FileOutputStream(newFile));
564             InputStream in = new BufferedInputStream(new FileInputStream(source));
565 
566             byte[] buffer = new byte[BUFFER_SIZE];
567             int b;
568             while ((b = in.read(buffer)) > -1) {
569                 out.write(buffer, 0, b);
570             }
571 
572             out.flush();
573             out.close();
574             in.close();
575         } catch (Exception e) {
576             throw new CmisStorageException("Could not roead or write content: " + e.getMessage(), e);
577         }
578 
579         // write properties
580         writePropertiesFile(newFile, newProperties);
581 
582         return getId(newFile);
583     }
584 
585     /**
586      * CMIS createFolder.
587      */
588     public String createFolder(CallContext context, Properties properties, String folderId) {
589         debug("createFolder");
590         checkUser(context, true);
591 
592         // check properties
593         if ((properties == null) || (properties.getProperties() == null)) {
594             throw new CmisInvalidArgumentException("Properties must be set!");
595         }
596 
597         // check type
598         String typeId = getTypeId(properties);
599         TypeDefinition type = types.getType(typeId);
600         if (type == null) {
601             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
602         }
603 
604         // compile the properties
605         Properties props = compileProperties(typeId, context.getUsername(),
606                 millisToCalendar(System.currentTimeMillis()), context.getUsername(), properties);
607 
608         // check the name
609         String name = getStringProperty(properties, PropertyIds.NAME);
610         if (!isValidName(name)) {
611             throw new CmisNameConstraintViolationException("Name is not valid.");
612         }
613 
614         // get parent File
615         File parent = getFile(folderId);
616         if (!parent.isDirectory()) {
617             throw new CmisObjectNotFoundException("Parent is not a folder!");
618         }
619 
620         // create the folder
621         File newFolder = new File(parent, name);
622         if (!newFolder.mkdir()) {
623             throw new CmisStorageException("Could not create folder!");
624         }
625 
626         // write properties
627         writePropertiesFile(newFolder, props);
628 
629         return getId(newFolder);
630     }
631 
632     /**
633      * CMIS moveObject.
634      */
635     public ObjectData moveObject(CallContext context, Holder<String> objectId, String targetFolderId,
636             ObjectInfoHandler objectInfos) {
637         debug("moveObject");
638         boolean userReadOnly = checkUser(context, true);
639 
640         if (objectId == null) {
641             throw new CmisInvalidArgumentException("Id is not valid!");
642         }
643 
644         // get the file and parent
645         File file = getFile(objectId.getValue());
646         File parent = getFile(targetFolderId);
647 
648         // build new path
649         File newFile = new File(parent, file.getName());
650         if (newFile.exists()) {
651             throw new CmisStorageException("Object already exists!");
652         }
653 
654         // move it
655         if (!file.renameTo(newFile)) {
656             throw new CmisStorageException("Move failed!");
657         } else {
658             // set new id
659             objectId.setValue(getId(newFile));
660 
661             // if it is a file, move properties file too
662             if (newFile.isFile()) {
663                 File propFile = getPropertiesFile(file);
664                 if (propFile.exists()) {
665                     File newPropFile = new File(parent, propFile.getName());
666                     propFile.renameTo(newPropFile);
667                 }
668             }
669         }
670 
671         return compileObjectType(context, newFile, null, false, false, userReadOnly, objectInfos);
672     }
673 
674     /**
675      * CMIS setContentStream and deleteContentStream.
676      */
677     public void setContentStream(CallContext context, Holder<String> objectId, Boolean overwriteFlag,
678             ContentStream contentStream) {
679         debug("setContentStream or deleteContentStream");
680         checkUser(context, true);
681 
682         if (objectId == null) {
683             throw new CmisInvalidArgumentException("Id is not valid!");
684         }
685 
686         // get the file
687         File file = getFile(objectId.getValue());
688         if (!file.isFile()) {
689             throw new CmisStreamNotSupportedException("Not a file!");
690         }
691 
692         // check overwrite
693         boolean owf = (overwriteFlag == null ? true : overwriteFlag.booleanValue());
694         if (!owf && file.length() > 0) {
695             throw new CmisContentAlreadyExistsException("Content already exists!");
696         }
697 
698         try {
699             OutputStream out = new BufferedOutputStream(new FileOutputStream(file), BUFFER_SIZE);
700 
701             if ((contentStream == null) || (contentStream.getStream() == null)) {
702                 // delete content
703                 out.write(new byte[0]);
704             } else {
705                 // set content
706                 InputStream in = new BufferedInputStream(contentStream.getStream(), BUFFER_SIZE);
707 
708                 byte[] buffer = new byte[BUFFER_SIZE];
709                 int b;
710                 while ((b = in.read(buffer)) > -1) {
711                     out.write(buffer, 0, b);
712                 }
713 
714                 in.close();
715             }
716 
717             out.close();
718         } catch (Exception e) {
719             throw new CmisStorageException("Could not write content: " + e.getMessage(), e);
720         }
721     }
722 
723     /**
724      * CMIS deleteObject.
725      */
726     public void deleteObject(CallContext context, String objectId) {
727         debug("deleteObject");
728         checkUser(context, true);
729 
730         // get the file or folder
731         File file = getFile(objectId);
732         if (!file.exists()) {
733             throw new CmisObjectNotFoundException("Object not found!");
734         }
735 
736         // check if it is a folder and if it is empty
737         if (!isFolderEmpty(file)) {
738             throw new CmisConstraintException("Folder is not empty!");
739         }
740 
741         // delete properties and actual file
742         getPropertiesFile(file).delete();
743         if (!file.delete()) {
744             throw new CmisStorageException("Deletion failed!");
745         }
746     }
747 
748     /**
749      * CMIS deleteTree.
750      */
751     public FailedToDeleteData deleteTree(CallContext context, String folderId, Boolean continueOnFailure) {
752         debug("deleteTree");
753         checkUser(context, true);
754 
755         boolean cof = (continueOnFailure == null ? false : continueOnFailure.booleanValue());
756 
757         // get the file or folder
758         File file = getFile(folderId);
759 
760         FailedToDeleteDataImpl result = new FailedToDeleteDataImpl();
761         result.setIds(new ArrayList<String>());
762 
763         // if it is a folder, remove it recursively
764         if (file.isDirectory()) {
765             deleteFolder(file, cof, result);
766         } else {
767             getPropertiesFile(file).delete();
768             if (!file.delete()) {
769                 result.getIds().add(getId(file));
770             }
771         }
772 
773         return result;
774     }
775 
776     /**
777      * CMIS updateProperties.
778      */
779     public ObjectData updateProperties(CallContext context, Holder<String> objectId, Properties properties,
780             ObjectInfoHandler objectInfos) {
781         debug("updateProperties");
782         boolean userReadOnly = checkUser(context, true);
783 
784         if (objectId == null) {
785             throw new CmisInvalidArgumentException("Id is not valid!");
786         }
787 
788         // get the file or folder
789         File file = getFile(objectId.getValue());
790 
791         // get and check the new name
792         String newName = getStringProperty(properties, PropertyIds.NAME);
793         boolean isRename = (newName != null) && (!file.getName().equals(newName));
794         if (isRename && !isValidName(newName)) {
795             throw new CmisNameConstraintViolationException("Name is not valid!");
796         }
797 
798         // get old properties
799         PropertiesImpl oldProperties = new PropertiesImpl();
800         readCustomProperties(file, oldProperties, null, new ObjectInfoImpl());
801 
802         // get the type id
803         String typeId = getIdProperty(oldProperties, PropertyIds.OBJECT_TYPE_ID);
804         if (typeId == null) {
805             typeId = (file.isDirectory() ? TypeManager.FOLDER_TYPE_ID : TypeManager.DOCUMENT_TYPE_ID);
806         }
807 
808         // get the creator
809         String creator = getStringProperty(oldProperties, PropertyIds.CREATED_BY);
810         if (creator == null) {
811             creator = context.getUsername();
812         }
813 
814         // get creation date
815         GregorianCalendar creationDate = getDateTimeProperty(oldProperties, PropertyIds.CREATION_DATE);
816         if (creationDate == null) {
817             creationDate = millisToCalendar(file.lastModified());
818         }
819 
820         // compile the properties
821         Properties props = updateProperties(typeId, creator, creationDate, context.getUsername(), oldProperties,
822                 properties);
823 
824         // write properties
825         writePropertiesFile(file, props);
826 
827         // rename file or folder if necessary
828         File newFile = file;
829         if (isRename) {
830             File parent = file.getParentFile();
831             File propFile = getPropertiesFile(file);
832             newFile = new File(parent, newName);
833             if (!file.renameTo(newFile)) {
834                 // if something went wrong, throw an exception
835                 throw new CmisUpdateConflictException("Could not rename object!");
836             } else {
837                 // set new id
838                 objectId.setValue(getId(newFile));
839 
840                 // if it is a file, rename properties file too
841                 if (newFile.isFile()) {
842                     if (propFile.exists()) {
843                         File newPropFile = new File(parent, newName + SHADOW_EXT);
844                         propFile.renameTo(newPropFile);
845                     }
846                 }
847             }
848         }
849 
850         return compileObjectType(context, newFile, null, false, false, userReadOnly, objectInfos);
851     }
852 
853     /**
854      * CMIS getObject.
855      */
856     public ObjectData getObject(CallContext context, String objectId, String versionServicesId, String filter,
857             Boolean includeAllowableActions, Boolean includeAcl, ObjectInfoHandler objectInfos) {
858         debug("getObject");
859         boolean userReadOnly = checkUser(context, false);
860 
861         // check id
862         if ((objectId == null) && (versionServicesId == null)) {
863             throw new CmisInvalidArgumentException("Object Id must be set.");
864         }
865 
866         if (objectId == null) {
867             // this works only because there are no versions in a file system
868             // and the object id and version series id are the same
869             objectId = versionServicesId;
870         }
871 
872         // get the file or folder
873         File file = getFile(objectId);
874 
875         // set defaults if values not set
876         boolean iaa = (includeAllowableActions == null ? false : includeAllowableActions.booleanValue());
877         boolean iacl = (includeAcl == null ? false : includeAcl.booleanValue());
878 
879         // split filter
880         Set<String> filterCollection = splitFilter(filter);
881 
882         // gather properties
883         return compileObjectType(context, file, filterCollection, iaa, iacl, userReadOnly, objectInfos);
884     }
885 
886     /**
887      * CMIS getAllowableActions.
888      */
889     public AllowableActions getAllowableActions(CallContext context, String objectId) {
890         debug("getAllowableActions");
891         boolean userReadOnly = checkUser(context, false);
892 
893         File file = getFile(objectId);
894         if (!file.exists()) {
895             throw new CmisObjectNotFoundException("Object not found!");
896         }
897 
898         return compileAllowableActions(file, userReadOnly);
899     }
900 
901     /**
902      * CMIS getACL.
903      */
904     public Acl getAcl(CallContext context, String objectId) {
905         debug("getAcl");
906         checkUser(context, false);
907 
908         // get the file or folder
909         File file = getFile(objectId);
910         if (!file.exists()) {
911             throw new CmisObjectNotFoundException("Object not found!");
912         }
913 
914         return compileAcl(file);
915     }
916 
917     /**
918      * CMIS getContentStream.
919      */
920     public ContentStream getContentStream(CallContext context, String objectId, BigInteger offset, BigInteger length) {
921         debug("getContentStream");
922         checkUser(context, false);
923 
924         if ((offset != null) || (length != null)) {
925             throw new CmisInvalidArgumentException("Offset and Length are not supported!");
926         }
927 
928         // get the file
929         final File file = getFile(objectId);
930         if (!file.isFile()) {
931             throw new CmisStreamNotSupportedException("Not a file!");
932         }
933 
934         if (file.length() == 0) {
935             throw new CmisConstraintException("Document has no content!");
936         }
937 
938         InputStream stream = null;
939         try {
940             stream = new BufferedInputStream(new FileInputStream(file), 4 * 1024);
941         } catch (FileNotFoundException e) {
942             throw new CmisObjectNotFoundException(e.getMessage(), e);
943         }
944 
945         // compile data
946         ContentStreamImpl result = new ContentStreamImpl();
947         result.setFileName(file.getName());
948         result.setLength(BigInteger.valueOf(file.length()));
949         result.setMimeType(MimeTypes.getMIMEType(file));
950         result.setStream(stream);
951 
952         return result;
953     }
954 
955     /**
956      * CMIS getChildren.
957      */
958     public ObjectInFolderList getChildren(CallContext context, String folderId, String filter,
959             Boolean includeAllowableActions, Boolean includePathSegment, BigInteger maxItems, BigInteger skipCount,
960             ObjectInfoHandler objectInfos) {
961         debug("getChildren");
962         boolean userReadOnly = checkUser(context, false);
963 
964         // split filter
965         Set<String> filterCollection = splitFilter(filter);
966 
967         // set defaults if values not set
968         boolean iaa = (includeAllowableActions == null ? false : includeAllowableActions.booleanValue());
969         boolean ips = (includePathSegment == null ? false : includePathSegment.booleanValue());
970 
971         // skip and max
972         int skip = (skipCount == null ? 0 : skipCount.intValue());
973         if (skip < 0) {
974             skip = 0;
975         }
976 
977         int max = (maxItems == null ? Integer.MAX_VALUE : maxItems.intValue());
978         if (max < 0) {
979             max = Integer.MAX_VALUE;
980         }
981 
982         // get the folder
983         File folder = getFile(folderId);
984         if (!folder.isDirectory()) {
985             throw new CmisObjectNotFoundException("Not a folder!");
986         }
987 
988         // set object info of the the folder
989         if (context.isObjectInfoRequired()) {
990             compileObjectType(context, folder, null, false, false, userReadOnly, objectInfos);
991         }
992 
993         // prepare result
994         ObjectInFolderListImpl result = new ObjectInFolderListImpl();
995         result.setObjects(new ArrayList<ObjectInFolderData>());
996         result.setHasMoreItems(false);
997         int count = 0;
998 
999         // iterate through children
1000         for (File child : folder.listFiles()) {
1001             // skip hidden and shadow files
1002             if (child.isHidden() || child.getName().equals(SHADOW_FOLDER) || child.getPath().endsWith(SHADOW_EXT)) {
1003                 continue;
1004             }
1005 
1006             count++;
1007 
1008             if (skip > 0) {
1009                 skip--;
1010                 continue;
1011             }
1012 
1013             if (result.getObjects().size() >= max) {
1014                 result.setHasMoreItems(true);
1015                 continue;
1016             }
1017 
1018             // build and add child object
1019             ObjectInFolderDataImpl objectInFolder = new ObjectInFolderDataImpl();
1020             objectInFolder.setObject(compileObjectType(context, child, filterCollection, iaa, false, userReadOnly,
1021                     objectInfos));
1022             if (ips) {
1023                 objectInFolder.setPathSegment(child.getName());
1024             }
1025 
1026             result.getObjects().add(objectInFolder);
1027         }
1028 
1029         result.setNumItems(BigInteger.valueOf(count));
1030 
1031         return result;
1032     }
1033 
1034     /**
1035      * CMIS getDescendants.
1036      */
1037     public List<ObjectInFolderContainer> getDescendants(CallContext context, String folderId, BigInteger depth,
1038             String filter, Boolean includeAllowableActions, Boolean includePathSegment, ObjectInfoHandler objectInfos,
1039             boolean foldersOnly) {
1040         debug("getDescendants or getFolderTree");
1041         boolean userReadOnly = checkUser(context, false);
1042 
1043         // check depth
1044         int d = (depth == null ? 2 : depth.intValue());
1045         if (d == 0) {
1046             throw new CmisInvalidArgumentException("Depth must not be 0!");
1047         }
1048         if (d < -1) {
1049             d = -1;
1050         }
1051 
1052         // split filter
1053         Set<String> filterCollection = splitFilter(filter);
1054 
1055         // set defaults if values not set
1056         boolean iaa = (includeAllowableActions == null ? false : includeAllowableActions.booleanValue());
1057         boolean ips = (includePathSegment == null ? false : includePathSegment.booleanValue());
1058 
1059         // get the folder
1060         File folder = getFile(folderId);
1061         if (!folder.isDirectory()) {
1062             throw new CmisObjectNotFoundException("Not a folder!");
1063         }
1064 
1065         // set object info of the the folder
1066         if (context.isObjectInfoRequired()) {
1067             compileObjectType(context, folder, null, false, false, userReadOnly, objectInfos);
1068         }
1069 
1070         // get the tree
1071         List<ObjectInFolderContainer> result = new ArrayList<ObjectInFolderContainer>();
1072         gatherDescendants(context, folder, result, foldersOnly, d, filterCollection, iaa, ips, userReadOnly,
1073                 objectInfos);
1074 
1075         return result;
1076     }
1077 
1078     /**
1079      * CMIS getFolderParent.
1080      */
1081     public ObjectData getFolderParent(CallContext context, String folderId, String filter, ObjectInfoHandler objectInfos) {
1082         List<ObjectParentData> parents = getObjectParents(context, folderId, filter, false, false, objectInfos);
1083 
1084         if (parents.size() == 0) {
1085             throw new CmisInvalidArgumentException("The root folder has no parent!");
1086         }
1087 
1088         return parents.get(0).getObject();
1089     }
1090 
1091     /**
1092      * CMIS getObjectParents.
1093      */
1094     public List<ObjectParentData> getObjectParents(CallContext context, String objectId, String filter,
1095             Boolean includeAllowableActions, Boolean includeRelativePathSegment, ObjectInfoHandler objectInfos) {
1096         debug("getObjectParents");
1097         boolean userReadOnly = checkUser(context, false);
1098 
1099         // split filter
1100         Set<String> filterCollection = splitFilter(filter);
1101 
1102         // set defaults if values not set
1103         boolean iaa = (includeAllowableActions == null ? false : includeAllowableActions.booleanValue());
1104         boolean irps = (includeRelativePathSegment == null ? false : includeRelativePathSegment.booleanValue());
1105 
1106         // get the file or folder
1107         File file = getFile(objectId);
1108 
1109         // don't climb above the root folder
1110         if (root.equals(file)) {
1111             return Collections.emptyList();
1112         }
1113 
1114         // set object info of the the object
1115         if (context.isObjectInfoRequired()) {
1116             compileObjectType(context, file, null, false, false, userReadOnly, objectInfos);
1117         }
1118 
1119         // get parent folder
1120         File parent = file.getParentFile();
1121         ObjectData object = compileObjectType(context, parent, filterCollection, iaa, false, userReadOnly, objectInfos);
1122 
1123         ObjectParentDataImpl result = new ObjectParentDataImpl();
1124         result.setObject(object);
1125         if (irps) {
1126             result.setRelativePathSegment(file.getName());
1127         }
1128 
1129         return Collections.singletonList((ObjectParentData) result);
1130     }
1131 
1132     /**
1133      * CMIS getObjectByPath.
1134      */
1135     public ObjectData getObjectByPath(CallContext context, String folderPath, String filter,
1136             boolean includeAllowableActions, boolean includeACL, ObjectInfoHandler objectInfos) {
1137         debug("getObjectByPath");
1138         boolean userReadOnly = checkUser(context, false);
1139 
1140         // split filter
1141         Set<String> filterCollection = splitFilter(filter);
1142 
1143         // check path
1144         if ((folderPath == null) || (!folderPath.startsWith("/"))) {
1145             throw new CmisInvalidArgumentException("Invalid folder path!");
1146         }
1147 
1148         // get the file or folder
1149         File file = null;
1150         if (folderPath.length() == 1) {
1151             file = root;
1152         } else {
1153             String path = folderPath.replace('/', File.separatorChar).substring(1);
1154             file = new File(root, path);
1155         }
1156 
1157         if (!file.exists()) {
1158             throw new CmisObjectNotFoundException("Path doesn't exist.");
1159         }
1160 
1161         return compileObjectType(context, file, filterCollection, includeAllowableActions, includeACL, userReadOnly,
1162                 objectInfos);
1163     }
1164 
1165     // --- helper methods ---
1166 
1167     /**
1168      * Gather the children of a folder.
1169      */
1170     private void gatherDescendants(CallContext context, File folder, List<ObjectInFolderContainer> list,
1171             boolean foldersOnly, int depth, Set<String> filter, boolean includeAllowableActions,
1172             boolean includePathSegments, boolean userReadOnly, ObjectInfoHandler objectInfos) {
1173         // iterate through children
1174         for (File child : folder.listFiles()) {
1175             // skip hidden and shadow files
1176             if (child.isHidden() || child.getName().equals(SHADOW_FOLDER) || child.getPath().endsWith(SHADOW_EXT)) {
1177                 continue;
1178             }
1179 
1180             // folders only?
1181             if (foldersOnly && !child.isDirectory()) {
1182                 continue;
1183             }
1184 
1185             // add to list
1186             ObjectInFolderDataImpl objectInFolder = new ObjectInFolderDataImpl();
1187             objectInFolder.setObject(compileObjectType(context, child, filter, includeAllowableActions, false,
1188                     userReadOnly, objectInfos));
1189             if (includePathSegments) {
1190                 objectInFolder.setPathSegment(child.getName());
1191             }
1192 
1193             ObjectInFolderContainerImpl container = new ObjectInFolderContainerImpl();
1194             container.setObject(objectInFolder);
1195 
1196             list.add(container);
1197 
1198             // move to next level
1199             if ((depth != 1) && child.isDirectory()) {
1200                 container.setChildren(new ArrayList<ObjectInFolderContainer>());
1201                 gatherDescendants(context, child, container.getChildren(), foldersOnly, depth - 1, filter,
1202                         includeAllowableActions, includePathSegments, userReadOnly, objectInfos);
1203             }
1204         }
1205     }
1206 
1207     /**
1208      * Removes a folder and its content.
1209      * 
1210      * @throws
1211      */
1212     private boolean deleteFolder(File folder, boolean continueOnFailure, FailedToDeleteDataImpl ftd) {
1213         boolean success = true;
1214 
1215         for (File file : folder.listFiles()) {
1216             if (file.isDirectory()) {
1217                 if (!deleteFolder(file, continueOnFailure, ftd)) {
1218                     if (!continueOnFailure) {
1219                         return false;
1220                     }
1221                     success = false;
1222                 }
1223             } else {
1224                 if (!file.delete()) {
1225                     ftd.getIds().add(getId(file));
1226                     if (!continueOnFailure) {
1227                         return false;
1228                     }
1229                     success = false;
1230                 }
1231             }
1232         }
1233 
1234         if (!folder.delete()) {
1235             ftd.getIds().add(getId(folder));
1236             success = false;
1237         }
1238 
1239         return success;
1240     }
1241 
1242     /**
1243      * Checks if the given name is valid for a file system.
1244      * 
1245      * @param name
1246      *            the name to check
1247      * 
1248      * @return <code>true</code> if the name is valid, <code>false</code>
1249      *         otherwise
1250      */
1251     private static boolean isValidName(String name) {
1252         if ((name == null) || (name.length() == 0) || (name.indexOf(File.separatorChar) != -1)
1253                 || (name.indexOf(File.pathSeparatorChar) != -1)) {
1254             return false;
1255         }
1256 
1257         return true;
1258     }
1259 
1260     /**
1261      * Checks if a folder is empty. A folder is considered as empty if no files
1262      * or only the shadow file reside in the folder.
1263      * 
1264      * @param folder
1265      *            the folder
1266      * 
1267      * @return <code>true</code> if the folder is empty.
1268      */
1269     private static boolean isFolderEmpty(File folder) {
1270         if (!folder.isDirectory()) {
1271             return true;
1272         }
1273 
1274         String[] fileNames = folder.list();
1275 
1276         if ((fileNames == null) || (fileNames.length == 0)) {
1277             return true;
1278         }
1279 
1280         if ((fileNames.length == 1) && (fileNames[0].equals(SHADOW_FOLDER))) {
1281             return true;
1282         }
1283 
1284         return false;
1285     }
1286 
1287     /**
1288      * Compiles an object type object from a file or folder.�
1289      */
1290     private ObjectData compileObjectType(CallContext context, File file, Set<String> filter,
1291             boolean includeAllowableActions, boolean includeAcl, boolean userReadOnly, ObjectInfoHandler objectInfos) {
1292         ObjectDataImpl result = new ObjectDataImpl();
1293         ObjectInfoImpl objectInfo = new ObjectInfoImpl();
1294 
1295         result.setProperties(compileProperties(file, filter, objectInfo));
1296 
1297         if (includeAllowableActions) {
1298             result.setAllowableActions(compileAllowableActions(file, userReadOnly));
1299         }
1300 
1301         if (includeAcl) {
1302             result.setAcl(compileAcl(file));
1303             result.setIsExactAcl(true);
1304         }
1305 
1306         if (context.isObjectInfoRequired()) {
1307             objectInfo.setObject(result);
1308             objectInfos.addObjectInfo(objectInfo);
1309         }
1310 
1311         return result;
1312     }
1313 
1314     /**
1315      * Gathers all base properties of a file or folder.
1316      */
1317     private Properties compileProperties(File file, Set<String> orgfilter, ObjectInfoImpl objectInfo) {
1318         if (file == null) {
1319             throw new IllegalArgumentException("File must not be null!");
1320         }
1321 
1322         // we can gather properties if the file or folder doesn't exist
1323         if (!file.exists()) {
1324             throw new CmisObjectNotFoundException("Object not found!");
1325         }
1326 
1327         // copy filter
1328         Set<String> filter = (orgfilter == null ? null : new HashSet<String>(orgfilter));
1329 
1330         // find base type
1331         String typeId = null;
1332 
1333         if (file.isDirectory()) {
1334             typeId = TypeManager.FOLDER_TYPE_ID;
1335             objectInfo.setBaseType(BaseTypeId.CMIS_FOLDER);
1336             objectInfo.setTypeId(typeId);
1337             objectInfo.setContentType(null);
1338             objectInfo.setFileName(null);
1339             objectInfo.setHasAcl(true);
1340             objectInfo.setHasContent(false);
1341             objectInfo.setVersionSeriesId(null);
1342             objectInfo.setIsCurrentVersion(true);
1343             objectInfo.setRelationshipSourceIds(null);
1344             objectInfo.setRelationshipTargetIds(null);
1345             objectInfo.setRenditionInfos(null);
1346             objectInfo.setSupportsDescendants(true);
1347             objectInfo.setSupportsFolderTree(true);
1348             objectInfo.setSupportsPolicies(false);
1349             objectInfo.setSupportsRelationships(false);
1350             objectInfo.setWorkingCopyId(null);
1351             objectInfo.setWorkingCopyOriginalId(null);
1352         } else {
1353             typeId = TypeManager.DOCUMENT_TYPE_ID;
1354             objectInfo.setBaseType(BaseTypeId.CMIS_DOCUMENT);
1355             objectInfo.setTypeId(typeId);
1356             objectInfo.setHasAcl(true);
1357             objectInfo.setHasContent(true);
1358             objectInfo.setHasParent(true);
1359             objectInfo.setVersionSeriesId(null);
1360             objectInfo.setIsCurrentVersion(true);
1361             objectInfo.setRelationshipSourceIds(null);
1362             objectInfo.setRelationshipTargetIds(null);
1363             objectInfo.setRenditionInfos(null);
1364             objectInfo.setSupportsDescendants(false);
1365             objectInfo.setSupportsFolderTree(false);
1366             objectInfo.setSupportsPolicies(false);
1367             objectInfo.setSupportsRelationships(false);
1368             objectInfo.setWorkingCopyId(null);
1369             objectInfo.setWorkingCopyOriginalId(null);
1370         }
1371 
1372         // let's do it
1373         try {
1374             PropertiesImpl result = new PropertiesImpl();
1375 
1376             // id
1377             String id = fileToId(file);
1378             addPropertyId(result, typeId, filter, PropertyIds.OBJECT_ID, id);
1379             objectInfo.setId(id);
1380 
1381             // name
1382             String name = file.getName();
1383             addPropertyString(result, typeId, filter, PropertyIds.NAME, name);
1384             objectInfo.setName(name);
1385 
1386             // created and modified by
1387             addPropertyString(result, typeId, filter, PropertyIds.CREATED_BY, USER_UNKNOWN);
1388             addPropertyString(result, typeId, filter, PropertyIds.LAST_MODIFIED_BY, USER_UNKNOWN);
1389             objectInfo.setCreatedBy(USER_UNKNOWN);
1390 
1391             // creation and modification date
1392             GregorianCalendar lastModified = millisToCalendar(file.lastModified());
1393             addPropertyDateTime(result, typeId, filter, PropertyIds.CREATION_DATE, lastModified);
1394             addPropertyDateTime(result, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified);
1395             objectInfo.setCreationDate(lastModified);
1396             objectInfo.setLastModificationDate(lastModified);
1397 
1398             // change token - always null
1399             addPropertyString(result, typeId, filter, PropertyIds.CHANGE_TOKEN, null);
1400 
1401             // directory or file
1402             if (file.isDirectory()) {
1403                 // base type and type name
1404                 addPropertyId(result, typeId, filter, PropertyIds.BASE_TYPE_ID, BaseTypeId.CMIS_FOLDER.value());
1405                 addPropertyId(result, typeId, filter, PropertyIds.OBJECT_TYPE_ID, TypeManager.FOLDER_TYPE_ID);
1406                 String path = getRepositoryPath(file);
1407                 addPropertyString(result, typeId, filter, PropertyIds.PATH, (path.length() == 0 ? "/" : path));
1408 
1409                 // folder properties
1410                 if (!root.equals(file)) {
1411                     addPropertyId(result, typeId, filter, PropertyIds.PARENT_ID,
1412                             (root.equals(file.getParentFile()) ? ROOT_ID : fileToId(file.getParentFile())));
1413                     objectInfo.setHasParent(true);
1414                 } else {
1415                     addPropertyId(result, typeId, filter, PropertyIds.PARENT_ID, null);
1416                     objectInfo.setHasParent(false);
1417                 }
1418 
1419                 addPropertyIdList(result, typeId, filter, PropertyIds.ALLOWED_CHILD_OBJECT_TYPE_IDS, null);
1420             } else {
1421                 // base type and type name
1422                 addPropertyId(result, typeId, filter, PropertyIds.BASE_TYPE_ID, BaseTypeId.CMIS_DOCUMENT.value());
1423                 addPropertyId(result, typeId, filter, PropertyIds.OBJECT_TYPE_ID, TypeManager.DOCUMENT_TYPE_ID);
1424 
1425                 // file properties
1426                 addPropertyBoolean(result, typeId, filter, PropertyIds.IS_IMMUTABLE, false);
1427                 addPropertyBoolean(result, typeId, filter, PropertyIds.IS_LATEST_VERSION, true);
1428                 addPropertyBoolean(result, typeId, filter, PropertyIds.IS_MAJOR_VERSION, true);
1429                 addPropertyBoolean(result, typeId, filter, PropertyIds.IS_LATEST_MAJOR_VERSION, true);
1430                 addPropertyString(result, typeId, filter, PropertyIds.VERSION_LABEL, file.getName());
1431                 addPropertyId(result, typeId, filter, PropertyIds.VERSION_SERIES_ID, fileToId(file));
1432                 addPropertyBoolean(result, typeId, filter, PropertyIds.IS_VERSION_SERIES_CHECKED_OUT, false);
1433                 addPropertyString(result, typeId, filter, PropertyIds.VERSION_SERIES_CHECKED_OUT_BY, null);
1434                 addPropertyString(result, typeId, filter, PropertyIds.VERSION_SERIES_CHECKED_OUT_ID, null);
1435                 addPropertyString(result, typeId, filter, PropertyIds.CHECKIN_COMMENT, "");
1436 
1437                 if (file.length() == 0) {
1438                     addPropertyBigInteger(result, typeId, filter, PropertyIds.CONTENT_STREAM_LENGTH, null);
1439                     addPropertyString(result, typeId, filter, PropertyIds.CONTENT_STREAM_MIME_TYPE, null);
1440                     addPropertyString(result, typeId, filter, PropertyIds.CONTENT_STREAM_FILE_NAME, null);
1441 
1442                     objectInfo.setHasContent(false);
1443                     objectInfo.setContentType(null);
1444                     objectInfo.setFileName(null);
1445                 } else {
1446                     addPropertyInteger(result, typeId, filter, PropertyIds.CONTENT_STREAM_LENGTH, file.length());
1447                     addPropertyString(result, typeId, filter, PropertyIds.CONTENT_STREAM_MIME_TYPE,
1448                             MimeTypes.getMIMEType(file));
1449                     addPropertyString(result, typeId, filter, PropertyIds.CONTENT_STREAM_FILE_NAME, file.getName());
1450 
1451                     objectInfo.setHasContent(true);
1452                     objectInfo.setContentType(MimeTypes.getMIMEType(file));
1453                     objectInfo.setFileName(file.getName());
1454                 }
1455 
1456                 addPropertyId(result, typeId, filter, PropertyIds.CONTENT_STREAM_ID, null);
1457             }
1458 
1459             // read custom properties
1460             readCustomProperties(file, result, filter, objectInfo);
1461 
1462             if (filter != null) {
1463                 if (!filter.isEmpty()) {
1464                     debug("Unknown filter properties: " + filter.toString(), null);
1465                 }
1466             }
1467 
1468             return result;
1469         } catch (Exception e) {
1470             if (e instanceof CmisBaseException) {
1471                 throw (CmisBaseException) e;
1472             }
1473             throw new CmisRuntimeException(e.getMessage(), e);
1474         }
1475     }
1476 
1477     /**
1478      * Reads and adds properties.
1479      */
1480     @SuppressWarnings("unchecked")
1481     private void readCustomProperties(File file, PropertiesImpl properties, Set<String> filter,
1482             ObjectInfoImpl objectInfo) {
1483         File propFile = getPropertiesFile(file);
1484 
1485         // if it doesn't exists, ignore it
1486         if (!propFile.exists()) {
1487             return;
1488         }
1489 
1490         // parse it
1491         JAXBElement<CmisObjectType> obj = null;
1492         try {
1493             Unmarshaller u = JaxBHelper.createUnmarshaller();
1494             obj = (JAXBElement<CmisObjectType>) u.unmarshal(propFile);
1495         } catch (Exception e) {
1496             warn("Unvalid CMIS properties: " + propFile.getAbsolutePath(), e);
1497         }
1498 
1499         if ((obj == null) || (obj.getValue() == null) || (obj.getValue().getProperties() == null)) {
1500             return;
1501         }
1502 
1503         // add it to properties
1504         for (CmisProperty cmisProp : obj.getValue().getProperties().getProperty()) {
1505             PropertyData<?> prop = Converter.convert(cmisProp);
1506 
1507             // overwrite object info
1508             if (prop instanceof PropertyString) {
1509                 String firstValueStr = ((PropertyString) prop).getFirstValue();
1510                 if (PropertyIds.NAME.equals(prop.getId())) {
1511                     objectInfo.setName(firstValueStr);
1512                 } else if (PropertyIds.OBJECT_TYPE_ID.equals(prop.getId())) {
1513                     objectInfo.setTypeId(firstValueStr);
1514                 } else if (PropertyIds.CREATED_BY.equals(prop.getId())) {
1515                     objectInfo.setCreatedBy(firstValueStr);
1516                 } else if (PropertyIds.CONTENT_STREAM_MIME_TYPE.equals(prop.getId())) {
1517                     objectInfo.setContentType(firstValueStr);
1518                 } else if (PropertyIds.CONTENT_STREAM_FILE_NAME.equals(prop.getId())) {
1519                     objectInfo.setFileName(firstValueStr);
1520                 }
1521             }
1522 
1523             if (prop instanceof PropertyDateTime) {
1524                 GregorianCalendar firstValueCal = ((PropertyDateTime) prop).getFirstValue();
1525                 if (PropertyIds.CREATION_DATE.equals(prop.getId())) {
1526                     objectInfo.setCreationDate(firstValueCal);
1527                 } else if (PropertyIds.LAST_MODIFICATION_DATE.equals(prop.getId())) {
1528                     objectInfo.setLastModificationDate(firstValueCal);
1529                 }
1530             }
1531 
1532             // check filter
1533             if (filter != null) {
1534                 if (!filter.contains(prop.getId())) {
1535                     continue;
1536                 } else {
1537                     filter.remove(prop.getId());
1538                 }
1539             }
1540 
1541             // don't overwrite id
1542             if (PropertyIds.OBJECT_ID.equals(prop.getId())) {
1543                 continue;
1544             }
1545 
1546             // don't overwrite base type
1547             if (PropertyIds.BASE_TYPE_ID.equals(prop.getId())) {
1548                 continue;
1549             }
1550 
1551             // add it
1552             properties.addProperty(prop);
1553         }
1554     }
1555 
1556     /**
1557      * Checks and compiles a property set that can be written to disc.
1558      */
1559     private Properties compileProperties(String typeId, String creator, GregorianCalendar creationDate,
1560             String modifier, Properties properties) {
1561         PropertiesImpl result = new PropertiesImpl();
1562         Set<String> addedProps = new HashSet<String>();
1563 
1564         if ((properties == null) || (properties.getProperties() == null)) {
1565             throw new CmisConstraintException("No properties!");
1566         }
1567 
1568         // get the property definitions
1569         TypeDefinition type = types.getType(typeId);
1570         if (type == null) {
1571             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
1572         }
1573 
1574         // check if all required properties are there
1575         for (PropertyData<?> prop : properties.getProperties().values()) {
1576             PropertyDefinition<?> propType = type.getPropertyDefinitions().get(prop.getId());
1577 
1578             // do we know that property?
1579             if (propType == null) {
1580                 throw new CmisConstraintException("Property '" + prop.getId() + "' is unknown!");
1581             }
1582 
1583             // can it be set?
1584             if ((propType.getUpdatability() == Updatability.READONLY)) {
1585                 throw new CmisConstraintException("Property '" + prop.getId() + "' is readonly!");
1586             }
1587 
1588             // empty properties are invalid
1589             if (isEmptyProperty(prop)) {
1590                 throw new CmisConstraintException("Property '" + prop.getId() + "' must not be empty!");
1591             }
1592 
1593             // add it
1594             result.addProperty(prop);
1595             addedProps.add(prop.getId());
1596         }
1597 
1598         // check if required properties are missing
1599         for (PropertyDefinition<?> propDef : type.getPropertyDefinitions().values()) {
1600             if (!addedProps.contains(propDef.getId()) && (propDef.getUpdatability() != Updatability.READONLY)) {
1601                 if (!addPropertyDefault(result, propDef) && propDef.isRequired()) {
1602                     throw new CmisConstraintException("Property '" + propDef.getId() + "' is required!");
1603                 }
1604             }
1605         }
1606 
1607         addPropertyId(result, typeId, null, PropertyIds.OBJECT_TYPE_ID, typeId);
1608         addPropertyString(result, typeId, null, PropertyIds.CREATED_BY, creator);
1609         addPropertyDateTime(result, typeId, null, PropertyIds.CREATION_DATE, creationDate);
1610         addPropertyString(result, typeId, null, PropertyIds.LAST_MODIFIED_BY, modifier);
1611 
1612         return result;
1613     }
1614 
1615     /**
1616      * Checks and updates a property set that can be written to disc.
1617      */
1618     private Properties updateProperties(String typeId, String creator, GregorianCalendar creationDate, String modifier,
1619             Properties oldProperties, Properties properties) {
1620         PropertiesImpl result = new PropertiesImpl();
1621 
1622         if (properties == null) {
1623             throw new CmisConstraintException("No properties!");
1624         }
1625 
1626         // get the property definitions
1627         TypeDefinition type = types.getType(typeId);
1628         if (type == null) {
1629             throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
1630         }
1631 
1632         // copy old properties
1633         for (PropertyData<?> prop : oldProperties.getProperties().values()) {
1634             PropertyDefinition<?> propType = type.getPropertyDefinitions().get(prop.getId());
1635 
1636             // do we know that property?
1637             if (propType == null) {
1638                 throw new CmisConstraintException("Property '" + prop.getId() + "' is unknown!");
1639             }
1640 
1641             // only add read/write properties
1642             if ((propType.getUpdatability() != Updatability.READWRITE)) {
1643                 continue;
1644             }
1645 
1646             result.addProperty(prop);
1647         }
1648 
1649         // update properties
1650         for (PropertyData<?> prop : properties.getProperties().values()) {
1651             PropertyDefinition<?> propType = type.getPropertyDefinitions().get(prop.getId());
1652 
1653             // do we know that property?
1654             if (propType == null) {
1655                 throw new CmisConstraintException("Property '" + prop.getId() + "' is unknown!");
1656             }
1657 
1658             // can it be set?
1659             if ((propType.getUpdatability() == Updatability.READONLY)) {
1660                 throw new CmisConstraintException("Property '" + prop.getId() + "' is readonly!");
1661             }
1662 
1663             if ((propType.getUpdatability() == Updatability.ONCREATE)) {
1664                 throw new CmisConstraintException("Property '" + prop.getId() + "' can only be set on create!");
1665             }
1666 
1667             // default or value
1668             if (isEmptyProperty(prop)) {
1669                 addPropertyDefault(result, propType);
1670             } else {
1671                 result.addProperty(prop);
1672             }
1673         }
1674 
1675         addPropertyId(result, typeId, null, PropertyIds.OBJECT_TYPE_ID, typeId);
1676         addPropertyString(result, typeId, null, PropertyIds.CREATED_BY, creator);
1677         addPropertyDateTime(result, typeId, null, PropertyIds.CREATION_DATE, creationDate);
1678         addPropertyString(result, typeId, null, PropertyIds.LAST_MODIFIED_BY, modifier);
1679 
1680         return result;
1681     }
1682 
1683     private static boolean isEmptyProperty(PropertyData<?> prop) {
1684         if ((prop == null) || (prop.getValues() == null)) {
1685             return true;
1686         }
1687 
1688         return prop.getValues().isEmpty();
1689     }
1690 
1691     private void addPropertyId(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
1692         if (!checkAddProperty(props, typeId, filter, id)) {
1693             return;
1694         }
1695 
1696         props.addProperty(new PropertyIdImpl(id, value));
1697     }
1698 
1699     private void addPropertyIdList(PropertiesImpl props, String typeId, Set<String> filter, String id,
1700             List<String> value) {
1701         if (!checkAddProperty(props, typeId, filter, id)) {
1702             return;
1703         }
1704 
1705         props.addProperty(new PropertyIdImpl(id, value));
1706     }
1707 
1708     private void addPropertyString(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
1709         if (!checkAddProperty(props, typeId, filter, id)) {
1710             return;
1711         }
1712 
1713         props.addProperty(new PropertyStringImpl(id, value));
1714     }
1715 
1716     private void addPropertyInteger(PropertiesImpl props, String typeId, Set<String> filter, String id, long value) {
1717         addPropertyBigInteger(props, typeId, filter, id, BigInteger.valueOf(value));
1718     }
1719 
1720     private void addPropertyBigInteger(PropertiesImpl props, String typeId, Set<String> filter, String id,
1721             BigInteger value) {
1722         if (!checkAddProperty(props, typeId, filter, id)) {
1723             return;
1724         }
1725 
1726         props.addProperty(new PropertyIntegerImpl(id, value));
1727     }
1728 
1729     private void addPropertyBoolean(PropertiesImpl props, String typeId, Set<String> filter, String id, boolean value) {
1730         if (!checkAddProperty(props, typeId, filter, id)) {
1731             return;
1732         }
1733 
1734         props.addProperty(new PropertyBooleanImpl(id, value));
1735     }
1736 
1737     private void addPropertyDateTime(PropertiesImpl props, String typeId, Set<String> filter, String id,
1738             GregorianCalendar value) {
1739         if (!checkAddProperty(props, typeId, filter, id)) {
1740             return;
1741         }
1742 
1743         props.addProperty(new PropertyDateTimeImpl(id, value));
1744     }
1745 
1746     private boolean checkAddProperty(Properties properties, String typeId, Set<String> filter, String id) {
1747         if ((properties == null) || (properties.getProperties() == null)) {
1748             throw new IllegalArgumentException("Properties must not be null!");
1749         }
1750 
1751         if (id == null) {
1752             throw new IllegalArgumentException("Id must not be null!");
1753         }
1754 
1755         TypeDefinition type = types.getType(typeId);
1756         if (type == null) {
1757             throw new IllegalArgumentException("Unknown type: " + typeId);
1758         }
1759         if (!type.getPropertyDefinitions().containsKey(id)) {
1760             throw new IllegalArgumentException("Unknown property: " + id);
1761         }
1762 
1763         String queryName = type.getPropertyDefinitions().get(id).getQueryName();
1764 
1765         if ((queryName != null) && (filter != null)) {
1766             if (!filter.contains(queryName)) {
1767                 return false;
1768             } else {
1769                 filter.remove(queryName);
1770             }
1771         }
1772 
1773         return true;
1774     }
1775 
1776     /**
1777      * Adds the default value of property if defined.
1778      */
1779     @SuppressWarnings("unchecked")
1780     private static boolean addPropertyDefault(PropertiesImpl props, PropertyDefinition<?> propDef) {
1781         if ((props == null) || (props.getProperties() == null)) {
1782             throw new IllegalArgumentException("Props must not be null!");
1783         }
1784 
1785         if (propDef == null) {
1786             return false;
1787         }
1788 
1789         List<?> defaultValue = propDef.getDefaultValue();
1790         if ((defaultValue != null) && (!defaultValue.isEmpty())) {
1791             switch (propDef.getPropertyType()) {
1792             case BOOLEAN:
1793                 props.addProperty(new PropertyBooleanImpl(propDef.getId(), (List<Boolean>) defaultValue));
1794                 break;
1795             case DATETIME:
1796                 props.addProperty(new PropertyDateTimeImpl(propDef.getId(), (List<GregorianCalendar>) defaultValue));
1797                 break;
1798             case DECIMAL:
1799                 props.addProperty(new PropertyDecimalImpl(propDef.getId(), (List<BigDecimal>) defaultValue));
1800                 break;
1801             case HTML:
1802                 props.addProperty(new PropertyHtmlImpl(propDef.getId(), (List<String>) defaultValue));
1803                 break;
1804             case ID:
1805                 props.addProperty(new PropertyIdImpl(propDef.getId(), (List<String>) defaultValue));
1806                 break;
1807             case INTEGER:
1808                 props.addProperty(new PropertyIntegerImpl(propDef.getId(), (List<BigInteger>) defaultValue));
1809                 break;
1810             case STRING:
1811                 props.addProperty(new PropertyStringImpl(propDef.getId(), (List<String>) defaultValue));
1812                 break;
1813             case URI:
1814                 props.addProperty(new PropertyUriImpl(propDef.getId(), (List<String>) defaultValue));
1815                 break;
1816             default:
1817                 throw new RuntimeException("Unknown datatype! Spec change?");
1818             }
1819 
1820             return true;
1821         }
1822 
1823         return false;
1824     }
1825 
1826     /**
1827      * Compiles the allowable actions for a file or folder.
1828      */
1829     private AllowableActions compileAllowableActions(File file, boolean userReadOnly) {
1830         if (file == null) {
1831             throw new IllegalArgumentException("File must not be null!");
1832         }
1833 
1834         // we can gather properties if the file or folder doesn't exist
1835         if (!file.exists()) {
1836             throw new CmisObjectNotFoundException("Object not found!");
1837         }
1838 
1839         boolean isReadOnly = !file.canWrite();
1840         boolean isFolder = file.isDirectory();
1841         boolean isRoot = root.equals(file);
1842 
1843         Set<Action> aas = new HashSet<Action>();
1844 
1845         addAction(aas, Action.CAN_GET_OBJECT_PARENTS, !isRoot);
1846         addAction(aas, Action.CAN_GET_PROPERTIES, true);
1847         addAction(aas, Action.CAN_UPDATE_PROPERTIES, !userReadOnly && !isReadOnly);
1848         addAction(aas, Action.CAN_MOVE_OBJECT, !userReadOnly);
1849         addAction(aas, Action.CAN_DELETE_OBJECT, !userReadOnly && !isReadOnly && !isRoot);
1850         addAction(aas, Action.CAN_GET_ACL, true);
1851 
1852         if (isFolder) {
1853             addAction(aas, Action.CAN_GET_DESCENDANTS, true);
1854             addAction(aas, Action.CAN_GET_CHILDREN, true);
1855             addAction(aas, Action.CAN_GET_FOLDER_PARENT, !isRoot);
1856             addAction(aas, Action.CAN_GET_FOLDER_TREE, true);
1857             addAction(aas, Action.CAN_CREATE_DOCUMENT, !userReadOnly);
1858             addAction(aas, Action.CAN_CREATE_FOLDER, !userReadOnly);
1859             addAction(aas, Action.CAN_DELETE_TREE, !userReadOnly && !isReadOnly);
1860         } else {
1861             addAction(aas, Action.CAN_GET_CONTENT_STREAM, true);
1862             addAction(aas, Action.CAN_SET_CONTENT_STREAM, !userReadOnly && !isReadOnly);
1863             addAction(aas, Action.CAN_DELETE_CONTENT_STREAM, !userReadOnly && !isReadOnly);
1864             addAction(aas, Action.CAN_GET_ALL_VERSIONS, true);
1865         }
1866 
1867         AllowableActionsImpl result = new AllowableActionsImpl();
1868         result.setAllowableActions(aas);
1869 
1870         return result;
1871     }
1872 
1873     private static void addAction(Set<Action> aas, Action action, boolean condition) {
1874         if (condition) {
1875             aas.add(action);
1876         }
1877     }
1878 
1879     /**
1880      * Compiles the ACL for a file or folder.
1881      */
1882     private Acl compileAcl(File file) {
1883         AccessControlListImpl result = new AccessControlListImpl();
1884         result.setAces(new ArrayList<Ace>());
1885 
1886         for (Map.Entry<String, Boolean> ue : userMap.entrySet()) {
1887             // create principal
1888             AccessControlPrincipalDataImpl principal = new AccessControlPrincipalDataImpl();
1889             principal.setPrincipalId(ue.getKey());
1890 
1891             // create ACE
1892             AccessControlEntryImpl entry = new AccessControlEntryImpl();
1893             entry.setPrincipal(principal);
1894             entry.setPermissions(new ArrayList<String>());
1895             entry.getPermissions().add(CMIS_READ);
1896             if (!ue.getValue().booleanValue() && file.canWrite()) {
1897                 entry.getPermissions().add(CMIS_WRITE);
1898                 entry.getPermissions().add(CMIS_ALL);
1899             }
1900 
1901             entry.setDirect(true);
1902 
1903             // add ACE
1904             result.getAces().add(entry);
1905         }
1906 
1907         return result;
1908     }
1909 
1910     /**
1911      * Writes the properties for a document or folder.
1912      */
1913     private static void writePropertiesFile(File file, Properties properties) {
1914         File propFile = getPropertiesFile(file);
1915 
1916         // if no properties set delete the properties file
1917         if ((properties == null) || (properties.getProperties() == null) || (properties.getProperties().size() == 0)) {
1918             propFile.delete();
1919             return;
1920         }
1921 
1922         // create object
1923         CmisObjectType object = new CmisObjectType();
1924         object.setProperties(Converter.convert(properties));
1925 
1926         // write it
1927         try {
1928             JaxBHelper.CMIS_EXTRA_OBJECT_FACTORY.createObject(object);
1929             JAXBElement<CmisObjectType> objElement = JaxBHelper.CMIS_EXTRA_OBJECT_FACTORY.createObject(object);
1930 
1931             Marshaller m = JaxBHelper.createMarshaller();
1932             m.setProperty("jaxb.formatted.output", true);
1933             m.marshal(objElement, propFile);
1934         } catch (Exception e) {
1935             throw new CmisStorageException("Couldn't store properties!", e);
1936         }
1937     }
1938 
1939     // --- internal stuff ---
1940 
1941     /**
1942      * Converts milliseconds into a calendar object.
1943      */
1944     private static GregorianCalendar millisToCalendar(long millis) {
1945         GregorianCalendar result = new GregorianCalendar();
1946         result.setTimeZone(TimeZone.getTimeZone("GMT"));
1947         result.setTimeInMillis((long) (Math.ceil(millis / 1000) * 1000));
1948 
1949         return result;
1950     }
1951 
1952     /**
1953      * Splits a filter statement into a collection of properties. If
1954      * <code>filter</code> is <code>null</code>, empty or one of the properties
1955      * is '*' , an empty collection will be returned.
1956      */
1957     private static Set<String> splitFilter(String filter) {
1958         if (filter == null) {
1959             return null;
1960         }
1961 
1962         if (filter.trim().length() == 0) {
1963             return null;
1964         }
1965 
1966         Set<String> result = new HashSet<String>();
1967         for (String s : filter.split(",")) {
1968             s = s.trim();
1969             if (s.equals("*")) {
1970                 return null;
1971             } else if (s.length() > 0) {
1972                 result.add(s);
1973             }
1974         }
1975 
1976         // set a few base properties
1977         // query name == id (for base type properties)
1978         result.add(PropertyIds.OBJECT_ID);
1979         result.add(PropertyIds.OBJECT_TYPE_ID);
1980         result.add(PropertyIds.BASE_TYPE_ID);
1981 
1982         return result;
1983     }
1984 
1985     /**
1986      * Gets the type id from a set of properties.
1987      */
1988     private static String getTypeId(Properties properties) {
1989         PropertyData<?> typeProperty = properties.getProperties().get(PropertyIds.OBJECT_TYPE_ID);
1990         if (!(typeProperty instanceof PropertyId)) {
1991             throw new CmisInvalidArgumentException("Type id must be set!");
1992         }
1993 
1994         String typeId = ((PropertyId) typeProperty).getFirstValue();
1995         if (typeId == null) {
1996             throw new CmisInvalidArgumentException("Type id must be set!");
1997         }
1998 
1999         return typeId;
2000     }
2001 
2002     /**
2003      * Returns the first value of an id property.
2004      */
2005     private static String getIdProperty(Properties properties, String name) {
2006         PropertyData<?> property = properties.getProperties().get(name);
2007         if (!(property instanceof PropertyId)) {
2008             return null;
2009         }
2010 
2011         return ((PropertyId) property).getFirstValue();
2012     }
2013 
2014     /**
2015      * Returns the first value of an string property.
2016      */
2017     private static String getStringProperty(Properties properties, String name) {
2018         PropertyData<?> property = properties.getProperties().get(name);
2019         if (!(property instanceof PropertyString)) {
2020             return null;
2021         }
2022 
2023         return ((PropertyString) property).getFirstValue();
2024     }
2025 
2026     /**
2027      * Returns the first value of an datetime property.
2028      */
2029     private static GregorianCalendar getDateTimeProperty(Properties properties, String name) {
2030         PropertyData<?> property = properties.getProperties().get(name);
2031         if (!(property instanceof PropertyDateTime)) {
2032             return null;
2033         }
2034 
2035         return ((PropertyDateTime) property).getFirstValue();
2036     }
2037 
2038     /**
2039      * Checks if the user in the given context is valid for this repository and
2040      * if the user has the required permissions.
2041      */
2042     private boolean checkUser(CallContext context, boolean writeRequired) {
2043         if (context == null) {
2044             throw new CmisPermissionDeniedException("No user context!");
2045         }
2046 
2047         Boolean readOnly = userMap.get(context.getUsername());
2048         if (readOnly == null) {
2049             throw new CmisPermissionDeniedException("Unknown user!");
2050         }
2051 
2052         if (readOnly.booleanValue() && writeRequired) {
2053             throw new CmisPermissionDeniedException("No write permission!");
2054         }
2055 
2056         return readOnly.booleanValue();
2057     }
2058 
2059     /**
2060      * Returns the properties file of the given file.
2061      */
2062     private static File getPropertiesFile(File file) {
2063         if (file.isDirectory()) {
2064             return new File(file, SHADOW_FOLDER);
2065         }
2066 
2067         return new File(file.getAbsolutePath() + SHADOW_EXT);
2068     }
2069 
2070     /**
2071      * Returns the File object by id or throws an appropriate exception.
2072      */
2073     private File getFile(String id) {
2074         try {
2075             return idToFile(id);
2076         } catch (Exception e) {
2077             throw new CmisObjectNotFoundException(e.getMessage(), e);
2078         }
2079     }
2080 
2081     /**
2082      * Converts an id to a File object. A simple and insecure implementation,
2083      * but good enough for now.
2084      */
2085     private File idToFile(String id) throws Exception {
2086         if ((id == null) || (id.length() == 0)) {
2087             throw new CmisInvalidArgumentException("Id is not valid!");
2088         }
2089 
2090         if (id.equals(ROOT_ID)) {
2091             return root;
2092         }
2093 
2094         return new File(root, (new String(Base64.decodeBase64(id.getBytes("ISO-8859-1")), "UTF-8")).replace('/',
2095                 File.separatorChar));
2096     }
2097 
2098     /**
2099      * Returns the id of a File object or throws an appropriate exception.
2100      */
2101     private String getId(File file) {
2102         try {
2103             return fileToId(file);
2104         } catch (Exception e) {
2105             throw new CmisRuntimeException(e.getMessage(), e);
2106         }
2107     }
2108 
2109     /**
2110      * Creates a File object from an id. A simple and insecure implementation,
2111      * but good enough for now.
2112      */
2113     private String fileToId(File file) throws Exception {
2114         if (file == null) {
2115             throw new IllegalArgumentException("File is not valid!");
2116         }
2117 
2118         if (root.equals(file)) {
2119             return ROOT_ID;
2120         }
2121 
2122         String path = getRepositoryPath(file);
2123 
2124         return new String(Base64.encodeBase64(path.getBytes("UTF-8")), "ISO-8859-1");
2125     }
2126 
2127     private String getRepositoryPath(File file) {
2128         return file.getAbsolutePath().substring(root.getAbsolutePath().length()).replace(File.separatorChar, '/');
2129     }
2130 
2131     private void warn(String msg, Throwable t) {
2132         log.warn("<" + repositoryId + "> " + msg, t);
2133     }
2134 
2135     private void debug(String msg) {
2136         debug(msg, null);
2137     }
2138 
2139     private void debug(String msg, Throwable t) {
2140         log.debug("<" + repositoryId + "> " + msg, t);
2141     }
2142 }