View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.jcr;
25  
26  import java.io.InputStream;
27  import java.security.AccessControlException;
28  import java.util.ArrayList;
29  import java.util.Calendar;
30  import java.util.Collection;
31  import java.util.Collections;
32  import java.util.HashSet;
33  import java.util.Iterator;
34  import java.util.LinkedList;
35  import java.util.List;
36  import java.util.Set;
37  import java.util.UUID;
38  import java.util.regex.Pattern;
39  import javax.jcr.AccessDeniedException;
40  import javax.jcr.InvalidItemStateException;
41  import javax.jcr.Item;
42  import javax.jcr.ItemExistsException;
43  import javax.jcr.ItemNotFoundException;
44  import javax.jcr.ItemVisitor;
45  import javax.jcr.NoSuchWorkspaceException;
46  import javax.jcr.NodeIterator;
47  import javax.jcr.PathNotFoundException;
48  import javax.jcr.Property;
49  import javax.jcr.PropertyIterator;
50  import javax.jcr.PropertyType;
51  import javax.jcr.RepositoryException;
52  import javax.jcr.UnsupportedRepositoryOperationException;
53  import javax.jcr.Value;
54  import javax.jcr.ValueFormatException;
55  import javax.jcr.lock.Lock;
56  import javax.jcr.lock.LockException;
57  import javax.jcr.nodetype.ConstraintViolationException;
58  import javax.jcr.nodetype.NoSuchNodeTypeException;
59  import javax.jcr.nodetype.NodeDefinition;
60  import javax.jcr.nodetype.NodeType;
61  import javax.jcr.nodetype.NodeTypeManager;
62  import javax.jcr.query.Query;
63  import javax.jcr.query.QueryResult;
64  import javax.jcr.version.Version;
65  import javax.jcr.version.VersionException;
66  import javax.jcr.version.VersionHistory;
67  import net.jcip.annotations.Immutable;
68  import org.modeshape.common.i18n.I18n;
69  import org.modeshape.common.util.CheckArg;
70  import org.modeshape.graph.Location;
71  import org.modeshape.graph.connector.RepositorySourceException;
72  import org.modeshape.graph.property.Binary;
73  import org.modeshape.graph.property.DateTime;
74  import org.modeshape.graph.property.Name;
75  import org.modeshape.graph.property.NamespaceRegistry;
76  import org.modeshape.graph.property.Path;
77  import org.modeshape.graph.property.PathFactory;
78  import org.modeshape.graph.property.ValueFactories;
79  import org.modeshape.graph.query.QueryBuilder;
80  import org.modeshape.graph.query.model.QueryCommand;
81  import org.modeshape.graph.session.GraphSession.Node;
82  import org.modeshape.graph.session.GraphSession.NodeId;
83  import org.modeshape.graph.session.GraphSession.PropertyInfo;
84  import org.modeshape.jcr.SessionCache.JcrNodePayload;
85  import org.modeshape.jcr.SessionCache.JcrPropertyPayload;
86  import org.modeshape.jcr.SessionCache.NodeEditor;
87  
88  /**
89   * An abstract implementation of the JCR {@link javax.jcr.Node} interface. Instances of this class are created and managed by the
90   * {@link SessionCache}. Each instance indirectly references the {@link javax.jcr.Node node information} also managed by the
91   * SessionCache, and finds and operates against this information with each method call.
92   */
93  @Immutable
94  abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node {
95  
96      private static final NodeType[] EMPTY_NODE_TYPES = new NodeType[] {};
97  
98      protected final NodeId nodeId;
99      protected final Location location;
100 
101     AbstractJcrNode( SessionCache cache,
102                      NodeId nodeId,
103                      Location location ) {
104         super(cache);
105         this.nodeId = nodeId;
106         this.location = location;
107     }
108 
109     abstract boolean isRoot();
110 
111     public abstract AbstractJcrNode getParent() throws ItemNotFoundException, RepositoryException;
112 
113     final NodeId internalId() {
114         return nodeId;
115     }
116 
117     final Name name() throws RepositoryException {
118         return nodeInfo().getName();
119     }
120 
121     final Path.Segment segment() throws RepositoryException {
122         return nodeInfo().getSegment();
123     }
124 
125     final Node<JcrNodePayload, JcrPropertyPayload> nodeInfo()
126         throws ItemNotFoundException, AccessDeniedException, RepositoryException {
127         return cache.findNode(nodeId, location.getPath());
128     }
129 
130     final NodeEditor editorForParent() throws RepositoryException {
131         try {
132             Node<JcrNodePayload, JcrPropertyPayload> parent = nodeInfo().getParent();
133             return cache.getEditorFor(parent);
134         } catch (ItemNotFoundException err) {
135             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
136             throw new RepositoryException(msg);
137         } catch (InvalidItemStateException err) {
138             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
139             throw new RepositoryException(msg);
140         }
141     }
142 
143     final NodeEditor editor() throws RepositoryException {
144         try {
145             return cache.getEditorFor(nodeId, location.getPath());
146         } catch (ItemNotFoundException err) {
147             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
148             throw new RepositoryException(msg);
149         } catch (InvalidItemStateException err) {
150             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
151             throw new RepositoryException(msg);
152         }
153     }
154 
155     final JcrValue valueFrom( int propertyType,
156                               Object value ) {
157         return new JcrValue(cache.factories(), cache, propertyType, value);
158     }
159 
160     final JcrValue valueFrom( Calendar value ) {
161         ValueFactories factories = cache.factories();
162         DateTime dateTime = factories.getDateFactory().create(value);
163         return new JcrValue(factories, cache, PropertyType.DATE, dateTime);
164     }
165 
166     final JcrValue valueFrom( InputStream value ) {
167         ValueFactories factories = cache.factories();
168         Binary binary = factories.getBinaryFactory().create(value);
169         return new JcrValue(factories, cache, PropertyType.DATE, binary);
170     }
171 
172     final JcrValue valueFrom( javax.jcr.Node value ) throws UnsupportedRepositoryOperationException, RepositoryException {
173         ValueFactories factories = cache.factories();
174         String uuid = factories.getStringFactory().create(value.getUUID());
175         return new JcrValue(factories, cache, PropertyType.REFERENCE, uuid);
176     }
177 
178     final JcrValue[] valuesFrom( int propertyType,
179                                  Object[] values ) {
180         /*
181          * Null values in the array are "compacted" (read: ignored) as per section 7.1.6 in the JCR 1.0.1 specification. 
182          */
183         int len = values.length;
184         ValueFactories factories = cache.factories();
185         List<JcrValue> results = new ArrayList<JcrValue>(len);
186         for (int i = 0; i != len; ++i) {
187             if (values[i] != null) results.add(new JcrValue(factories, cache, propertyType, values[i]));
188         }
189         return results.toArray(new JcrValue[results.size()]);
190     }
191 
192     @Override
193     Path path() throws RepositoryException {
194         // Don't use the path in the location, since it may no longer be valid
195         return nodeInfo().getPath();
196     }
197 
198     boolean isReferenceable() throws RepositoryException {
199         return isNodeType(JcrMixLexicon.REFERENCEABLE);
200     }
201 
202     /**
203      * {@inheritDoc}
204      * 
205      * @see javax.jcr.Node#getUUID()
206      */
207     public String getUUID() throws RepositoryException {
208         // Return "jcr:uuid" only if node is referenceable
209         if (!isReferenceable()) {
210             throw new UnsupportedRepositoryOperationException(JcrI18n.nodeNotReferenceable.text());
211         }
212         PropertyInfo<JcrPropertyPayload> uuidProp = nodeInfo().getProperty(JcrLexicon.UUID);
213         if (uuidProp == null) {
214             uuidProp = nodeInfo().getProperty(ModeShapeLexicon.UUID);
215         }
216         assert uuidProp != null;
217         assert !uuidProp.getProperty().isEmpty();
218         return context().getValueFactories().getStringFactory().create(uuidProp.getProperty().getFirstValue());
219     }
220 
221     /**
222      * {@inheritDoc}
223      * 
224      * @return <code>true</code>
225      * @see javax.jcr.Item#isNode()
226      */
227     public final boolean isNode() {
228         return true;
229     }
230 
231     /**
232      * {@inheritDoc}
233      * 
234      * @return <code>false</code>
235      * @see javax.jcr.Node#isNodeType(java.lang.String)
236      */
237     public boolean isNodeType( String nodeTypeName ) throws RepositoryException {
238         return isNodeType(nameFrom(nodeTypeName));
239     }
240 
241     /**
242      * Determine whether this node's primary type or any of the mixins are or extend the node type with the supplied name. This
243      * method is semantically equivalent to but slightly more efficient than the {@link #isNodeType(String) equivalent in the JCR
244      * API}, especially when the node type name is already a {@link Name} object.
245      * 
246      * @param nodeTypeName the name of the node type
247      * @return true if this node is of the node type given by the supplied name, or false otherwise
248      * @throws RepositoryException if there is an exception
249      */
250     public final boolean isNodeType( Name nodeTypeName ) throws RepositoryException {
251         return cache.isNodeType(nodeInfo(), nodeTypeName);
252     }
253 
254     /**
255      * {@inheritDoc}
256      * 
257      * @see javax.jcr.Node#getDefinition()
258      */
259     public NodeDefinition getDefinition() throws RepositoryException {
260         NodeDefinitionId definitionId = nodeInfo().getPayload().getDefinitionId();
261         return session().nodeTypeManager().getNodeDefinition(definitionId);
262     }
263 
264     /**
265      * {@inheritDoc}
266      * 
267      * @see javax.jcr.Node#getPrimaryNodeType()
268      */
269     public JcrNodeType getPrimaryNodeType() throws RepositoryException {
270         return session().nodeTypeManager().getNodeType(getPrimaryTypeName());
271     }
272 
273     Name getPrimaryTypeName() throws RepositoryException {
274         return nodeInfo().getPayload().getPrimaryTypeName();
275     }
276 
277     /**
278      * {@inheritDoc}
279      * 
280      * @see javax.jcr.Node#getMixinNodeTypes()
281      */
282     public NodeType[] getMixinNodeTypes() throws RepositoryException {
283         NodeTypeManager nodeTypeManager = session().nodeTypeManager();
284         Property mixinTypesProperty = getProperty(JcrLexicon.MIXIN_TYPES);
285         if (mixinTypesProperty == null) return EMPTY_NODE_TYPES;
286         List<NodeType> mixinNodeTypes = new LinkedList<NodeType>();
287         for (Value value : mixinTypesProperty.getValues()) {
288             String nodeTypeName = value.getString();
289             NodeType nodeType = nodeTypeManager.getNodeType(nodeTypeName);
290             if (nodeType != null) mixinNodeTypes.add(nodeType);
291         }
292         return mixinNodeTypes.toArray(new NodeType[mixinNodeTypes.size()]);
293     }
294 
295     List<Name> getMixinTypeNames() throws RepositoryException {
296         return nodeInfo().getPayload().getMixinTypeNames();
297     }
298 
299     /**
300      * {@inheritDoc}
301      * 
302      * @see javax.jcr.Node#getPrimaryItem()
303      */
304     public final Item getPrimaryItem() throws RepositoryException {
305         // Get the primary item name from this node's type ...
306         NodeType primaryType = getPrimaryNodeType();
307         String primaryItemNameString = primaryType.getPrimaryItemName();
308         if (primaryItemNameString == null) {
309             I18n msg = JcrI18n.noPrimaryItemNameDefinedOnPrimaryType;
310             throw new ItemNotFoundException(msg.text(primaryType.getName(), getPath(), cache.workspaceName()));
311         }
312         try {
313             Path primaryItemPath = context().getValueFactories().getPathFactory().create(primaryItemNameString);
314             if (primaryItemPath.size() != 1 || primaryItemPath.isAbsolute()) {
315                 I18n msg = JcrI18n.primaryItemNameForPrimaryTypeIsNotValid;
316                 throw new ItemNotFoundException(msg.text(primaryType.getName(),
317                                                          primaryItemNameString,
318                                                          getPath(),
319                                                          cache.workspaceName()));
320             }
321             return cache.findJcrItem(nodeId, location.getPath(), primaryItemPath);
322         } catch (ValueFormatException error) {
323             I18n msg = JcrI18n.primaryItemNameForPrimaryTypeIsNotValid;
324             throw new ItemNotFoundException(msg.text(primaryType.getName(),
325                                                      primaryItemNameString,
326                                                      getPath(),
327                                                      cache.workspaceName()));
328         } catch (PathNotFoundException error) {
329             I18n msg = JcrI18n.primaryItemDoesNotExist;
330             throw new ItemNotFoundException(msg.text(primaryType.getName(),
331                                                      primaryItemNameString,
332                                                      getPath(),
333                                                      cache.workspaceName()));
334         }
335     }
336 
337     /**
338      * {@inheritDoc}
339      * 
340      * @throws IllegalArgumentException if <code>otherItem</code> is <code>null</code>.
341      * @see javax.jcr.Item#isSame(javax.jcr.Item)
342      */
343     @Override
344     public boolean isSame( Item otherItem ) throws RepositoryException {
345         CheckArg.isNotNull(otherItem, "otherItem");
346         if (super.isSame(otherItem) && otherItem instanceof javax.jcr.Node) {
347             if (otherItem instanceof AbstractJcrNode) {
348                 AbstractJcrNode that = (AbstractJcrNode)otherItem;
349                 if (this.isReferenceable() && that.isReferenceable()) {
350                     // Both are referenceable, so compare the UUIDs ...
351                     return getUUID().equals(((AbstractJcrNode)otherItem).getUUID());
352                 }
353 
354                 // One or both are not referenceable, so find the nearest ancestor that is referenceable.
355                 // The correspondence identifier (per Section 4.10.2 of JSR-170, version 1.0.1) for a
356                 // non-referenceable node is the pair of the UUID of the nearest referenceable ancestor and
357                 // the relative path from that referenceable ancestor. Per Section 6.2.8, two non-referenceable
358                 // nodes are the same if they have the same correspondence identifier.
359                 CorrespondenceId thisId = this.getCorrespondenceId();
360                 CorrespondenceId thatId = that.getCorrespondenceId();
361                 return thisId.equals(thatId);
362             }
363             // If not our implementation, let the other item figure out whether we are the same.
364             return otherItem.isSame(this);
365         }
366         return false;
367     }
368 
369     protected CorrespondenceId getCorrespondenceId() throws RepositoryException {
370         if (this.isReferenceable()) return new CorrespondenceId(getUUID());
371         assert !this.isRoot(); // the root must be referenceable
372 
373         // Find the nearest ancestor that is referenceable ...
374         Path currentPath = path();
375         AbstractJcrNode node = this.getParent();
376         int beginIndex = currentPath.size() - 1;
377         while (!node.isRoot() && !node.isReferenceable()) {
378             node = node.getParent();
379             --beginIndex;
380         }
381         // Get the relative path from the ancestor to this node ...
382         Path relativePath = currentPath.relativeTo(node.path());
383         assert !relativePath.isAbsolute();
384         return new CorrespondenceId(node.getUUID(), relativePath);
385     }
386 
387     /**
388      * {@inheritDoc}
389      * 
390      * @see javax.jcr.Node#hasProperties()
391      */
392     public final boolean hasProperties() throws RepositoryException {
393         return nodeInfo().getPropertyCount() > 0;
394     }
395 
396     /**
397      * {@inheritDoc}
398      * 
399      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
400      * @see javax.jcr.Node#hasProperty(java.lang.String)
401      */
402     public final boolean hasProperty( String relativePath ) throws RepositoryException {
403         CheckArg.isNotEmpty(relativePath, "relativePath");
404         if (relativePath.indexOf('/') >= 0) {
405             try {
406                 getProperty(relativePath);
407                 return true;
408             } catch (PathNotFoundException e) {
409                 return false;
410             }
411         }
412         if (relativePath.equals(".")) return false;
413         if (relativePath.equals("..")) return false;
414         // Otherwise it should be a property on this node ...
415         return nodeInfo().getProperty(nameFrom(relativePath)) != null;
416     }
417 
418     public final boolean hasProperty( Name name ) throws RepositoryException {
419         return nodeInfo().getProperty(name) != null;
420     }
421 
422     /**
423      * {@inheritDoc}
424      * 
425      * @see javax.jcr.Node#getProperties()
426      */
427     public final PropertyIterator getProperties() throws RepositoryException {
428         return new JcrPropertyIterator(cache.findJcrPropertiesFor(nodeId, location.getPath()));
429     }
430 
431     /**
432      * {@inheritDoc}
433      * 
434      * @see javax.jcr.Node#getProperties(java.lang.String)
435      */
436     public PropertyIterator getProperties( String namePattern ) throws RepositoryException {
437         CheckArg.isNotNull(namePattern, "namePattern");
438         namePattern = namePattern.trim();
439         if (namePattern.length() == 0) return new JcrEmptyPropertyIterator();
440         Collection<AbstractJcrProperty> properties = cache.findJcrPropertiesFor(nodeId, location.getPath());
441         if ("*".equals(namePattern)) return new JcrPropertyIterator(properties);
442 
443         // Figure out the patterns for each of the different disjunctions in the supplied pattern ...
444         List<Object> patterns = createPatternsFor(namePattern);
445 
446         // Go through the properties and remove any property that doesn't match a pattern ...
447         boolean foundMatch = true;
448         Collection<AbstractJcrProperty> matchingProperties = new LinkedList<AbstractJcrProperty>();
449         Iterator<AbstractJcrProperty> iter = properties.iterator();
450         while (iter.hasNext()) {
451             AbstractJcrProperty property = iter.next();
452             String propName = property.getName();
453             assert foundMatch == true;
454             for (Object patternOrMatch : patterns) {
455                 if (patternOrMatch instanceof Pattern) {
456                     Pattern pattern = (Pattern)patternOrMatch;
457                     if (pattern.matcher(propName).matches()) break;
458                 } else {
459                     String match = (String)patternOrMatch;
460                     if (propName.equals(match)) break;
461                 }
462                 // No pattern matched ...
463                 foundMatch = false;
464             }
465             if (foundMatch) {
466                 matchingProperties.add(property);
467             } else {
468                 foundMatch = true; // for the next iteration ..
469             }
470         }
471         return new JcrPropertyIterator(matchingProperties);
472     }
473 
474     /**
475      * Obtain an iterator over the nodes that reference this node.
476      * 
477      * @param maxNumberOfNodes the maximum number of nodes that should be returned, or {@link Integer#MAX_VALUE} (or a negative
478      *        value) for all nodes
479      * @return the iterator over the referencing nodes; never null
480      * @throws RepositoryException if an error occurs while obtaining the information
481      */
482     protected final NodeIterator referencingNodes( int maxNumberOfNodes ) throws RepositoryException {
483         if (!this.isReferenceable()) {
484             return new JcrEmptyNodeIterator();
485         }
486         if (maxNumberOfNodes < 0) maxNumberOfNodes = Integer.MAX_VALUE;
487 
488         // Execute a query that will report all nodes referencing this node ...
489         String uuid = getUUID();
490         QueryBuilder builder = new QueryBuilder(context().getValueFactories().getTypeSystem());
491         QueryCommand query = builder.select("jcr:primaryType")
492                                     .fromAllNodesAs("allNodes")
493                                     .where()
494                                     .referenceValue("allNodes")
495                                     .isEqualTo(uuid)
496                                     .end()
497                                     .limit(maxNumberOfNodes)
498                                     .query();
499         Query jcrQuery = session().workspace().queryManager().createQuery(query);
500         QueryResult result = jcrQuery.execute();
501         return result.getNodes();
502     }
503 
504     /**
505      * Determine whether there is at least one other node that has a reference to this node.
506      * 
507      * @return true if this node is referenced by at least one other node, or false if there are no references to this node
508      * @throws RepositoryException if an error occurs while obtaining the information
509      */
510     protected final boolean hasIncomingReferences() throws RepositoryException {
511         return referencingNodes(1).hasNext();
512     }
513 
514     /**
515      * {@inheritDoc}
516      * 
517      * @see javax.jcr.Node#getReferences()
518      */
519     public final PropertyIterator getReferences() throws RepositoryException {
520         if (!this.isReferenceable()) {
521             // This node is not referenceable, so it cannot have any references to it ...
522             return new JcrEmptyPropertyIterator();
523         }
524         NodeIterator iter = referencingNodes(Integer.MAX_VALUE);
525         if (!iter.hasNext()) {
526             return new JcrEmptyPropertyIterator();
527         }
528         String uuid = getUUID();
529         List<Property> references = new LinkedList<Property>();
530         while (iter.hasNext()) {
531             javax.jcr.Node node = iter.nextNode();
532             // Go through the properties and look for reference properties that have a value of this node's UUID ...
533             PropertyIterator propIter = node.getProperties();
534             while (propIter.hasNext()) {
535                 Property prop = propIter.nextProperty();
536                 // Look at the definition's required type ...
537                 int propType = prop.getDefinition().getRequiredType();
538                 if (propType == PropertyType.REFERENCE || propType == PropertyType.UNDEFINED || propType == PropertyType.STRING) {
539                     if (prop.getDefinition().isMultiple()) {
540                         for (Value value : prop.getValues()) {
541                             if (uuid.equals(value.getString())) {
542                                 references.add(prop);
543                                 break;
544                             }
545                         }
546                     } else {
547                         Value value = prop.getValue();
548                         if (uuid.equals(value.getString())) {
549                             references.add(prop);
550                         }
551                     }
552                 }
553             }
554         }
555 
556         if (references.isEmpty()) return new JcrEmptyPropertyIterator();
557         return new JcrPropertyIterator(references);
558     }
559 
560     /**
561      * A non-standard method to obtain a property given the {@link Name ModeShape Name} object. This method is faster
562      * 
563      * @param propertyName the property name
564      * @return the JCR property with the supplied name, or null if the property doesn't exist
565      * @throws RepositoryException if there is an error finding the property with the supplied name
566      */
567     public final Property getProperty( Name propertyName ) throws RepositoryException {
568         Property property = cache.findJcrProperty(nodeId, location.getPath(), propertyName);
569         // Must be referenceable in order to return this property ...
570         if (property != null && JcrLexicon.UUID.equals(propertyName) && !isReferenceable()) return null;
571         return property;
572     }
573 
574     /**
575      * {@inheritDoc}
576      * 
577      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
578      * @see javax.jcr.Node#getProperty(java.lang.String)
579      */
580     public final Property getProperty( String relativePath ) throws RepositoryException {
581         CheckArg.isNotEmpty(relativePath, "relativePath");
582         int indexOfFirstSlash = relativePath.indexOf('/');
583         if (indexOfFirstSlash == 0) {
584             // Not a relative path ...
585             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
586         }
587         Name propertyName = null;
588         if (indexOfFirstSlash != -1) {
589             // We know it's a relative path with more than one segment ...
590             Path path = pathFrom(relativePath).getNormalizedPath();
591             if (path.size() > 1) {
592                 try {
593                     AbstractJcrItem item = cache.findJcrItem(nodeId, location.getPath(), path);
594                     if (item instanceof Property) {
595                         return (Property)item;
596                     }
597                 } catch (ItemNotFoundException e) {
598                     I18n msg = JcrI18n.propertyNotFoundAtPathRelativeToReferenceNode;
599                     throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
600                 }
601                 I18n msg = JcrI18n.propertyNotFoundAtPathRelativeToReferenceNode;
602                 throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
603             }
604             propertyName = path.getLastSegment().getName();
605         } else {
606             propertyName = nameFrom(relativePath);
607         }
608         // It's just a name, so look for it directly ...
609         Property result = getProperty(propertyName);
610         if (result != null) return result;
611         I18n msg = JcrI18n.pathNotFoundRelativeTo;
612         throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
613     }
614 
615     /**
616      * {@inheritDoc}
617      * 
618      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
619      * @see javax.jcr.Node#hasNode(java.lang.String)
620      */
621     public final boolean hasNode( String relativePath ) throws RepositoryException {
622         CheckArg.isNotEmpty(relativePath, "relativePath");
623         if (relativePath.equals(".")) return true;
624         if (relativePath.equals("..")) return isRoot() ? false : true;
625         int indexOfFirstSlash = relativePath.indexOf('/');
626         if (indexOfFirstSlash == 0) {
627             // Not a relative path ...
628             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
629         }
630         if (indexOfFirstSlash != -1) {
631             Path path = pathFrom(relativePath).getNormalizedPath();
632             try {
633                 AbstractJcrNode item = cache.findJcrNode(nodeId, location.getPath(), path);
634                 return item != null;
635             } catch (PathNotFoundException e) {
636                 return false;
637             }
638         }
639         // It's just a name, so look for a child ...
640         try {
641             Path.Segment segment = segmentFrom(relativePath);
642             return nodeInfo().getChild(segment) != null;
643         } catch (org.modeshape.graph.property.PathNotFoundException e) {
644             return false;
645         }
646     }
647 
648     /**
649      * {@inheritDoc}
650      * 
651      * @see javax.jcr.Node#hasNodes()
652      */
653     public final boolean hasNodes() throws RepositoryException {
654         return nodeInfo().getChildrenCount() > 0;
655     }
656 
657     /**
658      * {@inheritDoc}
659      * 
660      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
661      * @see javax.jcr.Node#getNode(java.lang.String)
662      */
663     public final javax.jcr.Node getNode( String relativePath ) throws RepositoryException {
664         CheckArg.isNotEmpty(relativePath, "relativePath");
665         if (relativePath.equals(".")) return this;
666         if (relativePath.equals("..")) return this.getParent();
667         int indexOfFirstSlash = relativePath.indexOf('/');
668         if (indexOfFirstSlash == 0) {
669             // Not a relative path ...
670             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
671         }
672         Path.Segment segment = null;
673         if (indexOfFirstSlash != -1) {
674             // We know it's a relative path with more than one segment ...
675             Path path = pathFrom(relativePath).getNormalizedPath();
676             if (path.size() == 1) {
677                 if (path.getLastSegment().isSelfReference()) return this;
678                 if (path.getLastSegment().isParentReference()) return this.getParent();
679             }
680             // We know it's a resolved relative path with more than one segment ...
681             if (path.size() > 1) {
682                 AbstractJcrItem item = cache.findJcrNode(nodeId, location.getPath(), path);
683                 if (item instanceof javax.jcr.Node) {
684                     return (javax.jcr.Node)item;
685                 }
686                 I18n msg = JcrI18n.nodeNotFoundAtPathRelativeToReferenceNode;
687                 throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
688             }
689             segment = path.getLastSegment();
690         } else {
691             segment = segmentFrom(relativePath);
692         }
693         // It's just a name, so look for a child ...
694         try {
695             return nodeInfo().getChild(segment).getPayload().getJcrNode();
696         } catch (org.modeshape.graph.property.PathNotFoundException e) {
697             String msg = JcrI18n.childNotFoundUnderNode.text(segment, getPath(), cache.workspaceName());
698             throw new PathNotFoundException(msg);
699         } catch (RepositorySourceException e) {
700             throw new RepositoryException(e.getLocalizedMessage(), e);
701         }
702     }
703 
704     /**
705      * {@inheritDoc}
706      * 
707      * @see javax.jcr.Node#getNodes()
708      */
709     public final NodeIterator getNodes() throws RepositoryException {
710         int childCount = nodeInfo().getChildrenCount();
711         if (childCount == 0) {
712             return new JcrEmptyNodeIterator();
713         }
714         List<AbstractJcrNode> matchingChildren = new LinkedList<AbstractJcrNode>();
715         for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren()) {
716             matchingChildren.add(child.getPayload().getJcrNode());
717         }
718         return new JcrChildNodeIterator(matchingChildren, childCount);
719     }
720 
721     /**
722      * {@inheritDoc}
723      * 
724      * @see javax.jcr.Node#getNodes(java.lang.String)
725      */
726     public NodeIterator getNodes( String namePattern ) throws RepositoryException {
727         CheckArg.isNotNull(namePattern, "namePattern");
728         namePattern = namePattern.trim();
729         if (namePattern.length() == 0) return new JcrEmptyNodeIterator();
730         if ("*".equals(namePattern)) return getNodes();
731         List<Object> patterns = createPatternsFor(namePattern);
732 
733         // Implementing exact-matching only for now to prototype types as properties
734         List<AbstractJcrNode> matchingChildren = new LinkedList<AbstractJcrNode>();
735         NamespaceRegistry registry = namespaces();
736         boolean foundMatch = false;
737         for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren()) {
738             String childName = child.getName().getString(registry);
739             for (Object patternOrMatch : patterns) {
740                 if (patternOrMatch instanceof Pattern) {
741                     Pattern pattern = (Pattern)patternOrMatch;
742                     if (pattern.matcher(childName).matches()) foundMatch = true;
743                 } else {
744                     String match = (String)patternOrMatch;
745                     if (childName.equals(match)) foundMatch = true;
746                 }
747                 if (foundMatch) {
748                     foundMatch = false;
749                     matchingChildren.add(child.getPayload().getJcrNode());
750                     break;
751                 }
752             }
753         }
754         return new JcrChildNodeIterator(matchingChildren, matchingChildren.size());
755     }
756 
757     /**
758      * {@inheritDoc}
759      * 
760      * @throws IllegalArgumentException if <code>visitor</code> is <code>null</code>.
761      * @see javax.jcr.Item#accept(javax.jcr.ItemVisitor)
762      */
763     public final void accept( ItemVisitor visitor ) throws RepositoryException {
764         CheckArg.isNotNull(visitor, "visitor");
765         visitor.visit(this);
766     }
767 
768     /**
769      * {@inheritDoc}
770      * <p>
771      * <b>ModeShape Implementation Notes</b>
772      * </p>
773      * <p>
774      * ModeShape imposes the following additional restrictions on the addition of mixin types in addition to the restrictions
775      * provided by the JCR 1.0 specification:
776      * <ol>
777      * <li>No properties defined by the mixin type can have the same name as any property defined by the node's primary type or
778      * any of its existing mixin types.</li>
779      * <li>No child nodes defined by the mixin type can have the same name as any child node defined by the node's primary type or
780      * any of its existing mixin types.</li>
781      * <li>If the node has a current residual definition for child nodes and/or properties, all nodes and properties that share a
782      * name with a child node definition or property definition from the new mixin type must be compatible with the definition
783      * provided by the new mixin type.</li>
784      * </ol>
785      * </p>
786      * 
787      * @see javax.jcr.Node#canAddMixin(java.lang.String)
788      */
789     public final boolean canAddMixin( String mixinName ) throws NoSuchNodeTypeException, RepositoryException {
790         CheckArg.isNotNull(mixinName, "mixinName");
791         CheckArg.isNotZeroLength(mixinName, "mixinName");
792 
793         /*
794          * Special workaround for SeralizationTest (and others) in JR TCK that incorrectly test whether a repository supports
795          * versioning by trying to add mix:versionable to a node.  The 1.0.1 says in section 4.11 that: 
796          * "A node is versionable if and only if it has been assigned the mixin type mix:versionable,
797          * otherwise it is nonversionable. Repositories that do not support versioning will simply not 
798          * provide this mixin type, whereas repositories that do support versioning must provide it."
799          */
800         if (JcrMixLexicon.VERSIONABLE.getString(namespaces()).equals(mixinName)) {
801             return false;
802         }
803 
804         JcrNodeType mixinCandidateType = cache.nodeTypes().getNodeType(mixinName);
805 
806         if (this.isLocked()) {
807             return false;
808         }
809 
810         if (this.getDefinition().isProtected()) {
811             return false;
812         }
813 
814         // TODO: Check access control when that support is added
815         // TODO: Throw VersionException if this node is versionable and checked in or unversionable and the nearest versionable
816         // ancestor is checked in
817 
818         NodeType primaryType = this.getPrimaryNodeType();
819         NodeType[] mixinTypes = this.getMixinNodeTypes();
820 
821         if (!mixinCandidateType.isMixin()) {
822             return false;
823         }
824 
825         if (mixinCandidateType.conflictsWith(primaryType, mixinTypes)) {
826             return false;
827         }
828 
829         // ------------------------------------------------------------------------------
830         // Check for any existing properties based on residual definitions that conflict
831         // ------------------------------------------------------------------------------
832         for (JcrPropertyDefinition propertyDefinition : mixinCandidateType.propertyDefinitions()) {
833             if (!hasProperty(propertyDefinition.getInternalName())) continue;
834             AbstractJcrProperty existingProp = cache.findJcrProperty(nodeId,
835                                                                      location.getPath(),
836                                                                      propertyDefinition.getInternalName());
837             if (existingProp != null) {
838                 if (propertyDefinition.isMultiple()) {
839                     if (!propertyDefinition.canCastToTypeAndSatisfyConstraints(existingProp.getValues())) {
840                         return false;
841                     }
842                 } else {
843                     if (!propertyDefinition.canCastToTypeAndSatisfyConstraints(existingProp.getValue())) {
844                         return false;
845                     }
846                 }
847             }
848         }
849 
850         // ------------------------------------------------------------------------------
851         // Check for any existing child nodes based on residual definitions that conflict
852         // ------------------------------------------------------------------------------
853         Set<Name> mixinChildNodeNames = new HashSet<Name>();
854         for (JcrNodeDefinition nodeDefinition : mixinCandidateType.childNodeDefinitions()) {
855             mixinChildNodeNames.add(nodeDefinition.getInternalName());
856         }
857 
858         for (Name nodeName : mixinChildNodeNames) {
859             // Need to figure out if the child node requires an SNS definition
860             int snsCount = nodeInfo().getChildrenCount(nodeName);
861             for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren(nodeName)) {
862                 JcrNodeDefinition match = this.cache.nodeTypes().findChildNodeDefinition(mixinCandidateType.getInternalName(),
863                                                                                          Collections.<Name>emptyList(),
864                                                                                          nodeName,
865                                                                                          child.getPayload().getPrimaryTypeName(),
866                                                                                          snsCount,
867                                                                                          false);
868 
869                 if (match == null) {
870                     return false;
871                 }
872             }
873 
874         }
875 
876         return true;
877     }
878 
879     /**
880      * {@inheritDoc}
881      * <p>
882      * <b>ModeShape Implementation Notes</b>
883      * </p>
884      * <p>
885      * The criteria noted in {@link #canAddMixin(String)} must be satisifed in addition to the criteria defined in the JCR 1.0
886      * specification.
887      * </p>
888      * 
889      * @see javax.jcr.Node#addMixin(java.lang.String)
890      */
891     public final void addMixin( String mixinName ) throws RepositoryException {
892         CheckArg.isNotNull(mixinName, "mixinName");
893         CheckArg.isNotZeroLength(mixinName, "mixinName");
894 
895         JcrNodeType mixinCandidateType = cache.nodeTypes().getNodeType(mixinName);
896 
897         // Check this separately since it throws a different type of exception
898         if (this.isLocked() && !holdsLock()) {
899             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
900         }
901 
902         if (!canAddMixin(mixinName)) {
903             throw new ConstraintViolationException(JcrI18n.cannotAddMixin.text(mixinName));
904         }
905 
906         this.editor().addMixin(mixinCandidateType);
907     }
908 
909     /**
910      * {@inheritDoc}
911      * <p>
912      * <b>ModeShape Implementation Notes</b>
913      * </p>
914      * <p>
915      * ModeShape allows the removal of a mixin type if and only if all of the node's existing child nodes and properties would
916      * still have a valid definition from the node's primary type or other mixin types. In practice, this means that either the
917      * node must have a residual definition compatible with any of the remaining child nodes or properties that currently use a
918      * definition from the to-be-removed mixin type or all of the child nodes and properties that use a definition from the
919      * to-be-removed mixin type must be removed prior to calling this method.
920      * </p>
921      * *
922      * 
923      * @see javax.jcr.Node#removeMixin(java.lang.String)
924      */
925     public final void removeMixin( String mixinName ) throws RepositoryException {
926 
927         if (this.isLocked() && !holdsLock()) {
928             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
929         }
930 
931         // TODO: Throw VersionException if this node is versionable and checked in or unversionable and the nearest versionable
932         // ancestor is checked in
933 
934         Property existingMixinProperty = getProperty(JcrLexicon.MIXIN_TYPES);
935 
936         if (existingMixinProperty == null) {
937             throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
938         }
939 
940         Value[] existingMixinValues = existingMixinProperty.getValues();
941 
942         if (existingMixinValues.length == 0) {
943             throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
944         }
945 
946         // ------------------------------------------------------------------------------
947         // Build the new list of mixin types
948         // ------------------------------------------------------------------------------
949 
950         int newMixinValuesCount = existingMixinValues.length - 1;
951         Value[] newMixinValues = new Value[newMixinValuesCount];
952         List<Name> newMixinNames = new ArrayList<Name>(newMixinValuesCount);
953         Name primaryTypeName = getPrimaryNodeType().getInternalName();
954 
955         int j = 0;
956         for (int i = 0; i < existingMixinValues.length; i++) {
957             if (!existingMixinValues[i].getString().equals(mixinName)) {
958                 if (j < newMixinValuesCount) {
959                     newMixinValues[j++] = existingMixinValues[i];
960                     newMixinNames.add(cache.nameFactory.create(existingMixinValues[i].getString()));
961                 } else {
962                     throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
963                 }
964             }
965         }
966 
967         // ------------------------------------------------------------------------------
968         // Check that any remaining properties that use the mixin type to be removed
969         // match the residual definition for the node.
970         // ------------------------------------------------------------------------------
971 
972         for (PropertyIterator iter = getProperties(); iter.hasNext();) {
973             Property property = iter.nextProperty();
974             if (mixinName.equals(property.getDefinition().getDeclaringNodeType().getName())) {
975                 JcrPropertyDefinition match;
976 
977                 // Only the residual definition would work - if there were any other definition for this name,
978                 // the mixin type would not have been added due to the conflict
979                 if (property.getDefinition().isMultiple()) {
980                     match = cache.nodeTypes().findPropertyDefinition(primaryTypeName,
981                                                                      newMixinNames,
982                                                                      JcrNodeType.RESIDUAL_NAME,
983                                                                      property.getValues(),
984                                                                      true);
985                 } else {
986                     match = cache.nodeTypes().findPropertyDefinition(primaryTypeName,
987                                                                      newMixinNames,
988                                                                      JcrNodeType.RESIDUAL_NAME,
989                                                                      property.getValue(),
990                                                                      true,
991                                                                      true);
992                 }
993 
994                 if (match == null) {
995                     throw new ConstraintViolationException(JcrI18n.noDefinition.text("property",
996                                                                                      property.getName(),
997                                                                                      getPath(),
998                                                                                      primaryTypeName,
999                                                                                      newMixinNames));
1000                 }
1001             }
1002         }
1003 
1004         // ------------------------------------------------------------------------------
1005         // Check that any remaining child nodes that use the mixin type to be removed
1006         // match the residual definition for the node.
1007         // ------------------------------------------------------------------------------
1008         for (NodeIterator iter = getNodes(); iter.hasNext();) {
1009             AbstractJcrNode node = (AbstractJcrNode)iter.nextNode();
1010             Name childNodeName = cache.nameFactory.create(node.getName());
1011             int snsCount = node.nodeInfo().getChildrenCount(childNodeName);
1012             if (mixinName.equals(node.getDefinition().getDeclaringNodeType().getName())) {
1013                 // Only the residual definition would work - if there were any other definition for this name,
1014                 // the mixin type would not have been added due to the conflict
1015                 JcrNodeDefinition match = cache.nodeTypes().findChildNodeDefinition(primaryTypeName,
1016                                                                                     newMixinNames,
1017                                                                                     JcrNodeType.RESIDUAL_NAME,
1018                                                                                     node.getPrimaryNodeType().getInternalName(),
1019                                                                                     snsCount,
1020                                                                                     true);
1021 
1022                 if (match == null) {
1023                     throw new ConstraintViolationException(JcrI18n.noDefinition.text("child node",
1024                                                                                      node.getName(),
1025                                                                                      getPath(),
1026                                                                                      primaryTypeName,
1027                                                                                      newMixinNames));
1028                 }
1029             }
1030         }
1031 
1032         editor().setProperty(JcrLexicon.MIXIN_TYPES, newMixinValues, PropertyType.NAME, false);
1033     }
1034 
1035     /**
1036      * {@inheritDoc}
1037      * 
1038      * @see javax.jcr.Node#addNode(java.lang.String)
1039      */
1040     public final javax.jcr.Node addNode( String relPath )
1041         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1042         RepositoryException {
1043         return addNode(relPath, null, null);
1044     }
1045 
1046     /**
1047      * {@inheritDoc}
1048      * 
1049      * @see javax.jcr.Node#addNode(java.lang.String, java.lang.String)
1050      */
1051     public final javax.jcr.Node addNode( String relPath,
1052                                          String primaryNodeTypeName )
1053         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1054         RepositoryException {
1055         return this.addNode(relPath, primaryNodeTypeName, null);
1056     }
1057 
1058     /**
1059      * Adds the a new node with the given primary type (if specified) at the given relative path with the given UUID (if
1060      * specified).
1061      * 
1062      * @param relPath the at which the new node should be created
1063      * @param primaryNodeTypeName the desired primary type for the new node; null value indicates that the default primary type
1064      *        from the appropriate definition for this node should be used
1065      * @param desiredUuid the UUID (for the jcr.uuid property) of this node; may be null
1066      * @return the newly created node
1067      * @throws ItemExistsException if an item at the specified path already exists and same-name siblings are not allowed.
1068      * @throws PathNotFoundException if the specified path implies intermediary nodes that do not exist.
1069      * @throws VersionException not thrown at this time, but included for compatibility with the specification
1070      * @throws ConstraintViolationException if the change would violate a node type or implementation-specific constraint.
1071      * @throws LockException not thrown at this time, but included for compatibility with the specification
1072      * @throws RepositoryException if another error occurs
1073      * @see #addNode(String, String)
1074      */
1075     final AbstractJcrNode addNode( String relPath,
1076                                    String primaryNodeTypeName,
1077                                    UUID desiredUuid )
1078         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1079         RepositoryException {
1080 
1081         if (isLocked() && !holdsLock()) {
1082             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1083         }
1084 
1085         // Determine the path ...
1086         NodeEditor editor = null;
1087         Path path = null;
1088         try {
1089             path = cache.pathFactory().create(relPath);
1090         } catch (org.modeshape.graph.property.ValueFormatException e) {
1091             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1092         }
1093         if (path.size() == 0) {
1094             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1095         }
1096         if (path.getLastSegment().getIndex() > 1 || relPath.endsWith("]")) {
1097             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1098         }
1099         if (path.size() > 1) {
1100             // The only segment in the path is the child name ...
1101             Path parentPath = path.getParent();
1102             try {
1103                 // Find the parent node ...
1104                 Node<JcrNodePayload, JcrPropertyPayload> parentOfNewNode = cache.findNode(nodeId, location.getPath(), parentPath);
1105                 editor = cache.getEditorFor(parentOfNewNode);
1106             } catch (RepositoryException e) {
1107                 // We're going to throw an exception ... the question is which one ...
1108                 try {
1109                     Node<JcrNodePayload, JcrPropertyPayload> grandparent;
1110                     if (parentPath.size() > 1) {
1111                         // Per the TCK, if relPath references a property, then we have to throw a ConstraintViolationException
1112                         // So, if we can't find the parent, try for the parent's parent and see if the last segment of the
1113                         // parent's
1114                         // path contains a property ...
1115                         Path grandparentPath = parentPath.getParent();
1116                         assert grandparentPath != null;
1117 
1118                         grandparent = cache.findNode(nodeId, location.getPath(), grandparentPath); // throws
1119                         // PathNotFoundException
1120                     } else {
1121                         grandparent = this.nodeInfo();
1122                     }
1123 
1124                     if (grandparent.getProperty(parentPath.getLastSegment().getName()) != null) {
1125                         // Need to throw a ConstraintViolationException since the request was to add a child to
1126                         // a property ...
1127                         throw new ConstraintViolationException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1128                     }
1129                 } catch (PathNotFoundException e2) {
1130                     // eat, since the original exception is what we want ...
1131                 }
1132 
1133                 // Otherwise, just throw the PathNotFoundException ...
1134                 throw e;
1135             }
1136         } else {
1137             assert path.size() == 1;
1138             editor = editor();
1139         }
1140         Name childName = path.getLastSegment().getName();
1141 
1142         // Determine the name for the primary node type
1143         Name childPrimaryTypeName = null;
1144         if (primaryNodeTypeName != null) {
1145             try {
1146                 childPrimaryTypeName = cache.nameFactory().create(primaryNodeTypeName);
1147             } catch (org.modeshape.graph.property.ValueFormatException e) {
1148                 throw new RepositoryException(JcrI18n.invalidNodeTypeNameParameter.text(primaryNodeTypeName,
1149                                                                                         "primaryNodeTypeName"));
1150             }
1151         }
1152 
1153         // Create the child ...
1154         return editor.createChild(childName, desiredUuid, childPrimaryTypeName);
1155     }
1156 
1157     protected final Property removeExistingValuedProperty( String name ) throws ConstraintViolationException, RepositoryException {
1158         AbstractJcrProperty property = cache.findJcrProperty(nodeId, location.getPath(), nameFrom(name));
1159         if (property != null) {
1160             property.remove();
1161             return property;
1162         }
1163         // else the property doesn't exist ...
1164         throw new RepositoryException(JcrI18n.propertyNotFoundOnNode.text(name, getPath(), cache.workspaceName()));
1165     }
1166 
1167     /**
1168      * {@inheritDoc}
1169      * 
1170      * @see javax.jcr.Node#setProperty(java.lang.String, boolean)
1171      */
1172     public final Property setProperty( String name,
1173                                        boolean value )
1174         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1175         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.BOOLEAN, value));
1176     }
1177 
1178     /**
1179      * {@inheritDoc}
1180      * 
1181      * @see javax.jcr.Node#setProperty(java.lang.String, java.util.Calendar)
1182      */
1183     public final Property setProperty( String name,
1184                                        Calendar value )
1185         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1186 
1187         if (value == null) {
1188             return removeExistingValuedProperty(name);
1189         }
1190 
1191         return editor().setProperty(nameFrom(name), valueFrom(value));
1192     }
1193 
1194     /**
1195      * {@inheritDoc}
1196      * 
1197      * @see javax.jcr.Node#setProperty(java.lang.String, double)
1198      */
1199     public final Property setProperty( String name,
1200                                        double value )
1201         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1202         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.DOUBLE, value));
1203     }
1204 
1205     /**
1206      * {@inheritDoc}
1207      * 
1208      * @see javax.jcr.Node#setProperty(java.lang.String, java.io.InputStream)
1209      */
1210     public final Property setProperty( String name,
1211                                        InputStream value )
1212         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1213         if (value == null) {
1214             return removeExistingValuedProperty(name);
1215         }
1216 
1217         return editor().setProperty(nameFrom(name), valueFrom(value));
1218     }
1219 
1220     /**
1221      * {@inheritDoc}
1222      * 
1223      * @see javax.jcr.Node#setProperty(java.lang.String, long)
1224      */
1225     public final Property setProperty( String name,
1226                                        long value )
1227         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1228         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.LONG, value));
1229     }
1230 
1231     /**
1232      * {@inheritDoc}
1233      * 
1234      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Node)
1235      */
1236     public final Property setProperty( String name,
1237                                        javax.jcr.Node value )
1238         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1239         if (value == null) {
1240             return removeExistingValuedProperty(name);
1241         }
1242 
1243         return editor().setProperty(nameFrom(name), valueFrom(value));
1244     }
1245 
1246     /**
1247      * {@inheritDoc}
1248      * 
1249      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String)
1250      */
1251     public final Property setProperty( String name,
1252                                        String value )
1253         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1254         if (value == null) {
1255             return removeExistingValuedProperty(name);
1256         }
1257 
1258         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.STRING, value));
1259     }
1260 
1261     /**
1262      * {@inheritDoc}
1263      * 
1264      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String, int)
1265      */
1266     public final Property setProperty( String name,
1267                                        String value,
1268                                        int type )
1269         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1270         if (value == null) {
1271             return removeExistingValuedProperty(name);
1272         }
1273 
1274         return editor().setProperty(nameFrom(name), valueFrom(type, value));
1275     }
1276 
1277     /**
1278      * {@inheritDoc}
1279      * 
1280      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[])
1281      */
1282     public final Property setProperty( String name,
1283                                        String[] values )
1284         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1285         if (values == null) {
1286             return removeExistingValuedProperty(name);
1287         }
1288 
1289         return editor().setProperty(nameFrom(name), valuesFrom(PropertyType.STRING, values), PropertyType.UNDEFINED);
1290     }
1291 
1292     /**
1293      * {@inheritDoc}
1294      * 
1295      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[], int)
1296      */
1297     public final Property setProperty( String name,
1298                                        String[] values,
1299                                        int type )
1300         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1301         if (values == null) {
1302             return removeExistingValuedProperty(name);
1303         }
1304 
1305         return editor().setProperty(nameFrom(name), valuesFrom(type, values), PropertyType.UNDEFINED);
1306     }
1307 
1308     /**
1309      * {@inheritDoc}
1310      * 
1311      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value)
1312      */
1313     public final Property setProperty( String name,
1314                                        Value value )
1315         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1316         if (value == null) {
1317             return removeExistingValuedProperty(name);
1318         }
1319 
1320         return editor().setProperty(nameFrom(name), (JcrValue)value);
1321     }
1322 
1323     /**
1324      * {@inheritDoc}
1325      * 
1326      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value, int)
1327      */
1328     public final Property setProperty( String name,
1329                                        Value value,
1330                                        int type )
1331         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1332         if (value == null) {
1333             return removeExistingValuedProperty(name);
1334         }
1335 
1336         return editor().setProperty(nameFrom(name), ((JcrValue)value).asType(type));
1337     }
1338 
1339     /**
1340      * {@inheritDoc}
1341      * 
1342      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[])
1343      */
1344     public final Property setProperty( String name,
1345                                        Value[] values )
1346         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1347         if (values == null) {
1348             // If there is an existing property, then remove it ...
1349             return removeExistingValuedProperty(name);
1350         }
1351 
1352         return setProperty(name, values, PropertyType.UNDEFINED);
1353     }
1354 
1355     /**
1356      * {@inheritDoc}
1357      * 
1358      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[], int)
1359      */
1360     public final Property setProperty( String name,
1361                                        Value[] values,
1362                                        int type )
1363         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1364         if (values == null) {
1365             // If there is an existing property, then remove it ...
1366             return removeExistingValuedProperty(name);
1367         }
1368 
1369         // Set the value, perhaps to an empty array ...
1370         return editor().setProperty(nameFrom(name), values, type);
1371     }
1372 
1373     /**
1374      * {@inheritDoc}
1375      * 
1376      * @return <code>false</code>
1377      * @see javax.jcr.Node#isCheckedOut()
1378      */
1379     public final boolean isCheckedOut() {
1380         return false;
1381     }
1382 
1383     /**
1384      * {@inheritDoc}
1385      * 
1386      * @throws UnsupportedRepositoryOperationException always
1387      * @see javax.jcr.Node#checkin()
1388      */
1389     public final Version checkin() throws UnsupportedRepositoryOperationException {
1390         throw new UnsupportedRepositoryOperationException();
1391     }
1392 
1393     /**
1394      * {@inheritDoc}
1395      * 
1396      * @throws UnsupportedRepositoryOperationException always
1397      * @see javax.jcr.Node#checkout()
1398      */
1399     public final void checkout() throws UnsupportedRepositoryOperationException {
1400         throw new UnsupportedRepositoryOperationException();
1401     }
1402 
1403     /**
1404      * {@inheritDoc}
1405      * 
1406      * @return <code>false</code>
1407      * @see javax.jcr.Node#holdsLock()
1408      */
1409     public final boolean holdsLock() /*throws RepositoryException*/{
1410         WorkspaceLockManager.ModeShapeLock lock = session().workspace().lockManager().lockFor(session(), this.location);
1411 
1412         return lock != null && cache.session().lockTokens().contains(lock.getLockToken());
1413     }
1414 
1415     /**
1416      * {@inheritDoc}
1417      * 
1418      * @return <code>false</code>
1419      * @see javax.jcr.Node#isLocked()
1420      */
1421     public final boolean isLocked() throws LockException, RepositoryException {
1422         return lock() != null;
1423     }
1424 
1425     /**
1426      * {@inheritDoc}
1427      * 
1428      * @see javax.jcr.Node#lock(boolean, boolean)
1429      */
1430     public final Lock lock( boolean isDeep,
1431                             boolean isSessionScoped ) throws LockException, RepositoryException {
1432         if (isLocked()) {
1433             throw new LockException(JcrI18n.alreadyLocked.text(this.location));
1434         }
1435 
1436         if (isDeep) {
1437             LinkedList<Node<JcrNodePayload, JcrPropertyPayload>> nodesToVisit = new LinkedList<Node<JcrNodePayload, JcrPropertyPayload>>();
1438             nodesToVisit.add(nodeInfo());
1439 
1440             while (!nodesToVisit.isEmpty()) {
1441                 Node<JcrNodePayload, JcrPropertyPayload> node = nodesToVisit.remove(nodesToVisit.size() - 1);
1442                 if (session().workspace().lockManager().lockFor(session(), node.getLocation()) != null) throw new LockException(
1443                                                                                                                                 JcrI18n.parentAlreadyLocked.text(this.location,
1444                                                                                                                                                                  node.getLocation()));
1445 
1446                 for (Node<JcrNodePayload, JcrPropertyPayload> child : node.getChildren()) {
1447                     nodesToVisit.add(child);
1448                 }
1449             }
1450         }
1451 
1452         WorkspaceLockManager.ModeShapeLock lock = session().workspace().lockManager().lock(session(),
1453                                                                                            this.location,
1454                                                                                            isDeep,
1455                                                                                            isSessionScoped);
1456 
1457         cache.session().addLockToken(lock.getLockToken());
1458         return lock.lockFor(cache);
1459     }
1460 
1461     /**
1462      * {@inheritDoc}
1463      * 
1464      * @see javax.jcr.Node#unlock()
1465      */
1466     public final void unlock() throws LockException, RepositoryException {
1467         WorkspaceLockManager.ModeShapeLock lock = session().workspace().lockManager().lockFor(session(), this.location);
1468 
1469         if (lock == null) {
1470             throw new LockException(JcrI18n.notLocked.text(this.location));
1471         }
1472 
1473         if (!session().lockTokens().contains(lock.getLockToken())) {
1474             try {
1475                 // See if the user has the permission to break someone else's lock
1476                 session().checkPermission(cache.workspaceName(), null, ModeShapePermissions.UNLOCK_ANY);
1477             } catch (AccessControlException iae) {
1478                 throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1479             }
1480         }
1481 
1482         session().workspace().lockManager().unlock(session().getExecutionContext(), lock);
1483         session().removeLockToken(lock.getLockToken());
1484     }
1485 
1486     private final WorkspaceLockManager.ModeShapeLock lock() throws RepositoryException {
1487         // This can only happen in mocked testing.
1488         if (session() == null || session().workspace() == null) return null;
1489 
1490         WorkspaceLockManager lockManager = session().workspace().lockManager();
1491         WorkspaceLockManager.ModeShapeLock lock = lockManager.lockFor(session(), this.location);
1492         if (lock != null) return lock;
1493 
1494         AbstractJcrNode parent = this;
1495         while (!parent.isRoot()) {
1496             parent = parent.getParent();
1497 
1498             WorkspaceLockManager.ModeShapeLock parentLock = lockManager.lockFor(session(), parent.location);
1499             if (parentLock != null && parentLock.isLive()) {
1500                 return parentLock.isDeep() ? parentLock : null;
1501             }
1502         }
1503         return null;
1504     }
1505 
1506     /**
1507      * {@inheritDoc}
1508      * 
1509      * @see javax.jcr.Node#getLock()
1510      */
1511     public final Lock getLock() throws LockException, RepositoryException {
1512         WorkspaceLockManager.ModeShapeLock lock = lock();
1513 
1514         if (lock == null) throw new LockException(JcrI18n.notLocked.text(this.location));
1515         return lock.lockFor(cache);
1516     }
1517 
1518     /**
1519      * {@inheritDoc}
1520      * 
1521      * @see javax.jcr.Item#isModified()
1522      */
1523     public final boolean isModified() {
1524         try {
1525             Node<JcrNodePayload, JcrPropertyPayload> node = nodeInfo();
1526             // Considered modified if *not* new but changed
1527             return !node.isNew() && node.isChanged(true);
1528         } catch (RepositoryException re) {
1529             throw new IllegalStateException(re);
1530         }
1531     }
1532 
1533     /**
1534      * {@inheritDoc}
1535      * 
1536      * @see javax.jcr.Item#isNew()
1537      */
1538     public final boolean isNew() {
1539         try {
1540             return nodeInfo().isNew();
1541         } catch (RepositoryException re) {
1542             throw new IllegalStateException(re);
1543         }
1544     }
1545 
1546     /**
1547      * {@inheritDoc}
1548      * 
1549      * @throws UnsupportedOperationException always
1550      * @see javax.jcr.Node#merge(java.lang.String, boolean)
1551      */
1552     public final NodeIterator merge( String srcWorkspace,
1553                                      boolean bestEffort ) {
1554         throw new UnsupportedOperationException();
1555     }
1556 
1557     /**
1558      * {@inheritDoc}
1559      * 
1560      * @throws UnsupportedOperationException always
1561      * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version)
1562      */
1563     public final void cancelMerge( Version version ) {
1564         throw new UnsupportedOperationException();
1565     }
1566 
1567     /**
1568      * {@inheritDoc}
1569      * 
1570      * @throws UnsupportedOperationException always
1571      * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version)
1572      */
1573     public final void doneMerge( Version version ) {
1574         throw new UnsupportedOperationException();
1575     }
1576 
1577     /**
1578      * {@inheritDoc}
1579      * 
1580      * @see javax.jcr.Node#getCorrespondingNodePath(java.lang.String)
1581      */
1582     public final String getCorrespondingNodePath( String workspaceName )
1583         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
1584         CheckArg.isNotNull(workspaceName, "workspace name");
1585         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
1586         return correspondingNodePath(workspaceName).getString(namespaces);
1587     }
1588 
1589     protected final Path correspondingNodePath( String workspaceName )
1590         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
1591         assert workspaceName != null;
1592         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
1593 
1594         // Find the closest ancestor (including this node) that is referenceable ...
1595         AbstractJcrNode referenceableRoot = this;
1596         while (!referenceableRoot.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(namespaces))) {
1597             referenceableRoot = referenceableRoot.getParent();
1598         }
1599 
1600         // Find the relative path from the nearest referenceable node to this node (or null if this node is referenceable) ...
1601         Path relativePath = path().equals(referenceableRoot.path()) ? null : path().relativeTo(referenceableRoot.path());
1602         UUID uuid = UUID.fromString(referenceableRoot.getUUID());
1603         return this.cache.getPathForCorrespondingNode(workspaceName, uuid, relativePath);
1604     }
1605 
1606     /**
1607      * {@inheritDoc}
1608      * 
1609      * @see javax.jcr.Node#update(java.lang.String)
1610      */
1611     public final void update( String srcWorkspaceName ) throws NoSuchWorkspaceException, RepositoryException {
1612         CheckArg.isNotNull(srcWorkspaceName, "workspace name");
1613 
1614         if (session().hasPendingChanges()) {
1615             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
1616         }
1617 
1618         Path correspondingPath = null;
1619         try {
1620             correspondingPath = correspondingNodePath(srcWorkspaceName);
1621         } catch (ItemNotFoundException infe) {
1622             return;
1623         }
1624 
1625         // Need to force remove in case this node is not referenceable
1626         cache.graphSession().immediateClone(correspondingPath, srcWorkspaceName, path(), true, true);
1627 
1628         session().refresh(false);
1629     }
1630 
1631     /**
1632      * {@inheritDoc}
1633      * 
1634      * @throws UnsupportedRepositoryOperationException always
1635      * @see javax.jcr.Node#getVersionHistory()
1636      */
1637     public final VersionHistory getVersionHistory() throws UnsupportedRepositoryOperationException {
1638         throw new UnsupportedRepositoryOperationException();
1639     }
1640 
1641     /**
1642      * {@inheritDoc}
1643      * 
1644      * @throws UnsupportedRepositoryOperationException always
1645      * @see javax.jcr.Node#getBaseVersion()
1646      */
1647     public final Version getBaseVersion() throws UnsupportedRepositoryOperationException {
1648         throw new UnsupportedRepositoryOperationException();
1649     }
1650 
1651     /**
1652      * {@inheritDoc}
1653      * 
1654      * @throws UnsupportedRepositoryOperationException always
1655      * @see javax.jcr.Node#restore(java.lang.String, boolean)
1656      */
1657     public final void restore( String versionName,
1658                                boolean removeExisting ) throws UnsupportedRepositoryOperationException {
1659         throw new UnsupportedRepositoryOperationException();
1660     }
1661 
1662     /**
1663      * {@inheritDoc}
1664      * 
1665      * @throws UnsupportedRepositoryOperationException always
1666      * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean)
1667      */
1668     public final void restore( Version version,
1669                                boolean removeExisting ) throws UnsupportedRepositoryOperationException {
1670         throw new UnsupportedRepositoryOperationException();
1671     }
1672 
1673     /**
1674      * {@inheritDoc}
1675      * 
1676      * @throws UnsupportedRepositoryOperationException always
1677      * @see javax.jcr.Node#restore(javax.jcr.version.Version, java.lang.String, boolean)
1678      */
1679     public final void restore( Version version,
1680                                String relPath,
1681                                boolean removeExisting ) throws UnsupportedRepositoryOperationException {
1682         throw new UnsupportedRepositoryOperationException();
1683     }
1684 
1685     /**
1686      * {@inheritDoc}
1687      * 
1688      * @throws UnsupportedRepositoryOperationException always
1689      * @see javax.jcr.Node#restoreByLabel(java.lang.String, boolean)
1690      */
1691     public final void restoreByLabel( String versionLabel,
1692                                       boolean removeExisting ) throws UnsupportedRepositoryOperationException {
1693         throw new UnsupportedRepositoryOperationException();
1694     }
1695 
1696     /**
1697      * {@inheritDoc}
1698      * 
1699      * @throws UnsupportedRepositoryOperationException always
1700      * @see javax.jcr.Node#orderBefore(java.lang.String, java.lang.String)
1701      */
1702     public final void orderBefore( String srcChildRelPath,
1703                                    String destChildRelPath ) throws UnsupportedRepositoryOperationException, RepositoryException {
1704         // This implementation is correct, except for not calling the SessionCache or graph layer to do the re-order
1705         if (!getPrimaryNodeType().hasOrderableChildNodes()) {
1706             throw new UnsupportedRepositoryOperationException(
1707                                                               JcrI18n.notOrderable.text(getPrimaryNodeType().getName(), getPath()));
1708         }
1709 
1710         PathFactory pathFactory = this.cache.pathFactory();
1711         Path srcPath = pathFactory.create(srcChildRelPath);
1712         if (srcPath.isAbsolute() || srcPath.size() != 1) {
1713             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath.getString(cache.context().getNamespaceRegistry()),
1714                                                                       cache.session().workspace().getName()));
1715         }
1716         // getLastSegment should return the only segment, since we verified that size() == 1
1717         Path.Segment sourceSegment = srcPath.getLastSegment();
1718         try {
1719             nodeInfo().getChild(sourceSegment);
1720         } catch (org.modeshape.graph.property.PathNotFoundException e) {
1721             String workspaceName = this.cache.session().getWorkspace().getName();
1722             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath, workspaceName));
1723         }
1724 
1725         Path.Segment destSegment = null;
1726 
1727         if (destChildRelPath != null) {
1728             Path destPath = pathFactory.create(destChildRelPath);
1729             if (destPath.isAbsolute() || destPath.size() != 1) {
1730                 throw new ItemNotFoundException(JcrI18n.pathNotFound.text(destPath.getString(cache.context()
1731                                                                                                   .getNamespaceRegistry()),
1732                                                                           cache.session().workspace().getName()));
1733             }
1734 
1735             destSegment = destPath.getLastSegment();
1736 
1737             // getLastSegment should return the only segment, since we verified that size() == 1
1738             try {
1739                 nodeInfo().getChild(destSegment);
1740             } catch (org.modeshape.graph.property.PathNotFoundException e) {
1741                 String workspaceName = this.cache.session().getWorkspace().getName();
1742                 throw new ItemNotFoundException(JcrI18n.pathNotFound.text(destPath, workspaceName));
1743             }
1744         }
1745 
1746         this.editor().orderChildBefore(sourceSegment, destSegment);
1747     }
1748 
1749     protected static List<Object> createPatternsFor( String namePattern ) throws RepositoryException {
1750         List<Object> patterns = new LinkedList<Object>();
1751         for (String stringPattern : namePattern.split("[|]")) {
1752             stringPattern = stringPattern.trim();
1753             int length = stringPattern.length();
1754             if (length == 0) continue;
1755             if (stringPattern.indexOf("*") == -1) {
1756                 // Doesn't use wildcard, so use String not Pattern
1757                 patterns.add(stringPattern);
1758             } else {
1759                 // We need to escape the regular expression characters ...
1760                 StringBuilder sb = new StringBuilder(length);
1761                 for (int i = 0; i != length; i++) {
1762                     char c = stringPattern.charAt(i);
1763                     switch (c) {
1764                         // Per the spec, the the following characters are not allowed in patterns:
1765                         case '/':
1766                         case '[':
1767                         case ']':
1768                         case '\'':
1769                         case '"':
1770                         case '|':
1771                         case '\t':
1772                         case '\n':
1773                         case '\r':
1774                             String msg = JcrI18n.invalidNamePattern.text(c, namePattern);
1775                             throw new RepositoryException(msg);
1776                             // The following characters must be escaped when used in regular expressions ...
1777                         case '?':
1778                         case '(':
1779                         case ')':
1780                         case '$':
1781                         case '^':
1782                         case '.':
1783                         case '{':
1784                         case '}':
1785                         case '\\':
1786                             sb.append("\\");
1787                             sb.append(c);
1788                             break;
1789                         case '*':
1790                             // replace with the regular expression wildcard
1791                             sb.append(".*");
1792                             break;
1793                         default:
1794                             sb.append(c);
1795                             break;
1796                     }
1797                 }
1798                 String escapedString = sb.toString();
1799                 Pattern pattern = Pattern.compile(escapedString);
1800                 patterns.add(pattern);
1801             }
1802         }
1803         return patterns;
1804     }
1805 
1806     /**
1807      * {@inheritDoc}
1808      * 
1809      * @see javax.jcr.Item#refresh(boolean)
1810      */
1811     public void refresh( boolean keepChanges ) throws RepositoryException {
1812         this.cache.refresh(this.nodeId, location.getPath(), keepChanges);
1813     }
1814 
1815     /**
1816      * {@inheritDoc}
1817      * 
1818      * @see javax.jcr.Item#save()
1819      */
1820     public void save() throws RepositoryException {
1821         session().checkReferentialIntegrityOfChanges();
1822         cache.save(nodeId, location.getPath());
1823     }
1824 
1825     @Override
1826     public String toString() {
1827 
1828         try {
1829             PropertyIterator iter = this.getProperties();
1830             StringBuffer propertyBuff = new StringBuffer();
1831             while (iter.hasNext()) {
1832                 AbstractJcrProperty prop = (AbstractJcrProperty)iter.nextProperty();
1833                 propertyBuff.append(prop).append(", ");
1834             }
1835             return this.getPath() + " {" + propertyBuff.toString() + "}";
1836         } catch (RepositoryException re) {
1837             return re.getMessage();
1838         }
1839     }
1840 
1841     /**
1842      * {@inheritDoc}
1843      * 
1844      * @see java.lang.Object#equals(java.lang.Object)
1845      */
1846     @Override
1847     public boolean equals( Object obj ) {
1848         if (obj == this) return true;
1849         if (obj instanceof AbstractJcrNode) {
1850             AbstractJcrNode that = (AbstractJcrNode)obj;
1851             if (this.cache != that.cache) return false;
1852             return this.location.equals(that.location);
1853         }
1854         return false;
1855     }
1856 }