This project has retired. For details please refer to its Attic page.
JcrNode 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  
20  package org.apache.chemistry.opencmis.jcr;
21  
22  import org.apache.chemistry.opencmis.commons.PropertyIds;
23  import org.apache.chemistry.opencmis.commons.data.AllowableActions;
24  import org.apache.chemistry.opencmis.commons.data.ObjectData;
25  import org.apache.chemistry.opencmis.commons.data.Properties;
26  import org.apache.chemistry.opencmis.commons.data.PropertyData;
27  import org.apache.chemistry.opencmis.commons.definitions.DocumentTypeDefinition;
28  import org.apache.chemistry.opencmis.commons.definitions.PropertyDefinition;
29  import org.apache.chemistry.opencmis.commons.definitions.TypeDefinition;
30  import org.apache.chemistry.opencmis.commons.enums.Action;
31  import org.apache.chemistry.opencmis.commons.enums.BaseTypeId;
32  import org.apache.chemistry.opencmis.commons.enums.Updatability;
33  import org.apache.chemistry.opencmis.commons.exceptions.CmisConstraintException;
34  import org.apache.chemistry.opencmis.commons.exceptions.CmisInvalidArgumentException;
35  import org.apache.chemistry.opencmis.commons.exceptions.CmisNameConstraintViolationException;
36  import org.apache.chemistry.opencmis.commons.exceptions.CmisObjectNotFoundException;
37  import org.apache.chemistry.opencmis.commons.exceptions.CmisRuntimeException;
38  import org.apache.chemistry.opencmis.commons.exceptions.CmisStorageException;
39  import org.apache.chemistry.opencmis.commons.exceptions.CmisUpdateConflictException;
40  import org.apache.chemistry.opencmis.commons.impl.dataobjects.AllowableActionsImpl;
41  import org.apache.chemistry.opencmis.commons.impl.dataobjects.ObjectDataImpl;
42  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertiesImpl;
43  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyBooleanImpl;
44  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyDateTimeImpl;
45  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIdImpl;
46  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyIntegerImpl;
47  import org.apache.chemistry.opencmis.commons.impl.dataobjects.PropertyStringImpl;
48  import org.apache.chemistry.opencmis.commons.impl.server.ObjectInfoImpl;
49  import org.apache.chemistry.opencmis.commons.server.ObjectInfoHandler;
50  import org.apache.chemistry.opencmis.jcr.type.JcrTypeHandlerManager;
51  import org.apache.chemistry.opencmis.jcr.util.Util;
52  import org.apache.commons.logging.Log;
53  import org.apache.commons.logging.LogFactory;
54  
55  import javax.jcr.ItemNotFoundException;
56  import javax.jcr.Node;
57  import javax.jcr.PathNotFoundException;
58  import javax.jcr.Property;
59  import javax.jcr.RepositoryException;
60  import javax.jcr.Session;
61  import javax.jcr.version.Version;
62  import javax.jcr.version.VersionHistory;
63  import javax.jcr.version.VersionManager;
64  import java.math.BigInteger;
65  import java.util.ArrayList;
66  import java.util.Calendar;
67  import java.util.GregorianCalendar;
68  import java.util.HashSet;
69  import java.util.List;
70  import java.util.Set;
71  
72  /**
73   * Common base class for all JCR <code>Node</code>s to be represented as CMIS objects. Instances of this class
74   * are responsible for mapping from CMIS to JCR and vice versa.
75   */
76  public abstract class JcrNode {
77  
78      private static final Log log = LogFactory.getLog(JcrNode.class);
79  
80      /**
81       * Default value for last cmis:createdBy and cmis:modifiedBy
82       */
83      public static final String USER_UNKNOWN = "unknown";
84  
85      /**
86       * Default value for cmis:createdBy and cmis:lastModifiedDate
87       * (Thu Jan 01 01:00:00 CET 1970)
88       */
89      public static final GregorianCalendar DATE_UNKNOWN;
90  
91      static {
92          DATE_UNKNOWN = new GregorianCalendar();
93          DATE_UNKNOWN.setTimeInMillis(0);
94      }
95  
96      private final Node node;
97      protected final JcrTypeManager typeManager;
98      protected final PathManager pathManager;
99      protected final JcrTypeHandlerManager typeHandlerManager;
100 
101     /**
102      * Create a new instance wrapping a JCR <code>node</code>.
103      *
104      * @param node  the JCR <code>node</code> to represent
105      * @param typeManager
106      * @param pathManager
107      * @param typeHandlerManager
108      */
109     protected JcrNode(Node node, JcrTypeManager typeManager, PathManager pathManager, JcrTypeHandlerManager typeHandlerManager) {
110         this.node = node;
111         this.typeManager = typeManager;
112         this.pathManager = pathManager;
113         this.typeHandlerManager = typeHandlerManager;
114     }
115 
116     /**
117      * @return  the JCR <code>node</code> represented by this instance
118      */
119     public Node getNode() {
120         return node;
121     }
122 
123     /**
124      * @return  the name of the CMIS object represented by this instance
125      * @throws  CmisRuntimeException
126      */
127     public String getName() {
128         try {
129             return getNodeName();
130         }
131         catch (RepositoryException e) {
132             log.debug(e.getMessage(), e);
133             throw new CmisRuntimeException(e.getMessage(), e);
134         }
135     }
136 
137     /**
138      * @return  the id of the CMIS object represented by this instance
139      * @throws  CmisRuntimeException
140      */
141     public String getId() {
142         try {
143             return getObjectId();
144         }
145         catch (RepositoryException e) {
146             log.debug(e.getMessage(), e);
147             throw new CmisRuntimeException(e.getMessage(), e);
148         }
149     }
150 
151     /**
152      * @return  the typeId of the CMIS object represented by this instance
153      */
154     public String getTypeId() {
155         return getTypeIdInternal();
156     }
157 
158     /**
159      * @return  <code>true</code> iff this instance represent the root of the CMIS folder hierarchy.
160      */
161     public boolean isRoot() {
162         return pathManager.isRoot(node);
163     }
164 
165     /**
166      * @return  <code>true</code> iff this instance represents a cmis:document type
167      */
168     public boolean isDocument() {
169         return BaseTypeId.CMIS_DOCUMENT == getBaseTypeId();
170     }
171 
172     /**
173      * @return  <code>true</code> iff this instance represents a cmis:folder type
174      */
175     public boolean isFolder() {
176         return BaseTypeId.CMIS_FOLDER == getBaseTypeId();
177     }
178 
179     /**
180      * @return  <code>true</code> iff this instance represents a versionable CMIS object
181      */
182     public boolean isVersionable() {
183         TypeDefinition typeDef = typeManager.getType(getTypeIdInternal());
184         return typeDef instanceof DocumentTypeDefinition
185                 ? ((DocumentTypeDefinition) typeDef).isVersionable()
186                 : false;
187     }
188 
189     /**
190      * @return  this instance as a <code>JcrDocument</code>
191      * @throws CmisConstraintException if <code>this.isDocument() == false</code>
192      */
193     public JcrDocument asDocument() {
194         if (isDocument()) {
195             return (JcrDocument) this;
196         }
197         else {
198             throw new CmisConstraintException("Not a document: " + this);
199         }
200     }
201 
202     /**
203      * @return  this instance as a <code>JcrFolder</code>
204      * @throws CmisConstraintException if <code>this.isFolder() == false</code>
205      */
206     public JcrFolder asFolder() {
207         if (isFolder()) {
208             return (JcrFolder) this;
209         }
210         else {
211             throw new CmisObjectNotFoundException("Not a folder: " + this);
212         }
213     }
214 
215     /**
216      * @return  this instance as a <code>JcrVersionBase</code>
217      * @throws CmisConstraintException if <code>this.isVersionable() == false</code>
218      */
219     public JcrVersionBase asVersion() {
220         if (isVersionable()) {
221             return (JcrVersionBase) this;
222         }
223         else {
224             throw new CmisObjectNotFoundException("Not a version: " + this);
225         }
226     }
227 
228     /**
229      * Factory method creating a new <code>JcrNode</code> from a node at a given JCR path.
230      *
231      * @param path  JCR path of the node
232      * @return  A new instance representing the JCR node at <code>path</code>
233      * @throws CmisObjectNotFoundException  if <code>path</code> does not identify a JCR node
234      * @throws CmisRuntimeException
235      */
236     public JcrNode getNode(String path) {
237         try {
238             return create(node.getNode(path));
239         }
240         catch (PathNotFoundException e) {
241             log.debug(e.getMessage(), e);
242             throw new CmisObjectNotFoundException(e.getMessage(), e);
243         }
244         catch (RepositoryException e) {
245             log.debug(e.getMessage(), e);
246             throw new CmisRuntimeException(e.getMessage(), e);
247         }
248     }
249 
250     /**
251      * Compile the <code>ObjectData</code> for this node
252      */
253     public ObjectData compileObjectType(Set<String> filter, Boolean includeAllowableActions,
254             ObjectInfoHandler objectInfos, boolean requiresObjectInfo) {
255 
256         try {
257             ObjectDataImpl result = new ObjectDataImpl();
258             ObjectInfoImpl objectInfo = new ObjectInfoImpl();
259 
260             PropertiesImpl properties = new PropertiesImpl();
261             filter = filter == null ? null : new HashSet<String>(filter);
262             compileProperties(properties, filter, objectInfo);
263             result.setProperties(properties);
264             if (filter != null && !filter.isEmpty()) {
265                 log.debug("Unknown filter properties: " + filter.toString());
266             }
267 
268             if (Boolean.TRUE.equals(includeAllowableActions)) {
269                 result.setAllowableActions(getAllowableActions());
270             }
271 
272             if (requiresObjectInfo) {
273                 objectInfo.setObject(result);
274                 objectInfos.addObjectInfo(objectInfo);
275             }
276 
277             return result;
278         }
279         catch (RepositoryException e) {
280             log.debug(e.getMessage(), e);
281             throw new CmisRuntimeException(e.getMessage(), e);
282         }
283     }
284 
285     /**
286      * See CMIS 1.0 section 2.2.4.6 getAllowableActions
287      */
288     public AllowableActions getAllowableActions() {
289         AllowableActionsImpl aas = new AllowableActionsImpl();
290         aas.setAllowableActions(compileAllowableActions(new HashSet<Action>()));
291         return aas;
292     }
293 
294     /**
295      * See CMIS 1.0 section 2.2.3.5 getObjectParents
296      *
297      * @return  parent of this object
298      * @throws  CmisObjectNotFoundException  if this is the root folder
299      * @throws  CmisRuntimeException
300      */
301     public JcrFolder getParent() {
302         try {
303             return create(node.getParent()).asFolder();
304         }
305         catch (ItemNotFoundException e) {
306             log.debug(e.getMessage(), e);
307             throw new CmisObjectNotFoundException(e.getMessage(), e);
308         }
309         catch (RepositoryException e) {
310             log.debug(e.getMessage(), e);
311             throw new CmisRuntimeException(e.getMessage(), e);
312         }
313     }
314 
315     /**
316      * See CMIS 1.0 section 2.2.4.12 updateProperties
317      *
318      * @throws CmisStorageException
319      */
320     public JcrNode updateProperties(Properties properties) {
321         // get and check the new name
322         String newName = PropertyHelper.getStringProperty(properties, PropertyIds.NAME);
323         boolean rename = newName != null && !getName().equals(newName);
324         if (rename && !JcrConverter.isValidJcrName(newName)) {
325             throw new CmisNameConstraintViolationException("Name is not valid: " + newName);
326         }
327         if (rename && isRoot()) {
328             throw new CmisUpdateConflictException("Cannot rename root node");
329         }
330 
331         try {
332             // rename file or folder if necessary
333             Session session = getNode().getSession();
334             Node newNode;
335             if (rename) {
336                 String destPath = PathManager.createCmisPath(node.getParent().getPath(), newName);
337                 session.move(node.getPath(), destPath);
338                 newNode = session.getNode(destPath);
339             }
340             else {
341                 newNode = node;
342             }
343 
344             // Are there properties to update?
345             PropertyUpdater propertyUpdater = PropertyUpdater.create(typeManager, getTypeId(), properties);
346 
347             JcrVersionBase jcrVersion = isVersionable()
348                     ? asVersion()
349                     : null;
350 
351             // Update properties. Checkout if required
352             boolean autoCheckout = false;
353             if (!propertyUpdater.isEmpty()) {
354                 autoCheckout = jcrVersion != null && !jcrVersion.isCheckedOut();
355                 if (autoCheckout) {
356                     jcrVersion.checkout();
357                 }
358 
359                 // update the properties
360                 propertyUpdater.apply(node);
361             }
362 
363             session.save();
364 
365             if (autoCheckout) {
366                 // auto versioning -> return new version created by checkin
367                 return jcrVersion.checkin(null, null, "auto checkout");
368             }
369             else if (jcrVersion != null && jcrVersion.isCheckedOut()) {
370                 // the node is checked out -> return pwc.
371                 JcrVersionBase jcrNewVersion = create(newNode).asVersion();
372                 return jcrNewVersion.getPwc();
373             }
374             else {
375                 // non versionable or not a new node -> return this
376                 return create(newNode);
377             }
378         }
379         catch (RepositoryException e) {
380             log.debug(e.getMessage(), e);
381             throw new CmisStorageException(e.getMessage(), e);
382         }
383 
384     }
385 
386     /**
387      * See CMIS 1.0 section 2.2.4.14 deleteObject
388      *
389      * @throws CmisRuntimeException
390      */
391     public void delete(boolean allVersions, boolean isPwc) {
392         try {
393             Session session = getNode().getSession();
394             getNode().remove();
395             session.save();
396         }
397         catch (RepositoryException e) {
398             log.debug(e.getMessage(), e);
399             throw new CmisRuntimeException(e.getMessage(), e);
400         }
401     }
402 
403     /**
404      * See CMIS 1.0 section 2.2.4.13 moveObject
405      *
406      * @throws CmisStorageException
407      */
408     public JcrNode move(JcrFolder parent) {
409         try {
410             // move it if target location is not same as source location
411             String destPath = PathManager.createCmisPath(parent.getNode().getPath(), node.getName());
412             String srcPath = node.getPath();
413             Node newNode;
414             if (srcPath.equals(destPath)) {
415                 newNode = node;
416             }
417             else {
418                 Session session = getNode().getSession();
419                 session.move(srcPath, destPath);
420                 newNode = session.getNode(destPath);
421                 session.save();
422             }
423 
424             return create(newNode);
425         }
426         catch (RepositoryException e) {
427             log.debug(e.getMessage(), e);
428             throw new CmisStorageException(e.getMessage(), e);
429         }
430     }
431 
432     @Override
433     public String toString() {
434         try {
435             return node.getPath();
436         }
437         catch (RepositoryException e) {
438             log.debug(e.getMessage(), e);
439             return e.getMessage();
440         }
441     }
442 
443     //------------------------------------------< protected >---
444 
445     /**
446      * Retrieve the context node of the CMIS object represented by this instance. The
447      * context node is the node which is used to derive the common properties from
448      * (creation date, modification date, ...)
449      *
450      * @return  the context node
451      * @throws RepositoryException
452      */
453     protected abstract Node getContextNode() throws RepositoryException;
454 
455     /**
456      * @return  the value of the <code>cmis:baseTypeId</code> property
457      */
458     protected abstract BaseTypeId getBaseTypeId();
459 
460     /**
461      * @return  the value of the <code>cmis:objectTypeId</code> property
462      */
463     protected abstract String getTypeIdInternal();
464 
465     /**
466      * Compile the properties of the CMIS object represented by this instance.
467      * See CMIS 1.0 section 2.2.4.7 getObject
468      *
469      * @param properties  compilation of properties
470      * @param filter
471      * @param objectInfo
472      * @throws RepositoryException
473      */
474     protected void compileProperties(PropertiesImpl properties, Set<String> filter, ObjectInfoImpl objectInfo)
475             throws RepositoryException {
476 
477         String typeId = getTypeIdInternal();
478         BaseTypeId baseTypeId = getBaseTypeId();
479 
480         objectInfo.setBaseType(baseTypeId);
481         objectInfo.setTypeId(typeId);
482         objectInfo.setHasAcl(false);
483         objectInfo.setVersionSeriesId(getVersionSeriesId());
484         objectInfo.setRelationshipSourceIds(null);
485         objectInfo.setRelationshipTargetIds(null);
486         objectInfo.setRenditionInfos(null);
487         objectInfo.setSupportsPolicies(false);
488         objectInfo.setSupportsRelationships(false);
489 
490         // id
491         String objectId = getObjectId();
492         addPropertyId(properties, typeId, filter, PropertyIds.OBJECT_ID, objectId);
493         objectInfo.setId(objectId);
494 
495         // name
496         String name = getNodeName();
497         addPropertyString(properties, typeId, filter, PropertyIds.NAME, name);
498         objectInfo.setName(name);
499 
500         // base type and type name
501         addPropertyId(properties, typeId, filter, PropertyIds.BASE_TYPE_ID, baseTypeId.value());
502         addPropertyId(properties, typeId, filter, PropertyIds.OBJECT_TYPE_ID, typeId);
503 
504         // created and modified by
505         String createdBy = getCreatedBy();
506         addPropertyString(properties, typeId, filter, PropertyIds.CREATED_BY, createdBy);
507         objectInfo.setCreatedBy(createdBy);
508 
509         addPropertyString(properties, typeId, filter, PropertyIds.LAST_MODIFIED_BY, getLastModifiedBy());
510 
511         // creation and modification date
512         GregorianCalendar created = getCreated();
513         addPropertyDateTime(properties, typeId, filter, PropertyIds.CREATION_DATE, created);
514         objectInfo.setCreationDate(created);
515 
516         GregorianCalendar lastModified = getLastModified();
517         addPropertyDateTime(properties, typeId, filter, PropertyIds.LAST_MODIFICATION_DATE, lastModified);
518         objectInfo.setLastModificationDate(lastModified);
519 
520         addPropertyString(properties, typeId, filter, PropertyIds.CHANGE_TOKEN, getChangeToken());
521     }
522 
523     /**
524      * Compile the allowed actions on the CMIS object represented by this instance
525      * See CMIS 1.0 section 2.2.4.6 getAllowableActions
526      *
527      * @param aas  compilation of allowed actions
528      * @return
529      */
530     protected Set<Action> compileAllowableActions(Set<Action> aas) {
531         setAction(aas, Action.CAN_GET_OBJECT_PARENTS, true);
532         setAction(aas, Action.CAN_GET_PROPERTIES, true);
533         setAction(aas, Action.CAN_UPDATE_PROPERTIES, true);
534         setAction(aas, Action.CAN_MOVE_OBJECT, true);
535         setAction(aas, Action.CAN_DELETE_OBJECT, true);
536         setAction(aas, Action.CAN_GET_ACL, false);
537         setAction(aas, Action.CAN_APPLY_ACL, false);
538         setAction(aas, Action.CAN_GET_OBJECT_RELATIONSHIPS, false);
539         setAction(aas, Action.CAN_ADD_OBJECT_TO_FOLDER, false);
540         setAction(aas, Action.CAN_REMOVE_OBJECT_FROM_FOLDER, false);
541         setAction(aas, Action.CAN_APPLY_POLICY, false);
542         setAction(aas, Action.CAN_GET_APPLIED_POLICIES, false);
543         setAction(aas, Action.CAN_REMOVE_POLICY, false);
544         setAction(aas, Action.CAN_CREATE_RELATIONSHIP, false);
545         return aas;
546     }
547 
548     /**
549      * @return  the change token of the CMIS object represented by this instance
550      * @throws RepositoryException
551      */
552     protected String getChangeToken() throws RepositoryException {
553         return null;
554     }
555 
556     /**
557      * @return  the last modifier of the CMIS object represented by this instance
558      * @throws RepositoryException
559      */
560     protected String getLastModifiedBy() throws RepositoryException {
561         return getPropertyOrElse(getContextNode(), Property.JCR_LAST_MODIFIED_BY, USER_UNKNOWN);
562     }
563 
564     /**
565      * @return  the last modification date of the CMIS object represented by this instance
566      * @throws RepositoryException
567      */
568     protected GregorianCalendar getLastModified() throws RepositoryException {
569         return getPropertyOrElse(getContextNode(), Property.JCR_LAST_MODIFIED, DATE_UNKNOWN);
570     }
571 
572     /**
573      * @return  the creation date of the CMIS object represented by this instance
574      * @throws RepositoryException
575      */
576     protected GregorianCalendar getCreated() throws RepositoryException {
577         return getPropertyOrElse(getContextNode(), Property.JCR_CREATED, DATE_UNKNOWN);
578     }
579 
580     /**
581      * @return  the creator of the CMIS object represented by this instance
582      * @throws RepositoryException
583      */
584     protected String getCreatedBy() throws RepositoryException {
585         return getPropertyOrElse(getContextNode(), Property.JCR_CREATED_BY, USER_UNKNOWN);
586     }
587 
588     /**
589      * @return  the name of the underlying JCR <code>node</code>.
590      * @throws RepositoryException
591      */
592     protected String getNodeName() throws RepositoryException {
593         return node.getName();
594     }
595 
596     /**
597      * @return  the object id of the CMIS object represented by this instance
598      * @throws RepositoryException
599      */
600     protected String getObjectId() throws RepositoryException {
601         return getVersionSeriesId();
602     }
603 
604     /**
605      * @return  the versions series id of the CMIS object represented by this instance
606      * @throws RepositoryException
607      */
608     protected String getVersionSeriesId() throws RepositoryException {
609         return node.getIdentifier();
610     }
611 
612     /**
613      * Factory method for creating a new <code>JcrNode</code> instance from a JCR <code>Node</code>
614      *
615      * @param node  the JCR <code>Node</code>
616      * @return  a new <code>JcrNode</code>
617      */
618     protected final JcrNode create(Node node) {
619         return typeHandlerManager.create(node);
620     }
621 
622     /**
623      * Add Id property to the CMIS object represented by this instance
624      */
625     protected final void addPropertyId(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
626         if (value == null) {
627             throw new IllegalArgumentException("Value must not be null!");
628         }
629 
630         if (!checkAddProperty(props, typeId, filter, id)) {
631             return;
632         }
633 
634         PropertyIdImpl prop = new PropertyIdImpl(id, value);
635         prop.setQueryName(id);
636         props.addProperty(prop);
637     }
638 
639     /**
640      * Add string property to the CMIS object represented by this instance
641      */
642     protected final void addPropertyString(PropertiesImpl props, String typeId, Set<String> filter, String id, String value) {
643         if (!checkAddProperty(props, typeId, filter, id)) {
644             return;
645         }
646 
647         PropertyStringImpl prop = new PropertyStringImpl(id, value);
648         prop.setQueryName(id);
649         props.addProperty(prop);
650     }
651 
652     /**
653      * Add integer property to the CMIS object represented by this instance
654      */
655     protected final void addPropertyInteger(PropertiesImpl props, String typeId, Set<String> filter, String id, long value) {
656         if (!checkAddProperty(props, typeId, filter, id)) {
657             return;
658         }
659 
660         PropertyIntegerImpl prop = new PropertyIntegerImpl(id, BigInteger.valueOf(value));
661         prop.setQueryName(id);
662         props.addProperty(prop);
663     }
664 
665     /**
666      * Add boolean property to the CMIS object represented by this instance
667      */
668     protected final void addPropertyBoolean(PropertiesImpl props, String typeId, Set<String> filter, String id, boolean value) {
669         if (!checkAddProperty(props, typeId, filter, id)) {
670             return;
671         }
672 
673         PropertyBooleanImpl prop = new PropertyBooleanImpl(id, value);
674         prop.setQueryName(id);
675         props.addProperty(prop);
676     }
677 
678     /**
679      * Add date-time property to the CMIS object represented by this instance
680      */
681     protected final void addPropertyDateTime(PropertiesImpl props, String typeId, Set<String> filter, String id,
682             GregorianCalendar value) {
683 
684         if (!checkAddProperty(props, typeId, filter, id)) {
685             return;
686         }
687 
688         PropertyDateTimeImpl prop = new PropertyDateTimeImpl(id, value);
689         prop.setQueryName(id);
690         props.addProperty(prop);
691     }
692 
693     /**
694      * Validate a set of properties against a filter and its definitions
695      */
696     protected final boolean checkAddProperty(Properties properties, String typeId, Set<String> filter, String id) {
697         if (properties == null || properties.getProperties() == null) {
698             throw new IllegalArgumentException("Properties must not be null!");
699         }
700 
701         if (id == null) {
702             throw new IllegalArgumentException("Id must not be null!");
703         }
704 
705         TypeDefinition type = typeManager.getType(typeId);
706         if (type == null) {
707             throw new IllegalArgumentException("Unknown type: " + typeId);
708         }
709         if (!type.getPropertyDefinitions().containsKey(id)) {
710             throw new IllegalArgumentException("Unknown property: " + id);
711         }
712 
713         String queryName = type.getPropertyDefinitions().get(id).getQueryName();
714 
715         if (queryName != null && filter != null) {
716             if (filter.contains(queryName)) {
717                 filter.remove(queryName);
718             }
719             else {
720                 return false;
721             }
722         }
723 
724         return true;
725     }
726 
727     /**
728      * Thunk for {@link JcrNode#updateProperties(Node, String, Properties)}
729      */
730     protected static final class PropertyUpdater {
731         private final List<PropertyData<?>> removeProperties = new ArrayList<PropertyData<?>>();
732         private final List<PropertyData<?>> updateProperties = new ArrayList<PropertyData<?>>();
733 
734         private PropertyUpdater() { }
735 
736         public static PropertyUpdater create(JcrTypeManager typeManager, String typeId, Properties properties) {
737             if (properties == null) {
738                 throw new CmisConstraintException("No properties!");
739             }
740 
741             // get the property definitions
742             TypeDefinition type = typeManager.getType(typeId);
743             if (type == null) {
744                 throw new CmisObjectNotFoundException("Type '" + typeId + "' is unknown!");
745             }
746 
747             PropertyUpdater propertyUpdater = new PropertyUpdater();
748             // update properties
749             for (PropertyData<?> prop : properties.getProperties().values()) {
750                 PropertyDefinition<?> propDef = type.getPropertyDefinitions().get(prop.getId());
751 
752                 // do we know that property?
753                 if (propDef == null) {
754                     throw new CmisInvalidArgumentException("Property '" + prop.getId() + "' is unknown!");
755                 }
756 
757                 // skip content stream file name
758                 if (propDef.getId().equals(PropertyIds.CONTENT_STREAM_FILE_NAME)) {
759                     log.warn("Cannot set " + PropertyIds.CONTENT_STREAM_FILE_NAME + ". Ignoring");
760                     continue;
761                 }
762 
763                 // silently skip name
764                 if (propDef.getId().equals(PropertyIds.NAME)) {
765                     continue;
766                 }
767 
768                 // can it be set?
769                 if (propDef.getUpdatability() == Updatability.READONLY) {
770                     throw new CmisConstraintException("Property '" + prop.getId() + "' is readonly!");
771                 }
772 
773                 if (propDef.getUpdatability() == Updatability.ONCREATE) {
774                     throw new CmisConstraintException("Property '" + prop.getId() + "' can only be set on create!");
775                 }
776 
777                 // default or value
778                 PropertyData<?> newProp;
779                 newProp = PropertyHelper.isPropertyEmpty(prop)
780                         ? PropertyHelper.getDefaultValue(propDef)
781                         : prop;
782 
783                 // Schedule for remove or update
784                 if (newProp == null) {
785                     propertyUpdater.removeProperties.add(prop);
786                 }
787                 else {
788                     propertyUpdater.updateProperties.add(newProp);
789                 }
790             }
791 
792             return propertyUpdater;
793         }
794 
795         public boolean isEmpty() {
796             return removeProperties.isEmpty() && updateProperties.isEmpty();
797         }
798 
799         public void apply(Node node) {
800             try {
801                 for (PropertyData<?> prop: removeProperties) {
802                     JcrConverter.removeProperty(node, prop);
803                 }
804                 for (PropertyData<?> prop: updateProperties) {
805                     JcrConverter.setProperty(node, prop);
806                 }
807             }
808             catch (RepositoryException e) {
809                 log.debug(e.getMessage(), e);
810                 throw new CmisStorageException(e.getMessage(), e);
811             }
812         }
813     }
814 
815     /**
816      * Update the properties of the CMIS object represented by this instance
817      */
818     protected final void updateProperties(Node node, String typeId, Properties properties) {
819         PropertyUpdater.create(typeManager, typeId, properties).apply(node);
820     }
821 
822     /**
823      * Utility function for retrieving the version history of a JCR <code>Node</code>.
824      *
825      * @param node  the node for which to retrieve the version history
826      * @return  version history of <code>node</code>
827      * @throws RepositoryException  if <code>node</code> is not versionable
828      */
829     protected static VersionHistory getVersionHistory(Node node) throws RepositoryException {
830         return getVersionManager(node).getVersionHistory(node.getPath());
831     }
832 
833     /**
834      * Utility function for retrieving the version manager from a JCR <code>Node</code>.
835      *
836      * @param node
837      * @return
838      * @throws RepositoryException
839      */
840     protected static VersionManager getVersionManager(Node node) throws RepositoryException {
841         return node.getSession().getWorkspace().getVersionManager();
842     }
843 
844     /**
845      * Utility function for retrieving the base version of a JCR <code>Node</code>.
846      *
847      * @param node  the node for which to retrieve the base version
848      * @return  version base version of <code>node</code>
849      * @throws RepositoryException  if <code>node</code> is not versionable
850      */
851     protected static Version getBaseVersion(Node node) throws RepositoryException {
852         return getVersionManager(node).getBaseVersion(node.getPath());
853     }
854 
855     /**
856      * Utility function to retrieve the length of a property of a JCR <code>Node</code>.
857      *
858      * @param node
859      * @param propertyName
860      * @return
861      * @throws RepositoryException
862      */
863     protected static long getPropertyLength(Node node, String propertyName) throws RepositoryException {
864         return node.hasProperty(propertyName)
865             ? node.getProperty(propertyName).getLength()
866             : -1;
867     }
868 
869     /**
870      * Utility function for retrieving a string property from a JCR <code>Node</code> or a default
871      * value in case of an error.
872      *
873      * @param node
874      * @param propertyName
875      * @param defaultValue
876      * @return
877      * @throws RepositoryException
878      */
879     protected static String getPropertyOrElse(Node node, String propertyName, String defaultValue)
880             throws RepositoryException {
881 
882         return node.hasProperty(propertyName)
883             ? node.getProperty(propertyName).getString()
884             : defaultValue;
885     }
886 
887     /**
888      * Utility function for retrieving a date property from a JCR <code>Node</code> or a default
889      * value in case of an error.
890      *
891      * @param node
892      * @param propertyName
893      * @param defaultValue
894      * @return
895      * @throws RepositoryException
896      */
897     protected static GregorianCalendar getPropertyOrElse(Node node, String propertyName, GregorianCalendar defaultValue)
898             throws RepositoryException {
899 
900         if (node.hasProperty(propertyName)) {
901             Calendar date = node.getProperty(propertyName).getDate();
902             return Util.toCalendar(date);
903         }
904         else {
905             return defaultValue;
906         }
907     }
908 
909     /**
910      * Add <code>action</code> to <code>actions</code> iff <code>condition</code> is true.
911      *
912      * @param actions
913      * @param action
914      * @param condition
915      */
916     protected static void setAction(Set<Action> actions, Action action, boolean condition) {
917         if (condition) {
918             actions.add(action);
919         }
920         else {
921             actions.remove(action);
922         }
923     }
924 }