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.math.BigDecimal;
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.Binary;
41  import javax.jcr.InvalidItemStateException;
42  import javax.jcr.InvalidLifecycleTransitionException;
43  import javax.jcr.Item;
44  import javax.jcr.ItemExistsException;
45  import javax.jcr.ItemNotFoundException;
46  import javax.jcr.ItemVisitor;
47  import javax.jcr.NoSuchWorkspaceException;
48  import javax.jcr.NodeIterator;
49  import javax.jcr.PathNotFoundException;
50  import javax.jcr.Property;
51  import javax.jcr.PropertyIterator;
52  import javax.jcr.PropertyType;
53  import javax.jcr.RepositoryException;
54  import javax.jcr.Session;
55  import javax.jcr.UnsupportedRepositoryOperationException;
56  import javax.jcr.Value;
57  import javax.jcr.ValueFormatException;
58  import javax.jcr.lock.Lock;
59  import javax.jcr.lock.LockException;
60  import javax.jcr.nodetype.ConstraintViolationException;
61  import javax.jcr.nodetype.NoSuchNodeTypeException;
62  import javax.jcr.nodetype.NodeDefinition;
63  import javax.jcr.nodetype.NodeType;
64  import javax.jcr.nodetype.NodeTypeManager;
65  import javax.jcr.query.Query;
66  import javax.jcr.query.QueryResult;
67  import javax.jcr.version.Version;
68  import javax.jcr.version.VersionException;
69  import net.jcip.annotations.Immutable;
70  import org.modeshape.common.i18n.I18n;
71  import org.modeshape.common.util.CheckArg;
72  import org.modeshape.common.util.HashCode;
73  import org.modeshape.graph.Location;
74  import org.modeshape.graph.connector.RepositorySourceException;
75  import org.modeshape.graph.property.DateTime;
76  import org.modeshape.graph.property.Name;
77  import org.modeshape.graph.property.NamespaceRegistry;
78  import org.modeshape.graph.property.Path;
79  import org.modeshape.graph.property.PathFactory;
80  import org.modeshape.graph.property.Reference;
81  import org.modeshape.graph.property.ValueFactories;
82  import org.modeshape.graph.query.QueryBuilder;
83  import org.modeshape.graph.query.model.QueryCommand;
84  import org.modeshape.graph.session.GraphSession.Node;
85  import org.modeshape.graph.session.GraphSession.NodeId;
86  import org.modeshape.graph.session.GraphSession.PropertyInfo;
87  import org.modeshape.jcr.SessionCache.JcrNodePayload;
88  import org.modeshape.jcr.SessionCache.JcrPropertyPayload;
89  import org.modeshape.jcr.SessionCache.NodeEditor;
90  
91  /**
92   * An abstract implementation of the JCR {@link javax.jcr.Node} interface. Instances of this class are created and managed by the
93   * {@link SessionCache}. Each instance indirectly references the {@link javax.jcr.Node node information} also managed by the
94   * SessionCache, and finds and operates against this information with each method call.
95   */
96  @Immutable
97  abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node {
98  
99      private static final NodeType[] EMPTY_NODE_TYPES = new NodeType[] {};
100     private static final Set<Name> INTERNAL_NODE_TYPE_NAMES = Collections.singleton(ModeShapeLexicon.SHARE);
101 
102     protected final NodeId nodeId;
103     protected Location location;
104 
105     AbstractJcrNode( SessionCache cache,
106                      NodeId nodeId,
107                      Location location ) {
108         super(cache);
109         this.nodeId = nodeId;
110         this.location = location;
111     }
112 
113     abstract boolean isRoot();
114 
115     public abstract AbstractJcrNode getParent() throws ItemNotFoundException, RepositoryException;
116 
117     final NodeId internalId() {
118         return nodeId;
119     }
120 
121     Location location() {
122         return location;
123     }
124 
125     void setLocation( Location location ) {
126         this.location = location;
127     }
128 
129     Path.Segment segment() throws RepositoryException {
130         return nodeInfo().getSegment();
131     }
132 
133     JcrLockManager lockManager() {
134         return session().lockManager();
135     }
136 
137     Node<JcrNodePayload, JcrPropertyPayload> nodeInfo()
138         throws InvalidItemStateException, AccessDeniedException, RepositoryException {
139         try {
140             // This method will find the node if the 'nodeId' is still valid and cached...
141             return cache.findNode(nodeId, location.getPath());
142         } catch (ItemNotFoundException infe) {
143             // However, if this node has changed (e.g., moved), the path may not be sufficient to find the node,
144             // so try to find by location ...
145             try {
146                 Node<JcrNodePayload, JcrPropertyPayload> info = null;
147                 if (location.getUuid() != null) {
148                     info = cache.findNodeWith(location.with((Path)null));
149                 } else {
150                     info = cache.findNodeWith(location);
151                 }
152                 // Update this node's location if required ...
153                 Location newLocation = info.getLocation();
154                 if (!newLocation.equals(location)) {
155                     location = newLocation;
156                 }
157                 return info;
158             } catch (ItemNotFoundException infe2) {
159                 // Use the first exception ...
160                 throw new InvalidItemStateException(infe.getMessage());
161             }
162         }
163     }
164 
165     Node<JcrNodePayload, JcrPropertyPayload> parentNodeInfo()
166         throws InvalidItemStateException, AccessDeniedException, RepositoryException {
167         return nodeInfo().getParent();
168     }
169 
170     NodeEditor editorForParent() throws RepositoryException {
171         try {
172             Node<JcrNodePayload, JcrPropertyPayload> parent = parentNodeInfo();
173             return cache.getEditorFor(parent);
174         } catch (ItemNotFoundException err) {
175             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
176             throw new RepositoryException(msg);
177         } catch (InvalidItemStateException err) {
178             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
179             throw new RepositoryException(msg);
180         }
181     }
182 
183     final NodeEditor editor() throws RepositoryException {
184         try {
185             return cache.getEditorFor(nodeId, location.getPath());
186         } catch (ItemNotFoundException err) {
187             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
188             throw new RepositoryException(msg);
189         } catch (InvalidItemStateException err) {
190             String msg = JcrI18n.nodeHasAlreadyBeenRemovedFromThisSession.text(nodeId, cache.workspaceName());
191             throw new RepositoryException(msg);
192         }
193     }
194 
195     final JcrValue valueFrom( int propertyType,
196                               Object value ) {
197         if (value instanceof JcrBinary) {
198             value = ((JcrBinary)value).binary();
199         }
200         return new JcrValue(cache.factories(), cache, propertyType, value);
201     }
202 
203     final JcrValue valueFrom( String value ) {
204         return new JcrValue(cache.factories(), cache, PropertyType.STRING, value);
205     }
206 
207     final JcrValue valueFrom( Calendar value ) {
208         ValueFactories factories = cache.factories();
209         DateTime dateTime = factories.getDateFactory().create(value);
210         return new JcrValue(factories, cache, PropertyType.DATE, dateTime);
211     }
212 
213     final JcrValue valueFrom( InputStream value ) {
214         ValueFactories factories = cache.factories();
215         org.modeshape.graph.property.Binary binary = factories.getBinaryFactory().create(value);
216         return new JcrValue(factories, cache, PropertyType.DATE, binary);
217     }
218 
219     final JcrValue valueFrom( Binary value ) {
220         ValueFactories factories = cache.factories();
221         org.modeshape.graph.property.Binary binary = ((JcrBinary)value).binary();
222         return new JcrValue(factories, cache, PropertyType.DATE, binary);
223     }
224 
225     final JcrValue valueFrom( javax.jcr.Node value ) throws UnsupportedRepositoryOperationException, RepositoryException {
226         ValueFactories factories = cache.factories();
227         Reference ref = factories.getReferenceFactory().create(value.getIdentifier());
228         return new JcrValue(factories, cache, PropertyType.REFERENCE, ref);
229     }
230 
231     final JcrValue[] valuesFrom( int propertyType,
232                                  Object[] values ) {
233         /*
234          * Null values in the array are "compacted" (read: ignored) as per section 7.1.6 in the JCR 1.0.1 specification. 
235          */
236         int len = values.length;
237         ValueFactories factories = cache.factories();
238         List<JcrValue> results = new ArrayList<JcrValue>(len);
239         for (int i = 0; i != len; ++i) {
240             if (values[i] != null) results.add(new JcrValue(factories, cache, propertyType, values[i]));
241         }
242         return results.toArray(new JcrValue[results.size()]);
243     }
244 
245     @Override
246     Path path() throws RepositoryException {
247         // Don't use the path in the location, since it may no longer be valid
248         return nodeInfo().getPath();
249     }
250 
251     boolean isReferenceable() throws RepositoryException {
252         return isNodeType(JcrMixLexicon.REFERENCEABLE);
253     }
254 
255     boolean isLockable() throws RepositoryException {
256         return isNodeType(JcrMixLexicon.LOCKABLE);
257     }
258 
259     boolean isShareable() throws RepositoryException {
260         return isNodeType(JcrMixLexicon.SHAREABLE);
261     }
262 
263     boolean isShared() {
264         return false;
265     }
266 
267     /**
268      * Get the UUID of this node, regardless of whether this node is referenceable.
269      * 
270      * @return the UUID of this node; never null
271      * @throws RepositoryException if there is an error accessing the UUID of the node
272      */
273     UUID uuid() throws RepositoryException {
274         UUID uuid = nodeInfo().getLocation().getUuid();
275         if (uuid == null) {
276             PropertyInfo<JcrPropertyPayload> uuidProp = nodeInfo().getProperty(JcrLexicon.UUID);
277             if (uuidProp == null) {
278                 uuidProp = nodeInfo().getProperty(ModeShapeLexicon.UUID);
279             }
280             assert uuidProp != null;
281             assert !uuidProp.getProperty().isEmpty();
282             uuid = context().getValueFactories().getUuidFactory().create(uuidProp.getProperty().getFirstValue());
283         }
284         assert uuid != null;
285         return uuid;
286     }
287 
288     /**
289      * Get the JCR 2.0-compatible identifier of this node, regardless of whether this node is referenceable.
290      * 
291      * @return the JCR 2.0 identifier of this node; never null
292      * @throws RepositoryException if there is an error accessing the identifier of the node
293      */
294     String identifier() throws RepositoryException {
295         String identifier = null;
296         UUID uuid = nodeInfo().getLocation().getUuid();
297         if (uuid == null) {
298             PropertyInfo<JcrPropertyPayload> uuidProp = nodeInfo().getProperty(JcrLexicon.UUID);
299             if (uuidProp == null) {
300                 uuidProp = nodeInfo().getProperty(ModeShapeLexicon.UUID);
301             }
302             if (uuidProp != null) {
303                 assert !uuidProp.getProperty().isEmpty();
304                 identifier = context().getValueFactories().getStringFactory().create(uuidProp.getProperty().getFirstValue());
305             } else {
306                 // There is no UUID property, then we need to return an identifier, so use the path ...
307                 identifier = getPath();
308             }
309         } else {
310             identifier = uuid.toString();
311         }
312         assert identifier != null;
313         return identifier;
314     }
315 
316     /**
317      * Get the absolute and normalized identifier path for this node, regardless of whether this node is referenceable.
318      * 
319      * @return the node's identifier path; never null
320      * @throws RepositoryException if there is an error accessing the identifier of this node
321      */
322     String identifierPath() throws RepositoryException {
323         return "[" + identifier() + "]";
324     }
325 
326     /**
327      * {@inheritDoc}
328      * 
329      * @see javax.jcr.Node#getUUID()
330      * @deprecated As of JCR 2.0, {@link #getIdentifier()} should be used instead.
331      */
332     @Deprecated
333     public String getUUID() throws RepositoryException {
334         // Return "jcr:uuid" only if node is referenceable
335         if (!isReferenceable()) {
336             throw new UnsupportedRepositoryOperationException(JcrI18n.nodeNotReferenceable.text());
337         }
338         return identifier();
339     }
340 
341     /**
342      * {@inheritDoc}
343      * 
344      * @see javax.jcr.Node#getIdentifier()
345      */
346     @Override
347     public String getIdentifier() throws RepositoryException {
348         return identifier();
349     }
350 
351     /**
352      * {@inheritDoc}
353      * 
354      * @return <code>true</code>
355      * @see javax.jcr.Item#isNode()
356      */
357     public final boolean isNode() {
358         return true;
359     }
360 
361     /**
362      * {@inheritDoc}
363      * 
364      * @return <code>false</code>
365      * @see javax.jcr.Node#isNodeType(java.lang.String)
366      */
367     public boolean isNodeType( String nodeTypeName ) throws RepositoryException {
368         return isNodeType(nameFrom(nodeTypeName));
369     }
370 
371     /**
372      * Determine whether this node's primary type or any of the mixins are or extend the node type with the supplied name. This
373      * method is semantically equivalent to but slightly more efficient than the {@link #isNodeType(String) equivalent in the JCR
374      * API}, especially when the node type name is already a {@link Name} object.
375      * 
376      * @param nodeTypeName the name of the node type
377      * @return true if this node is of the node type given by the supplied name, or false otherwise
378      * @throws RepositoryException if there is an exception
379      */
380     public final boolean isNodeType( Name nodeTypeName ) throws RepositoryException {
381         checkSession();
382         return cache.isNodeType(nodeInfo(), nodeTypeName);
383     }
384 
385     /**
386      * {@inheritDoc}
387      * 
388      * @see javax.jcr.Node#getDefinition()
389      */
390     public NodeDefinition getDefinition() throws RepositoryException {
391         checkSession();
392         NodeDefinitionId definitionId = nodeInfo().getPayload().getDefinitionId();
393         return session().nodeTypeManager().getNodeDefinition(definitionId);
394     }
395 
396     /**
397      * {@inheritDoc}
398      * 
399      * @see javax.jcr.Node#getPrimaryNodeType()
400      */
401     public JcrNodeType getPrimaryNodeType() throws RepositoryException {
402         checkSession();
403         return session().nodeTypeManager().getNodeType(getPrimaryTypeName());
404     }
405 
406     Name getPrimaryTypeName() throws RepositoryException {
407         return nodeInfo().getPayload().getPrimaryTypeName();
408     }
409 
410     /**
411      * {@inheritDoc}
412      * 
413      * @see javax.jcr.Node#getMixinNodeTypes()
414      */
415     public NodeType[] getMixinNodeTypes() throws RepositoryException {
416         checkSession();
417         NodeTypeManager nodeTypeManager = session().nodeTypeManager();
418         Property mixinTypesProperty = getProperty(JcrLexicon.MIXIN_TYPES);
419         if (mixinTypesProperty == null) return EMPTY_NODE_TYPES;
420         List<NodeType> mixinNodeTypes = new LinkedList<NodeType>();
421         for (Value value : mixinTypesProperty.getValues()) {
422             String nodeTypeName = value.getString();
423             NodeType nodeType = nodeTypeManager.getNodeType(nodeTypeName);
424             if (nodeType != null) mixinNodeTypes.add(nodeType);
425         }
426         return mixinNodeTypes.toArray(new NodeType[mixinNodeTypes.size()]);
427     }
428 
429     List<Name> getMixinTypeNames() throws RepositoryException {
430         return nodeInfo().getPayload().getMixinTypeNames();
431     }
432 
433     /**
434      * {@inheritDoc}
435      * 
436      * @see javax.jcr.Node#getPrimaryItem()
437      */
438     public final Item getPrimaryItem() throws RepositoryException {
439         checkSession();
440         // Get the primary item name from this node's type ...
441         NodeType primaryType = getPrimaryNodeType();
442         String primaryItemNameString = primaryType.getPrimaryItemName();
443         if (primaryItemNameString == null) {
444             I18n msg = JcrI18n.noPrimaryItemNameDefinedOnPrimaryType;
445             throw new ItemNotFoundException(msg.text(primaryType.getName(), getPath(), cache.workspaceName()));
446         }
447         try {
448             Path primaryItemPath = context().getValueFactories().getPathFactory().create(primaryItemNameString);
449             if (primaryItemPath.size() != 1 || primaryItemPath.isAbsolute()) {
450                 I18n msg = JcrI18n.primaryItemNameForPrimaryTypeIsNotValid;
451                 throw new ItemNotFoundException(msg.text(primaryType.getName(),
452                                                          primaryItemNameString,
453                                                          getPath(),
454                                                          cache.workspaceName()));
455             }
456             return cache.findJcrItem(nodeId, location.getPath(), primaryItemPath);
457         } catch (ValueFormatException error) {
458             I18n msg = JcrI18n.primaryItemNameForPrimaryTypeIsNotValid;
459             throw new ItemNotFoundException(msg.text(primaryType.getName(),
460                                                      primaryItemNameString,
461                                                      getPath(),
462                                                      cache.workspaceName()));
463         } catch (PathNotFoundException error) {
464             I18n msg = JcrI18n.primaryItemDoesNotExist;
465             throw new ItemNotFoundException(msg.text(primaryType.getName(),
466                                                      primaryItemNameString,
467                                                      getPath(),
468                                                      cache.workspaceName()));
469         }
470     }
471 
472     /**
473      * {@inheritDoc}
474      * 
475      * @throws IllegalArgumentException if <code>otherItem</code> is <code>null</code>.
476      * @see javax.jcr.Item#isSame(javax.jcr.Item)
477      */
478     @Override
479     public boolean isSame( Item otherItem ) throws RepositoryException {
480         CheckArg.isNotNull(otherItem, "otherItem");
481         checkSession();
482         if (super.isSame(otherItem) && otherItem instanceof javax.jcr.Node) {
483             if (otherItem instanceof AbstractJcrNode) {
484                 AbstractJcrNode that = (AbstractJcrNode)otherItem;
485                 if (this.isReferenceable() && that.isReferenceable()) {
486                     // Both are referenceable, so compare the UUIDs ...
487                     return getUUID().equals(((AbstractJcrNode)otherItem).getUUID());
488                 }
489 
490                 // One or both are not referenceable, so find the nearest ancestor that is referenceable.
491                 // The correspondence identifier (per Section 4.10.2 of JSR-170, version 1.0.1) for a
492                 // non-referenceable node is the pair of the UUID of the nearest referenceable ancestor and
493                 // the relative path from that referenceable ancestor. Per Section 6.2.8, two non-referenceable
494                 // nodes are the same if they have the same correspondence identifier.
495                 CorrespondenceId thisId = this.getCorrespondenceId();
496                 CorrespondenceId thatId = that.getCorrespondenceId();
497                 return thisId.equals(thatId);
498             }
499             // If not our implementation, let the other item figure out whether we are the same.
500             return otherItem.isSame(this);
501         }
502         return false;
503     }
504 
505     protected CorrespondenceId getCorrespondenceId() throws RepositoryException {
506         if (this.isReferenceable()) return new CorrespondenceId(getUUID());
507         assert !this.isRoot(); // the root must be referenceable
508 
509         // Find the nearest ancestor that is referenceable ...
510         Path currentPath = path();
511         AbstractJcrNode node = this.getParent();
512         int beginIndex = currentPath.size() - 1;
513         while (!node.isRoot() && !node.isReferenceable()) {
514             node = node.getParent();
515             --beginIndex;
516         }
517         // Get the relative path from the ancestor to this node ...
518         Path relativePath = currentPath.relativeTo(node.path());
519         assert !relativePath.isAbsolute();
520         return new CorrespondenceId(node.getUUID(), relativePath);
521     }
522 
523     /**
524      * {@inheritDoc}
525      * 
526      * @see javax.jcr.Node#hasProperties()
527      */
528     public final boolean hasProperties() throws RepositoryException {
529         checkSession();
530         return nodeInfo().getPropertyCount() > 0;
531     }
532 
533     /**
534      * {@inheritDoc}
535      * 
536      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
537      * @see javax.jcr.Node#hasProperty(java.lang.String)
538      */
539     public final boolean hasProperty( String relativePath ) throws RepositoryException {
540         CheckArg.isNotEmpty(relativePath, "relativePath");
541         checkSession();
542         if (relativePath.indexOf('/') >= 0 || relativePath.startsWith("[")) {
543             try {
544                 getProperty(relativePath);
545                 return true;
546             } catch (PathNotFoundException e) {
547                 return false;
548             }
549         }
550         if (relativePath.equals(".")) return false;
551         if (relativePath.equals("..")) return false;
552         // Otherwise it should be a property on this node ...
553         return nodeInfo().getProperty(nameFrom(relativePath)) != null;
554     }
555 
556     public final boolean hasProperty( Name name ) throws RepositoryException {
557         checkSession();
558         return nodeInfo().getProperty(name) != null;
559     }
560 
561     /**
562      * {@inheritDoc}
563      * 
564      * @see javax.jcr.Node#getProperties()
565      */
566     public PropertyIterator getProperties() throws RepositoryException {
567         checkSession();
568         return new JcrPropertyIterator(cache.findJcrPropertiesFor(nodeId, location.getPath()));
569     }
570 
571     /**
572      * {@inheritDoc}
573      * 
574      * @see javax.jcr.Node#getProperties(java.lang.String)
575      */
576     public PropertyIterator getProperties( String namePattern ) throws RepositoryException {
577         CheckArg.isNotNull(namePattern, "namePattern");
578         checkSession();
579         namePattern = namePattern.trim();
580         if (namePattern.length() == 0) return new JcrEmptyPropertyIterator();
581         if ("*".equals(namePattern)) {
582             Collection<AbstractJcrProperty> properties = cache.findJcrPropertiesFor(nodeId, location.getPath());
583             return new JcrPropertyIterator(properties);
584         }
585 
586         return getProperties(namePattern.split("[|]"));
587     }
588 
589     public PropertyIterator getProperties( String[] nameGlobs ) throws RepositoryException {
590         CheckArg.isNotNull(nameGlobs, "nameGlobs");
591         if (nameGlobs.length == 0) return new JcrEmptyPropertyIterator();
592         Collection<AbstractJcrProperty> properties = cache.findJcrPropertiesFor(nodeId, location.getPath());
593 
594         // Figure out the patterns for each of the different disjunctions in the supplied pattern ...
595         List<Object> patterns = createPatternsFor(nameGlobs);
596 
597         boolean foundMatch = false;
598         Collection<AbstractJcrProperty> matchingProperties = new LinkedList<AbstractJcrProperty>();
599         Iterator<AbstractJcrProperty> iter = properties.iterator();
600         while (iter.hasNext()) {
601             AbstractJcrProperty property = iter.next();
602             String propName = property.getName();
603             assert foundMatch == false;
604             for (Object patternOrMatch : patterns) {
605                 if (patternOrMatch instanceof Pattern) {
606                     Pattern pattern = (Pattern)patternOrMatch;
607                     if (pattern.matcher(propName).matches()) {
608                         foundMatch = true;
609                         break;
610                     }
611                 } else {
612                     String match = (String)patternOrMatch;
613                     if (propName.equals(match)) {
614                         foundMatch = true;
615                         break;
616                     }
617                 }
618             }
619             if (foundMatch) {
620                 matchingProperties.add(property);
621                 foundMatch = false; // for the next iteration ..
622             }
623         }
624         return new JcrPropertyIterator(matchingProperties);
625     }
626 
627     /**
628      * Obtain an iterator over the nodes that reference this node.
629      * 
630      * @param maxNumberOfNodes the maximum number of nodes that should be returned, or {@link Integer#MAX_VALUE} (or a negative
631      *        value) for all nodes
632      * @return the iterator over the referencing nodes; never null
633      * @throws RepositoryException if an error occurs while obtaining the information
634      */
635     protected final NodeIterator referencingNodes( int maxNumberOfNodes ) throws RepositoryException {
636         if (!this.isReferenceable()) {
637             return new JcrEmptyNodeIterator();
638         }
639         if (maxNumberOfNodes < 0) maxNumberOfNodes = Integer.MAX_VALUE;
640 
641         // Execute a query that will report all nodes referencing this node ...
642         String uuid = getUUID();
643         QueryBuilder builder = new QueryBuilder(context().getValueFactories().getTypeSystem());
644         QueryCommand query = builder.select("jcr:primaryType")
645                                     .fromAllNodesAs("allNodes")
646                                     .where()
647                                     .referenceValue("allNodes")
648                                     .isEqualTo(uuid)
649                                     .end()
650                                     .limit(maxNumberOfNodes)
651                                     .query();
652         Query jcrQuery = session().workspace().queryManager().createQuery(query);
653         QueryResult result = jcrQuery.execute();
654         return result.getNodes();
655     }
656 
657     /**
658      * Determine whether there is at least one other node that has a reference to this node.
659      * 
660      * @return true if this node is referenced by at least one other node, or false if there are no references to this node
661      * @throws RepositoryException if an error occurs while obtaining the information
662      */
663     protected final boolean hasIncomingReferences() throws RepositoryException {
664         return referencingNodes(1).hasNext();
665     }
666 
667     /**
668      * {@inheritDoc}
669      * 
670      * @see javax.jcr.Node#getReferences()
671      */
672     public final PropertyIterator getReferences() throws RepositoryException {
673         return getReferences(null);
674     }
675 
676     /**
677      * This method returns all REFERENCE properties that refer to this node, have the specified name and that are accessible
678      * through the current Session. If the name parameter is null then all referring REFERENCES are returned regardless of name.
679      * <p>
680      * Some implementations may only return properties that have been persisted. Some may return both properties that have been
681      * persisted and those that have been dispatched but not persisted (for example, those saved within a transaction but not yet
682      * committed) while others implementations may return these two categories of property as well as properties that are still
683      * pending and not yet dispatched.
684      * </p>
685      * <p>
686      * In implementations that support versioning, this method does not return properties that are part of the frozen state of a
687      * version in version storage.
688      * </p>
689      * <p>
690      * If this node has no referring REFERENCE properties with the specified name, an empty iterator is returned. This includes
691      * the case where this node is not referenceable.
692      * </p>
693      * 
694      * @param propertyName - name of referring REFERENCE properties to be returned; if null then all referring REFERENCEs are
695      *        returned.
696      * @return A PropertyIterator.
697      * @throws RepositoryException if an error occurs
698      * @see javax.jcr.Node#getReferences()
699      */
700     public final PropertyIterator getReferences( String propertyName ) throws RepositoryException {
701         checkSession();
702         return propertiesOnOtherNodesReferencingThis(propertyName, PropertyType.REFERENCE);
703     }
704 
705     /**
706      * {@inheritDoc}
707      * 
708      * @see javax.jcr.Node#getWeakReferences()
709      */
710     @Override
711     public PropertyIterator getWeakReferences() throws RepositoryException {
712         return getWeakReferences(null);
713     }
714 
715     /**
716      * {@inheritDoc}
717      * 
718      * @see javax.jcr.Node#getWeakReferences(java.lang.String)
719      */
720     @Override
721     public PropertyIterator getWeakReferences( String propertyName ) throws RepositoryException {
722         checkSession();
723         return propertiesOnOtherNodesReferencingThis(propertyName, PropertyType.WEAKREFERENCE);
724     }
725 
726     /**
727      * Find the properties on other nodes that are REFERENCE or WEAKREFERENCE properties (as dictated by the
728      * <code>referenceType</code> parameter) to this node.
729      * 
730      * @param propertyName the name of the referring REFERENCE or WEAKREFERENCE properties on the other nodes, or null if all
731      *        referring REFERENCE or WEAKREFERENCE properties should be returned
732      * @param referenceType either {@link PropertyType#REFERENCE} or {@link PropertyType#WEAKREFERENCE};
733      * @return the property iterator; never null by may be {@link JcrEmptyPropertyIterator empty} if there are no references or if
734      *         this node is not referenceable
735      * @throws RepositoryException if there is an error finding the referencing properties
736      */
737     protected PropertyIterator propertiesOnOtherNodesReferencingThis( String propertyName,
738                                                                       int referenceType ) throws RepositoryException {
739         if (!this.isReferenceable()) {
740             // This node is not referenceable, so it cannot have any references to it ...
741             return new JcrEmptyPropertyIterator();
742         }
743         NodeIterator iter = referencingNodes(Integer.MAX_VALUE);
744         if (!iter.hasNext()) {
745             return new JcrEmptyPropertyIterator();
746         }
747         // Use the identifier, not the UUID (since the getUUID() method just calls the getIdentifier() method) ...
748         String id = getIdentifier();
749         List<Property> references = new LinkedList<Property>();
750         while (iter.hasNext()) {
751             javax.jcr.Node node = iter.nextNode();
752 
753             // Go through the properties and look for reference properties that have a value of this node's UUID ...
754             PropertyIterator propIter = node.getProperties();
755             while (propIter.hasNext()) {
756                 Property prop = propIter.nextProperty();
757                 // Look at the definition's required type ...
758                 int propType = prop.getDefinition().getRequiredType();
759                 if (propType == referenceType || propType == PropertyType.UNDEFINED || propType == PropertyType.STRING) {
760                     if (propertyName != null && !propertyName.equals(prop.getName())) continue;
761                     if (prop.getDefinition().isMultiple()) {
762                         for (Value value : prop.getValues()) {
763                             if (id.equals(value.getString())) {
764                                 references.add(prop);
765                                 break;
766                             }
767                         }
768                     } else {
769                         Value value = prop.getValue();
770                         if (id.equals(value.getString())) {
771                             references.add(prop);
772                         }
773                     }
774                 }
775             }
776         }
777 
778         if (references.isEmpty()) return new JcrEmptyPropertyIterator();
779         return new JcrPropertyIterator(references);
780     }
781 
782     /**
783      * A non-standard method to obtain a property given the {@link Name ModeShape Name} object. This method is faster
784      * 
785      * @param propertyName the property name
786      * @return the JCR property with the supplied name, or null if the property doesn't exist
787      * @throws RepositoryException if there is an error finding the property with the supplied name
788      */
789     public final AbstractJcrProperty getProperty( Name propertyName ) throws RepositoryException {
790         AbstractJcrProperty property = cache.findJcrProperty(nodeId, location.getPath(), propertyName);
791         // Must be referenceable in order to return this property ...
792         if (property != null && JcrLexicon.UUID.equals(propertyName) && !isReferenceable()) return null;
793         return property;
794     }
795 
796     /**
797      * {@inheritDoc}
798      * 
799      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
800      * @see javax.jcr.Node#getProperty(java.lang.String)
801      */
802     public Property getProperty( String relativePath ) throws RepositoryException {
803         CheckArg.isNotEmpty(relativePath, "relativePath");
804         checkSession();
805         int indexOfFirstSlash = relativePath.indexOf('/');
806         if (indexOfFirstSlash == 0 || relativePath.startsWith("[")) {
807             // Not a relative path ...
808             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
809         }
810         Name propertyName = null;
811         if (indexOfFirstSlash != -1) {
812             // We know it's a relative path with more than one segment ...
813             Path path = pathFrom(relativePath).getNormalizedPath();
814             assert !path.isIdentifier();
815             if (path.size() > 1) {
816                 try {
817                     AbstractJcrItem item = cache.findJcrItem(nodeId, location.getPath(), path);
818                     if (item instanceof Property) {
819                         return (Property)item;
820                     }
821                 } catch (ItemNotFoundException e) {
822                     I18n msg = JcrI18n.propertyNotFoundAtPathRelativeToReferenceNode;
823                     throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
824                 }
825                 I18n msg = JcrI18n.propertyNotFoundAtPathRelativeToReferenceNode;
826                 throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
827             }
828             propertyName = path.getLastSegment().getName();
829         } else {
830             propertyName = nameFrom(relativePath);
831         }
832         // It's just a name, so look for it directly ...
833         Property result = getProperty(propertyName);
834         if (result != null) return result;
835         I18n msg = JcrI18n.pathNotFoundRelativeTo;
836         throw new PathNotFoundException(msg.text(relativePath, getPath(), cache.workspaceName()));
837     }
838 
839     /**
840      * {@inheritDoc}
841      * 
842      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
843      * @see javax.jcr.Node#hasNode(java.lang.String)
844      */
845     public final boolean hasNode( String relativePath ) throws RepositoryException {
846         CheckArg.isNotEmpty(relativePath, "relativePath");
847         checkSession();
848         if (relativePath.equals(".")) return true;
849         if (relativePath.equals("..")) return isRoot() ? false : true;
850         int indexOfFirstSlash = relativePath.indexOf('/');
851         if (indexOfFirstSlash == 0 || relativePath.startsWith("[")) {
852             // Not a relative path ...
853             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
854         }
855         if (indexOfFirstSlash != -1) {
856             Path path = pathFrom(relativePath).getNormalizedPath();
857             try {
858                 AbstractJcrNode item = cache.findJcrNode(nodeId, location.getPath(), path);
859                 return item != null;
860             } catch (PathNotFoundException e) {
861                 return false;
862             }
863         }
864         // It's just a name, so look for a child ...
865         try {
866             Path.Segment segment = segmentFrom(relativePath);
867             return nodeInfo().getChild(segment) != null;
868         } catch (org.modeshape.graph.property.PathNotFoundException e) {
869             return false;
870         }
871     }
872 
873     /**
874      * {@inheritDoc}
875      * 
876      * @see javax.jcr.Node#hasNodes()
877      */
878     public final boolean hasNodes() throws RepositoryException {
879         checkSession();
880         return nodeInfo().getChildrenCount() > 0;
881     }
882 
883     /**
884      * A non-standard method to obtain a child node given the {@link Name ModeShape Name} object. This method is faster
885      * 
886      * @param childNodeName the child node name
887      * @return the child node with the supplied name, or null if no child node exists with that name
888      * @throws RepositoryException if there is an error finding the child node with the supplied name
889      */
890     public final AbstractJcrNode getNode( Name childNodeName ) throws RepositoryException {
891         try {
892             Path childPath = context().getValueFactories().getPathFactory().createRelativePath(childNodeName);
893             return cache.findJcrNode(nodeId, location.getPath(), childPath);
894         } catch (PathNotFoundException infe) {
895             return null;
896         } catch (ItemNotFoundException infe) {
897             return null;
898         }
899     }
900 
901     /**
902      * A non-standard method to obtain a child node given the {@link Path ModeShape relative path} object.
903      * 
904      * @param relativePath the relative path
905      * @return the child node with the supplied name, or null if no child node exists with that name
906      * @throws RepositoryException if there is an error finding the child node with the supplied name
907      */
908     public final AbstractJcrNode getNode( Path relativePath ) throws RepositoryException {
909         return getNode(relativePath.getString(namespaces()));
910     }
911 
912     /**
913      * {@inheritDoc}
914      * 
915      * @throws IllegalArgumentException if <code>relativePath</code> is empty or <code>null</code>.
916      * @see javax.jcr.Node#getNode(java.lang.String)
917      */
918     public final AbstractJcrNode getNode( String relativePath ) throws RepositoryException {
919         CheckArg.isNotEmpty(relativePath, "relativePath");
920         checkSession();
921         if (relativePath.equals(".")) return this;
922         if (relativePath.equals("..")) return this.getParent();
923         int indexOfFirstSlash = relativePath.indexOf('/');
924         if (indexOfFirstSlash == 0 || relativePath.startsWith("[")) {
925             // Not a relative path ...
926             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(relativePath, "relativePath"));
927         }
928         Path.Segment segment = null;
929         if (indexOfFirstSlash != -1) {
930             // We know it's a relative path with more than one segment ...
931             Path path = pathFrom(relativePath).getNormalizedPath();
932             if (path.size() == 1) {
933                 if (path.getLastSegment().isSelfReference()) return this;
934                 if (path.getLastSegment().isParentReference()) return this.getParent();
935             }
936             // We know it's a resolved relative path with more than one segment ...
937             if (path.size() > 1) {
938                 return cache.findJcrNode(nodeId, location.getPath(), path);
939             }
940             segment = path.getLastSegment();
941         } else {
942             segment = segmentFrom(relativePath);
943         }
944         assert !segment.isIdentifier();
945         // It's just a name, so look for a child ...
946         try {
947             return nodeInfo().getChild(segment).getPayload().getJcrNode();
948         } catch (org.modeshape.graph.property.PathNotFoundException e) {
949             String msg = JcrI18n.childNotFoundUnderNode.text(segment, getPath(), cache.workspaceName());
950             throw new PathNotFoundException(msg);
951         } catch (RepositorySourceException e) {
952             throw new RepositoryException(e.getLocalizedMessage(), e);
953         }
954     }
955 
956     /**
957      * {@inheritDoc}
958      * 
959      * @see javax.jcr.Node#getNodes()
960      */
961     public final NodeIterator getNodes() throws RepositoryException {
962         checkSession();
963         int childCount = nodeInfo().getChildrenCount();
964         if (childCount == 0) {
965             return new JcrEmptyNodeIterator();
966         }
967         List<AbstractJcrNode> matchingChildren = new LinkedList<AbstractJcrNode>();
968         for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren()) {
969             matchingChildren.add(child.getPayload().getJcrNode());
970         }
971         return new JcrChildNodeIterator(matchingChildren, childCount);
972     }
973 
974     /**
975      * {@inheritDoc}
976      * 
977      * @see javax.jcr.Node#getNodes(java.lang.String)
978      */
979     public NodeIterator getNodes( String namePattern ) throws RepositoryException {
980         CheckArg.isNotNull(namePattern, "namePattern");
981         checkSession();
982         namePattern = namePattern.trim();
983         if (namePattern.length() == 0) return new JcrEmptyNodeIterator();
984         if ("*".equals(namePattern)) return getNodes();
985 
986         return getNodes(namePattern.split("[|]"));
987     }
988 
989     public NodeIterator getNodes( String[] nameGlobs ) throws RepositoryException {
990         CheckArg.isNotNull(nameGlobs, "nameGlobs");
991         if (nameGlobs.length == 0) return new JcrEmptyNodeIterator();
992 
993         List<Object> patterns = createPatternsFor(nameGlobs);
994 
995         List<AbstractJcrNode> matchingChildren = new LinkedList<AbstractJcrNode>();
996         NamespaceRegistry registry = namespaces();
997         boolean foundMatch = false;
998         for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren()) {
999             String childName = child.getName().getString(registry);
1000             for (Object patternOrMatch : patterns) {
1001                 if (patternOrMatch instanceof Pattern) {
1002                     Pattern pattern = (Pattern)patternOrMatch;
1003                     if (pattern.matcher(childName).matches()) foundMatch = true;
1004                 } else {
1005                     String match = (String)patternOrMatch;
1006                     if (childName.equals(match)) foundMatch = true;
1007                 }
1008                 if (foundMatch) {
1009                     foundMatch = false;
1010                     matchingChildren.add(child.getPayload().getJcrNode());
1011                     break;
1012                 }
1013             }
1014         }
1015         return new JcrChildNodeIterator(matchingChildren, matchingChildren.size());
1016     }
1017 
1018     /**
1019      * {@inheritDoc}
1020      * 
1021      * @throws IllegalArgumentException if <code>visitor</code> is <code>null</code>.
1022      * @see javax.jcr.Item#accept(javax.jcr.ItemVisitor)
1023      */
1024     public final void accept( ItemVisitor visitor ) throws RepositoryException {
1025         CheckArg.isNotNull(visitor, "visitor");
1026         checkSession();
1027         visitor.visit(this);
1028     }
1029 
1030     /**
1031      * {@inheritDoc}
1032      * <p>
1033      * <b>ModeShape Implementation Notes</b>
1034      * </p>
1035      * <p>
1036      * ModeShape imposes the following additional restrictions on the addition of mixin types in addition to the restrictions
1037      * provided by the JCR 1.0 and JCR 2.0 specifications:
1038      * <ol>
1039      * <li>No properties defined by the mixin type can have the same name as any property defined by the node's primary type or
1040      * any of its existing mixin types.</li>
1041      * <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
1042      * any of its existing mixin types.</li>
1043      * <li>If the node has a current residual definition for child nodes and/or properties, all nodes and properties that share a
1044      * name with a child node definition or property definition from the new mixin type must be compatible with the definition
1045      * provided by the new mixin type.</li>
1046      * </ol>
1047      * </p>
1048      * 
1049      * @see javax.jcr.Node#canAddMixin(java.lang.String)
1050      */
1051     public final boolean canAddMixin( String mixinName ) throws NoSuchNodeTypeException, RepositoryException {
1052         CheckArg.isNotNull(mixinName, "mixinName");
1053         CheckArg.isNotZeroLength(mixinName, "mixinName");
1054         checkSession();
1055 
1056         session().checkPermission(path(), ModeShapePermissions.SET_PROPERTY);
1057 
1058         JcrNodeType mixinCandidateType = cache.nodeTypes().getNodeType(mixinName);
1059 
1060         if (this.isLocked()) {
1061             return false;
1062         }
1063 
1064         if (!isCheckedOut()) {
1065             return false;
1066         }
1067 
1068         if (this.getDefinition().isProtected()) {
1069             return false;
1070         }
1071 
1072         if (mixinCandidateType.isAbstract()) {
1073             return false;
1074         }
1075 
1076         if (!mixinCandidateType.isMixin()) {
1077             return false;
1078         }
1079 
1080         if (isNodeType(mixinCandidateType.getInternalName())) return true;
1081 
1082         // ------------------------------------------------------------------------------
1083         // Check for any existing properties based on residual definitions that conflict
1084         // ------------------------------------------------------------------------------
1085         for (JcrPropertyDefinition propertyDefinition : mixinCandidateType.propertyDefinitions()) {
1086             if (!hasProperty(propertyDefinition.getInternalName())) continue;
1087             AbstractJcrProperty existingProp = cache.findJcrProperty(nodeId,
1088                                                                      location.getPath(),
1089                                                                      propertyDefinition.getInternalName());
1090             if (existingProp != null) {
1091                 if (propertyDefinition.isMultiple()) {
1092                     if (!propertyDefinition.canCastToTypeAndSatisfyConstraints(existingProp.getValues())) {
1093                         return false;
1094                     }
1095                 } else {
1096                     if (!propertyDefinition.canCastToTypeAndSatisfyConstraints(existingProp.getValue())) {
1097                         return false;
1098                     }
1099                 }
1100             }
1101         }
1102 
1103         // ------------------------------------------------------------------------------
1104         // Check for any existing child nodes based on residual definitions that conflict
1105         // ------------------------------------------------------------------------------
1106         Set<Name> mixinChildNodeNames = new HashSet<Name>();
1107         for (JcrNodeDefinition nodeDefinition : mixinCandidateType.childNodeDefinitions()) {
1108             mixinChildNodeNames.add(nodeDefinition.getInternalName());
1109         }
1110 
1111         for (Name nodeName : mixinChildNodeNames) {
1112             // Need to figure out if the child node requires an SNS definition
1113             int snsCount = nodeInfo().getChildrenCount(nodeName);
1114             for (Node<JcrNodePayload, JcrPropertyPayload> child : nodeInfo().getChildren(nodeName)) {
1115                 JcrNodeDefinition match = this.cache.nodeTypes().findChildNodeDefinition(mixinCandidateType.getInternalName(),
1116                                                                                          Collections.<Name>emptyList(),
1117                                                                                          nodeName,
1118                                                                                          child.getPayload().getPrimaryTypeName(),
1119                                                                                          snsCount,
1120                                                                                          false);
1121 
1122                 if (match == null) {
1123                     return false;
1124                 }
1125             }
1126 
1127         }
1128 
1129         return true;
1130     }
1131 
1132     /**
1133      * {@inheritDoc}
1134      * <p>
1135      * <b>ModeShape Implementation Notes</b>
1136      * </p>
1137      * <p>
1138      * The criteria noted in {@link #canAddMixin(String)} must be satisifed in addition to the criteria defined in the JCR 1.0 and
1139      * JCR 2.0 specifications.
1140      * </p>
1141      * 
1142      * @see javax.jcr.Node#addMixin(java.lang.String)
1143      */
1144     public final void addMixin( String mixinName ) throws RepositoryException {
1145         CheckArg.isNotNull(mixinName, "mixinName");
1146         CheckArg.isNotZeroLength(mixinName, "mixinName");
1147         checkSession();
1148 
1149         JcrNodeType mixinCandidateType = cache.nodeTypes().getNodeType(mixinName);
1150 
1151         // Check this separately since it throws a different type of exception
1152         if (this.isLocked() && !getLock().isLockOwningSession()) {
1153             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1154         }
1155 
1156         if (!isCheckedOut()) {
1157             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(getPath()));
1158         }
1159 
1160         if (!canAddMixin(mixinName)) {
1161             throw new ConstraintViolationException(JcrI18n.cannotAddMixin.text(mixinName));
1162         }
1163 
1164         if (isNodeType(mixinName)) return;
1165 
1166         this.editor().addMixin(mixinCandidateType);
1167     }
1168 
1169     /**
1170      * {@inheritDoc}
1171      * <p>
1172      * <b>ModeShape Implementation Notes</b>
1173      * </p>
1174      * <p>
1175      * ModeShape allows the removal of a mixin type if and only if all of the node's existing child nodes and properties would
1176      * still have a valid definition from the node's primary type or other mixin types. In practice, this means that either the
1177      * node must have a residual definition compatible with any of the remaining child nodes or properties that currently use a
1178      * definition from the to-be-removed mixin type or all of the child nodes and properties that use a definition from the
1179      * to-be-removed mixin type must be removed prior to calling this method.
1180      * </p>
1181      * *
1182      * 
1183      * @see javax.jcr.Node#removeMixin(java.lang.String)
1184      */
1185     public void removeMixin( String mixinName ) throws RepositoryException {
1186         checkSession();
1187 
1188         if (this.isLocked() && !getLock().isLockOwningSession()) {
1189             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1190         }
1191 
1192         if (!isCheckedOut()) {
1193             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(getPath()));
1194         }
1195 
1196         if (getDefinition().isProtected()) {
1197             throw new ConstraintViolationException(JcrI18n.cannotRemoveFromProtectedNode.text(getPath()));
1198         }
1199 
1200         Property existingMixinProperty = getProperty(JcrLexicon.MIXIN_TYPES);
1201 
1202         if (existingMixinProperty == null) {
1203             throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
1204         }
1205 
1206         Value[] existingMixinValues = existingMixinProperty.getValues();
1207 
1208         if (existingMixinValues.length == 0) {
1209             throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
1210         }
1211 
1212         // ------------------------------------------------------------------------------
1213         // Build the new list of mixin types
1214         // ------------------------------------------------------------------------------
1215 
1216         int newMixinValuesCount = existingMixinValues.length - 1;
1217         Value[] newMixinValues = new Value[newMixinValuesCount];
1218         List<Name> newMixinNames = new ArrayList<Name>(newMixinValuesCount);
1219         Name primaryTypeName = getPrimaryNodeType().getInternalName();
1220 
1221         int j = 0;
1222         for (int i = 0; i < existingMixinValues.length; i++) {
1223             if (!existingMixinValues[i].getString().equals(mixinName)) {
1224                 if (j < newMixinValuesCount) {
1225                     newMixinValues[j++] = existingMixinValues[i];
1226                     newMixinNames.add(cache.nameFactory.create(existingMixinValues[i].getString()));
1227                 } else {
1228                     throw new NoSuchNodeTypeException(JcrI18n.invalidMixinTypeForNode.text(mixinName, getPath()));
1229                 }
1230             }
1231         }
1232 
1233         // ------------------------------------------------------------------------------
1234         // Check that any remaining properties that use the mixin type to be removed
1235         // match the residual definition for the node.
1236         // ------------------------------------------------------------------------------
1237 
1238         for (PropertyIterator iter = getProperties(); iter.hasNext();) {
1239             Property property = iter.nextProperty();
1240             if (mixinName.equals(property.getDefinition().getDeclaringNodeType().getName())) {
1241                 JcrPropertyDefinition match;
1242 
1243                 // Only the residual definition would work - if there were any other definition for this name,
1244                 // the mixin type would not have been added due to the conflict
1245                 if (property.getDefinition().isMultiple()) {
1246                     match = cache.nodeTypes().findPropertyDefinition(primaryTypeName,
1247                                                                      newMixinNames,
1248                                                                      JcrNodeType.RESIDUAL_NAME,
1249                                                                      property.getValues(),
1250                                                                      true);
1251                 } else {
1252                     match = cache.nodeTypes().findPropertyDefinition(primaryTypeName,
1253                                                                      newMixinNames,
1254                                                                      JcrNodeType.RESIDUAL_NAME,
1255                                                                      property.getValue(),
1256                                                                      true,
1257                                                                      true);
1258                 }
1259 
1260                 if (match == null) {
1261                     throw new ConstraintViolationException(JcrI18n.noDefinition.text("property",
1262                                                                                      property.getName(),
1263                                                                                      getPath(),
1264                                                                                      primaryTypeName,
1265                                                                                      newMixinNames));
1266                 }
1267             }
1268         }
1269 
1270         // ------------------------------------------------------------------------------
1271         // Check that any remaining child nodes that use the mixin type to be removed
1272         // match the residual definition for the node.
1273         // ------------------------------------------------------------------------------
1274         for (NodeIterator iter = getNodes(); iter.hasNext();) {
1275             AbstractJcrNode node = (AbstractJcrNode)iter.nextNode();
1276             Name childNodeName = cache.nameFactory.create(node.getName());
1277             int snsCount = node.nodeInfo().getChildrenCount(childNodeName);
1278             if (mixinName.equals(node.getDefinition().getDeclaringNodeType().getName())) {
1279                 // Only the residual definition would work - if there were any other definition for this name,
1280                 // the mixin type would not have been added due to the conflict
1281                 JcrNodeDefinition match = cache.nodeTypes().findChildNodeDefinition(primaryTypeName,
1282                                                                                     newMixinNames,
1283                                                                                     JcrNodeType.RESIDUAL_NAME,
1284                                                                                     node.getPrimaryNodeType().getInternalName(),
1285                                                                                     snsCount,
1286                                                                                     true);
1287 
1288                 if (match == null) {
1289                     throw new ConstraintViolationException(JcrI18n.noDefinition.text("child node",
1290                                                                                      node.getName(),
1291                                                                                      getPath(),
1292                                                                                      primaryTypeName,
1293                                                                                      newMixinNames));
1294                 }
1295             }
1296         }
1297 
1298         editor().setProperty(JcrLexicon.MIXIN_TYPES, newMixinValues, PropertyType.NAME, false);
1299     }
1300 
1301     /**
1302      * Attempts to change the primary type of this node. Not yet supported, but some error checking is added.
1303      * 
1304      * @param nodeTypeName the name of the new primary type for this node
1305      * @throws NoSuchNodeTypeException if no node with the given name exists
1306      * @throws VersionException if this node is versionable and checked-in or is non-versionable but its nearest versionable
1307      *         ancestor is checked-in.
1308      * @throws ConstraintViolationException if existing child nodes or properties (or the lack of sufficient child nodes or
1309      *         properties) prevent this node from satisfying the definition of the new primary type
1310      * @throws LockException if a lock prevents the modification of the primary type
1311      * @throws RepositoryException if any other error occurs
1312      */
1313     public void setPrimaryType( String nodeTypeName )
1314         throws NoSuchNodeTypeException, VersionException, ConstraintViolationException, LockException, RepositoryException {
1315         checkSession();
1316 
1317         if (this.isLocked() && !getLock().isLockOwningSession()) {
1318             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1319         }
1320 
1321         if (!isCheckedOut()) {
1322             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(getPath()));
1323         }
1324 
1325         JcrNodeType nodeType = session().nodeTypeManager().getNodeType(nodeTypeName);
1326 
1327         if (nodeType.equals(getPrimaryNodeType())) return;
1328 
1329         if (nodeType.isMixin()) {
1330             throw new ConstraintViolationException(JcrI18n.cannotUseMixinTypeAsPrimaryType.text(nodeTypeName));
1331         }
1332 
1333         throw new ConstraintViolationException(JcrI18n.setPrimaryTypeNotSupported.text());
1334     }
1335 
1336     /**
1337      * {@inheritDoc}
1338      * 
1339      * @see javax.jcr.Node#addNode(java.lang.String)
1340      */
1341     public final javax.jcr.Node addNode( String relPath )
1342         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1343         RepositoryException {
1344         return addNode(relPath, null, null);
1345     }
1346 
1347     /**
1348      * {@inheritDoc}
1349      * 
1350      * @see javax.jcr.Node#addNode(java.lang.String, java.lang.String)
1351      */
1352     public final javax.jcr.Node addNode( String relPath,
1353                                          String primaryNodeTypeName )
1354         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1355         RepositoryException {
1356         return this.addNode(relPath, primaryNodeTypeName, null);
1357     }
1358 
1359     /**
1360      * Adds the a new node with the given primary type (if specified) at the given relative path with the given UUID (if
1361      * specified).
1362      * 
1363      * @param relPath the at which the new node should be created
1364      * @param primaryNodeTypeName the desired primary type for the new node; null value indicates that the default primary type
1365      *        from the appropriate definition for this node should be used
1366      * @param desiredUuid the UUID (for the jcr.uuid property) of this node; may be null
1367      * @return the newly created node
1368      * @throws ItemExistsException if an item at the specified path already exists and same-name siblings are not allowed.
1369      * @throws PathNotFoundException if the specified path implies intermediary nodes that do not exist.
1370      * @throws VersionException not thrown at this time, but included for compatibility with the specification
1371      * @throws ConstraintViolationException if the change would violate a node type or implementation-specific constraint.
1372      * @throws LockException not thrown at this time, but included for compatibility with the specification
1373      * @throws RepositoryException if another error occurs
1374      * @see #addNode(String, String)
1375      */
1376     final AbstractJcrNode addNode( String relPath,
1377                                    String primaryNodeTypeName,
1378                                    UUID desiredUuid )
1379         throws ItemExistsException, PathNotFoundException, VersionException, ConstraintViolationException, LockException,
1380         RepositoryException {
1381         checkSession();
1382 
1383         if (isLocked() && !getLock().isLockOwningSession()) {
1384             throw new LockException(JcrI18n.lockTokenNotHeld.text(this.location));
1385         }
1386 
1387         // Determine the path ...
1388         NodeEditor editor = null;
1389         Path path = null;
1390         try {
1391             path = cache.pathFactory().create(relPath);
1392         } catch (org.modeshape.graph.property.ValueFormatException e) {
1393             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1394         }
1395         if (path.size() == 0) {
1396             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1397         }
1398         if (path.isIdentifier()) {
1399             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1400         }
1401         if (path.getLastSegment().getIndex() > 1 || relPath.endsWith("]")) {
1402             throw new RepositoryException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1403         }
1404         if (path.size() > 1) {
1405             // The only segment in the path is the child name ...
1406             Path parentPath = path.getParent();
1407             try {
1408                 // Find the parent node ...
1409                 Node<JcrNodePayload, JcrPropertyPayload> parentOfNewNode = cache.findNode(nodeId, location.getPath(), parentPath);
1410                 editor = cache.getEditorFor(parentOfNewNode);
1411             } catch (RepositoryException e) {
1412                 // We're going to throw an exception ... the question is which one ...
1413                 try {
1414                     Node<JcrNodePayload, JcrPropertyPayload> grandparent;
1415                     if (parentPath.size() > 1) {
1416                         // Per the TCK, if relPath references a property, then we have to throw a ConstraintViolationException
1417                         // So, if we can't find the parent, try for the parent's parent and see if the last segment of the
1418                         // parent's
1419                         // path contains a property ...
1420                         Path grandparentPath = parentPath.getParent();
1421                         assert grandparentPath != null;
1422 
1423                         grandparent = cache.findNode(nodeId, location.getPath(), grandparentPath); // throws
1424                         // PathNotFoundException
1425                     } else {
1426                         grandparent = this.nodeInfo();
1427                     }
1428 
1429                     if (grandparent.getProperty(parentPath.getLastSegment().getName()) != null) {
1430                         // Need to throw a ConstraintViolationException since the request was to add a child to
1431                         // a property ...
1432                         throw new ConstraintViolationException(JcrI18n.invalidPathParameter.text(relPath, "relPath"));
1433                     }
1434                 } catch (PathNotFoundException e2) {
1435                     // eat, since the original exception is what we want ...
1436                 }
1437 
1438                 // Otherwise, just throw the PathNotFoundException ...
1439                 throw e;
1440             }
1441         } else {
1442             assert path.size() == 1;
1443             editor = editor();
1444         }
1445         Name childName = path.getLastSegment().getName();
1446 
1447         // Determine the name for the primary node type
1448         Name childPrimaryTypeName = null;
1449         if (primaryNodeTypeName != null) {
1450             try {
1451                 childPrimaryTypeName = cache.nameFactory().create(primaryNodeTypeName);
1452             } catch (org.modeshape.graph.property.ValueFormatException e) {
1453                 throw new RepositoryException(JcrI18n.invalidNodeTypeNameParameter.text(primaryNodeTypeName,
1454                                                                                         "primaryNodeTypeName"));
1455             }
1456             if (INTERNAL_NODE_TYPE_NAMES.contains(childPrimaryTypeName)) {
1457                 String workspaceName = getSession().getWorkspace().getName();
1458                 String childPath = cache.readable(path);
1459                 throw new ConstraintViolationException(
1460                                                        JcrI18n.unableToCreateNodeWithInternalPrimaryType.text(primaryNodeTypeName,
1461                                                                                                               childPath,
1462                                                                                                               workspaceName));
1463 
1464             }
1465         }
1466 
1467         // Create the child ...
1468         return editor.createChild(childName, desiredUuid, childPrimaryTypeName);
1469     }
1470 
1471     /**
1472      * Performs a "best effort" check on whether a node can be added at the given relative path from this node with the given
1473      * primary node type (if one is specified).
1474      * <p>
1475      * Note that a result of {@code true} from this method does not guarantee that a call to {@code #addNode(String, String)} with
1476      * the same arguments will succeed, but a result of {@code false} guarantees that it would fail (assuming that the current
1477      * repository state does not change).
1478      * </p>
1479      * 
1480      * @param relPath the relative path at which the node would be added; may not be null
1481      * @param primaryNodeTypeName the primary type that would be used for the node; null indicates that a default primary type
1482      *        should be used if possible
1483      * @return false if the node could not be added for any reason; true if the node <i>might</i> be able to be added
1484      * @throws RepositoryException if an error occurs accessing the repository
1485      */
1486     final boolean canAddNode( String relPath,
1487                               String primaryNodeTypeName ) throws RepositoryException {
1488         CheckArg.isNotEmpty(relPath, relPath);
1489         checkSession();
1490 
1491         if (isLocked() && !getLock().isLockOwningSession()) {
1492             return false;
1493         }
1494 
1495         // Determine the path ...
1496         Path path = null;
1497         try {
1498             path = cache.pathFactory().create(relPath);
1499         } catch (org.modeshape.graph.property.ValueFormatException e) {
1500             return false;
1501         }
1502         if (path.size() == 0) {
1503             return false;
1504         }
1505         if (path.isIdentifier()) {
1506             return false;
1507         }
1508         if (path.getLastSegment().getIndex() > 1 || relPath.endsWith("]")) {
1509             return false;
1510         }
1511         if (path.size() > 1) {
1512             // The only segment in the path is the child name ...
1513             Path parentPath = path.getParent();
1514             try {
1515                 // Find the parent node ...
1516                 cache.findNode(nodeId, location.getPath(), parentPath);
1517             } catch (RepositoryException e) {
1518                 return false;
1519             }
1520         }
1521 
1522         // Determine the name for the primary node type
1523         if (primaryNodeTypeName != null) {
1524             if (!session().nodeTypeManager().hasNodeType(primaryNodeTypeName)) return false;
1525 
1526             JcrNodeType nodeType = session().nodeTypeManager().getNodeType(primaryNodeTypeName);
1527             if (nodeType.isAbstract()) return false;
1528             if (nodeType.isMixin()) return false;
1529             if (INTERNAL_NODE_TYPE_NAMES.contains(nodeType.getInternalName())) return false;
1530         }
1531 
1532         return true;
1533     }
1534 
1535     protected final Property removeExistingValuedProperty( String name ) throws ConstraintViolationException, RepositoryException {
1536         AbstractJcrProperty property = cache.findJcrProperty(nodeId, location.getPath(), nameFrom(name));
1537         if (property != null) {
1538             property.remove();
1539             return property;
1540         }
1541         // else the property doesn't exist ...
1542         throw new RepositoryException(JcrI18n.propertyNotFoundOnNode.text(name, getPath(), cache.workspaceName()));
1543     }
1544 
1545     /**
1546      * {@inheritDoc}
1547      * 
1548      * @see javax.jcr.Node#setProperty(java.lang.String, boolean)
1549      */
1550     public final Property setProperty( String name,
1551                                        boolean value )
1552         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1553         checkSession();
1554         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.BOOLEAN, value));
1555     }
1556 
1557     /**
1558      * {@inheritDoc}
1559      * 
1560      * @see javax.jcr.Node#setProperty(java.lang.String, Binary)
1561      */
1562     @Override
1563     public Property setProperty( String name,
1564                                  Binary value )
1565         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1566         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.BINARY, value));
1567     }
1568 
1569     /**
1570      * {@inheritDoc}
1571      * 
1572      * @see javax.jcr.Node#setProperty(java.lang.String, BigDecimal)
1573      */
1574     @Override
1575     public Property setProperty( String name,
1576                                  BigDecimal value )
1577         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1578         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.DECIMAL, value));
1579     }
1580 
1581     /**
1582      * {@inheritDoc}
1583      * 
1584      * @see javax.jcr.Node#setProperty(java.lang.String, java.util.Calendar)
1585      */
1586     public final Property setProperty( String name,
1587                                        Calendar value )
1588         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1589         checkSession();
1590 
1591         if (value == null) {
1592             return removeExistingValuedProperty(name);
1593         }
1594 
1595         return editor().setProperty(nameFrom(name), valueFrom(value));
1596     }
1597 
1598     /**
1599      * {@inheritDoc}
1600      * 
1601      * @see javax.jcr.Node#setProperty(java.lang.String, double)
1602      */
1603     public final Property setProperty( String name,
1604                                        double value )
1605         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1606         checkSession();
1607         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.DOUBLE, value));
1608     }
1609 
1610     /**
1611      * {@inheritDoc}
1612      * 
1613      * @see javax.jcr.Node#setProperty(java.lang.String, java.io.InputStream)
1614      */
1615     public final Property setProperty( String name,
1616                                        InputStream value )
1617         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1618         checkSession();
1619         if (value == null) {
1620             return removeExistingValuedProperty(name);
1621         }
1622 
1623         return editor().setProperty(nameFrom(name), valueFrom(value));
1624     }
1625 
1626     /**
1627      * {@inheritDoc}
1628      * 
1629      * @see javax.jcr.Node#setProperty(java.lang.String, long)
1630      */
1631     public final Property setProperty( String name,
1632                                        long value )
1633         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1634         checkSession();
1635         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.LONG, value));
1636     }
1637 
1638     /**
1639      * {@inheritDoc}
1640      * 
1641      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Node)
1642      */
1643     public final Property setProperty( String name,
1644                                        javax.jcr.Node value )
1645         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1646         checkSession();
1647         if (value == null) {
1648             return removeExistingValuedProperty(name);
1649         }
1650 
1651         return editor().setProperty(nameFrom(name), valueFrom(value));
1652     }
1653 
1654     /**
1655      * {@inheritDoc}
1656      * 
1657      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String)
1658      */
1659     public final Property setProperty( String name,
1660                                        String value )
1661         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1662         checkSession();
1663         if (value == null) {
1664             return removeExistingValuedProperty(name);
1665         }
1666 
1667         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.STRING, value));
1668     }
1669 
1670     /**
1671      * {@inheritDoc}
1672      * 
1673      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String, int)
1674      */
1675     public final Property setProperty( String name,
1676                                        String value,
1677                                        int type )
1678         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1679         checkSession();
1680         if (value == null) {
1681             return removeExistingValuedProperty(name);
1682         }
1683 
1684         return editor().setProperty(nameFrom(name), valueFrom(type, value));
1685     }
1686 
1687     /**
1688      * {@inheritDoc}
1689      * 
1690      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[])
1691      */
1692     public final Property setProperty( String name,
1693                                        String[] values )
1694         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1695         checkSession();
1696         if (values == null) {
1697             return removeExistingValuedProperty(name);
1698         }
1699 
1700         return editor().setProperty(nameFrom(name), valuesFrom(PropertyType.STRING, values), PropertyType.UNDEFINED);
1701     }
1702 
1703     /**
1704      * {@inheritDoc}
1705      * 
1706      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[], int)
1707      */
1708     public final Property setProperty( String name,
1709                                        String[] values,
1710                                        int type )
1711         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1712         checkSession();
1713         if (values == null) {
1714             return removeExistingValuedProperty(name);
1715         }
1716 
1717         return editor().setProperty(nameFrom(name), valuesFrom(type, values), PropertyType.UNDEFINED);
1718     }
1719 
1720     /**
1721      * {@inheritDoc}
1722      * 
1723      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value)
1724      */
1725     public final Property setProperty( String name,
1726                                        Value value )
1727         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1728         checkSession();
1729         if (value == null) {
1730             return removeExistingValuedProperty(name);
1731         }
1732 
1733         return editor().setProperty(nameFrom(name), (JcrValue)value);
1734     }
1735 
1736     /**
1737      * {@inheritDoc}
1738      * 
1739      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value, int)
1740      */
1741     public final Property setProperty( String name,
1742                                        Value value,
1743                                        int type )
1744         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1745         checkSession();
1746         if (value == null) {
1747             return removeExistingValuedProperty(name);
1748         }
1749 
1750         return editor().setProperty(nameFrom(name), ((JcrValue)value).asType(type));
1751     }
1752 
1753     /**
1754      * {@inheritDoc}
1755      * 
1756      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[])
1757      */
1758     public final Property setProperty( String name,
1759                                        Value[] values )
1760         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1761         checkSession();
1762         if (values == null) {
1763             // If there is an existing property, then remove it ...
1764             return removeExistingValuedProperty(name);
1765         }
1766 
1767         return setProperty(name, values, PropertyType.UNDEFINED);
1768     }
1769 
1770     /**
1771      * {@inheritDoc}
1772      * 
1773      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[], int)
1774      */
1775     public final Property setProperty( String name,
1776                                        Value[] values,
1777                                        int type )
1778         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1779         checkSession();
1780         if (values == null) {
1781             // If there is an existing property, then remove it ...
1782             return removeExistingValuedProperty(name);
1783         }
1784 
1785         // Set the value, perhaps to an empty array ...
1786         return editor().setProperty(nameFrom(name), values, type);
1787     }
1788 
1789     /**
1790      * Throw a {@link ConstraintViolationException} if this node is protected (based on the its node definition).
1791      * 
1792      * @throws ConstraintViolationException if this node's definition indicates that the node is protected
1793      * @throws RepositoryException if an error occurs retrieving the definition for this node
1794      */
1795     private void checkNotProtected() throws ConstraintViolationException, RepositoryException {
1796         JcrNodeDefinition nodeDefn = cache.nodeTypes().getNodeDefinition(nodeInfo().getPayload().getDefinitionId());
1797         if (nodeDefn.isProtected()) {
1798             throw new ConstraintViolationException(JcrI18n.cannotRemoveItemWithProtectedDefinition.text(getPath()));
1799         }
1800     }
1801 
1802     final JcrVersionManager versionManager() {
1803         return session().workspace().versionManager();
1804     }
1805 
1806     /**
1807      * {@inheritDoc}
1808      * 
1809      * @see javax.jcr.Node#isCheckedOut()
1810      */
1811     public final boolean isCheckedOut() throws RepositoryException {
1812         checkSession();
1813         return editor().isCheckedOut();
1814     }
1815 
1816     /**
1817      * {@inheritDoc}
1818      * 
1819      * @see javax.jcr.Node#checkin()
1820      */
1821     public final Version checkin() throws RepositoryException {
1822         return versionManager().checkin(this);
1823     }
1824 
1825     /**
1826      * {@inheritDoc}
1827      * 
1828      * @see javax.jcr.Node#checkout()
1829      */
1830     public final void checkout() throws UnsupportedRepositoryOperationException, LockException, RepositoryException {
1831         versionManager().checkout(this);
1832     }
1833 
1834     /**
1835      * {@inheritDoc}
1836      * 
1837      * @see javax.jcr.Node#merge(java.lang.String, boolean)
1838      */
1839     public final NodeIterator merge( String srcWorkspace,
1840                                      boolean bestEffort ) throws ConstraintViolationException, RepositoryException {
1841         CheckArg.isNotNull(srcWorkspace, "source workspace name");
1842 
1843         checkNotProtected();
1844 
1845         return versionManager().merge(this, srcWorkspace, bestEffort, false);
1846     }
1847 
1848     /**
1849      * {@inheritDoc}
1850      * 
1851      * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version)
1852      */
1853     public final void cancelMerge( Version version ) throws RepositoryException {
1854         versionManager().cancelMerge(this, version);
1855     }
1856 
1857     /**
1858      * {@inheritDoc}
1859      * 
1860      * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version)
1861      */
1862     public final void doneMerge( Version version ) throws RepositoryException {
1863         versionManager().doneMerge(this, version);
1864     }
1865 
1866     /**
1867      * {@inheritDoc}
1868      * 
1869      * @see javax.jcr.Node#getVersionHistory()
1870      */
1871     public final JcrVersionHistoryNode getVersionHistory() throws RepositoryException {
1872         return versionManager().getVersionHistory(this);
1873     }
1874 
1875     /**
1876      * {@inheritDoc}
1877      * 
1878      * @see javax.jcr.Node#getBaseVersion()
1879      */
1880     public final JcrVersionNode getBaseVersion() throws RepositoryException {
1881         checkSession();
1882 
1883         // This can happen if the versionable type was added to the node, but it hasn't been saved yet
1884         if (!hasProperty(JcrLexicon.BASE_VERSION)) {
1885             throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text());
1886         }
1887 
1888         return (JcrVersionNode)session().getNodeByUUID(getProperty(JcrLexicon.BASE_VERSION).getString());
1889     }
1890 
1891     /**
1892      * {@inheritDoc}
1893      * 
1894      * @see javax.jcr.Node#restore(java.lang.String, boolean)
1895      */
1896     public final void restore( String versionName,
1897                                boolean removeExisting ) throws RepositoryException {
1898         restore(getVersionHistory().getVersion(versionName), removeExisting);
1899     }
1900 
1901     /**
1902      * {@inheritDoc}
1903      * 
1904      * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean)
1905      */
1906     public final void restore( Version version,
1907                                boolean removeExisting ) throws RepositoryException {
1908         try {
1909             checkNotProtected();
1910         } catch (ConstraintViolationException cve) {
1911             throw new UnsupportedRepositoryOperationException(cve);
1912         }
1913         versionManager().restore(path(), version, null, removeExisting);
1914     }
1915 
1916     /**
1917      * {@inheritDoc}
1918      * 
1919      * @see javax.jcr.Node#restore(javax.jcr.version.Version, java.lang.String, boolean)
1920      */
1921     public final void restore( Version version,
1922                                String relPath,
1923                                boolean removeExisting ) throws RepositoryException {
1924         checkNotProtected();
1925 
1926         PathFactory pathFactory = context().getValueFactories().getPathFactory();
1927         Path relPathAsPath = pathFactory.create(relPath);
1928         if (relPathAsPath.isAbsolute()) throw new RepositoryException(JcrI18n.invalidRelativePath.text(relPath));
1929         Path actualPath = pathFactory.create(path(), relPathAsPath).getCanonicalPath();
1930 
1931         versionManager().restore(actualPath, version, null, removeExisting);
1932     }
1933 
1934     /**
1935      * {@inheritDoc}
1936      * 
1937      * @see javax.jcr.Node#restoreByLabel(java.lang.String, boolean)
1938      */
1939     public final void restoreByLabel( String versionLabel,
1940                                       boolean removeExisting ) throws RepositoryException {
1941         restore(getVersionHistory().getVersionByLabel(versionLabel), removeExisting);
1942     }
1943 
1944     /**
1945      * {@inheritDoc}
1946      * 
1947      * @return <code>false</code>
1948      * @see javax.jcr.Node#holdsLock()
1949      */
1950     public final boolean holdsLock() throws RepositoryException {
1951         checkSession();
1952         return lockManager().holdsLock(this);
1953     }
1954 
1955     /**
1956      * {@inheritDoc}
1957      * 
1958      * @return <code>false</code>
1959      * @see javax.jcr.Node#isLocked()
1960      */
1961     public final boolean isLocked() throws LockException, RepositoryException {
1962         return lockManager().isLocked(this);
1963     }
1964 
1965     /**
1966      * {@inheritDoc}
1967      * 
1968      * @see javax.jcr.Node#lock(boolean, boolean)
1969      */
1970     public final Lock lock( boolean isDeep,
1971                             boolean isSessionScoped ) throws LockException, RepositoryException {
1972         checkSession();
1973         return lockManager().lock(this, isDeep, isSessionScoped, -1L, null);
1974     }
1975 
1976     /**
1977      * {@inheritDoc}
1978      * 
1979      * @see javax.jcr.Node#unlock()
1980      */
1981     public final void unlock() throws LockException, RepositoryException {
1982         checkSession();
1983         lockManager().unlock(this);
1984     }
1985 
1986     /**
1987      * {@inheritDoc}
1988      * 
1989      * @see javax.jcr.Node#getLock()
1990      */
1991     public final Lock getLock() throws LockException, RepositoryException {
1992         checkSession();
1993         return lockManager().getLock(this);
1994     }
1995 
1996     /**
1997      * {@inheritDoc}
1998      * 
1999      * @see javax.jcr.Item#isModified()
2000      */
2001     public final boolean isModified() {
2002         try {
2003             checkSession();
2004             Node<JcrNodePayload, JcrPropertyPayload> node = nodeInfo();
2005             // Considered modified if *not* new but changed
2006             return !node.isNew() && node.isChanged(true);
2007         } catch (RepositoryException re) {
2008             throw new IllegalStateException(re);
2009         }
2010     }
2011 
2012     /**
2013      * {@inheritDoc}
2014      * 
2015      * @see javax.jcr.Item#isNew()
2016      */
2017     public final boolean isNew() {
2018         try {
2019             checkSession();
2020             return nodeInfo().isNew();
2021         } catch (RepositoryException re) {
2022             throw new IllegalStateException(re);
2023         }
2024     }
2025 
2026     /**
2027      * {@inheritDoc}
2028      * 
2029      * @see javax.jcr.Node#getCorrespondingNodePath(java.lang.String)
2030      */
2031     public final String getCorrespondingNodePath( String workspaceName )
2032         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
2033         CheckArg.isNotNull(workspaceName, "workspace name");
2034         checkSession();
2035         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
2036         return correspondingNodePath(workspaceName).getString(namespaces);
2037     }
2038 
2039     protected final Path correspondingNodePath( String workspaceName )
2040         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
2041         assert workspaceName != null;
2042         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
2043 
2044         // Find the closest ancestor (including this node) that is referenceable ...
2045         AbstractJcrNode referenceableRoot = this;
2046         while (!referenceableRoot.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(namespaces))) {
2047             referenceableRoot = referenceableRoot.getParent();
2048         }
2049 
2050         // Find the relative path from the nearest referenceable node to this node (or null if this node is referenceable) ...
2051         Path relativePath = path().equals(referenceableRoot.path()) ? null : path().relativeTo(referenceableRoot.path());
2052         UUID uuid = UUID.fromString(referenceableRoot.getUUID());
2053         return this.cache.getPathForCorrespondingNode(workspaceName, uuid, relativePath);
2054     }
2055 
2056     /**
2057      * {@inheritDoc}
2058      * 
2059      * @see javax.jcr.Node#update(java.lang.String)
2060      */
2061     public final void update( String srcWorkspaceName ) throws NoSuchWorkspaceException, RepositoryException {
2062         CheckArg.isNotNull(srcWorkspaceName, "workspace name");
2063         checkSession();
2064 
2065         if (session().hasPendingChanges()) {
2066             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
2067         }
2068 
2069         checkNotProtected();
2070 
2071         Path correspondingPath = null;
2072         try {
2073             correspondingPath = correspondingNodePath(srcWorkspaceName);
2074         } catch (ItemNotFoundException infe) {
2075             return;
2076         }
2077 
2078         // Need to force remove in case this node is not referenceable
2079         cache.graphSession().immediateClone(correspondingPath, srcWorkspaceName, path(), true, true);
2080 
2081         session().refresh(false);
2082     }
2083 
2084     /**
2085      * {@inheritDoc}
2086      * 
2087      * @throws UnsupportedRepositoryOperationException always
2088      * @see javax.jcr.Node#orderBefore(java.lang.String, java.lang.String)
2089      */
2090     public final void orderBefore( String srcChildRelPath,
2091                                    String destChildRelPath ) throws UnsupportedRepositoryOperationException, RepositoryException {
2092         checkSession();
2093         // This implementation is correct, except for not calling the SessionCache or graph layer to do the re-order
2094         if (!getPrimaryNodeType().hasOrderableChildNodes()) {
2095             throw new UnsupportedRepositoryOperationException(
2096                                                               JcrI18n.notOrderable.text(getPrimaryNodeType().getName(), getPath()));
2097         }
2098 
2099         PathFactory pathFactory = this.cache.pathFactory();
2100         Path srcPath = pathFactory.create(srcChildRelPath);
2101         if (srcPath.isAbsolute()) {
2102             // Not a relative path ...
2103             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(srcChildRelPath, "relativePath"));
2104         }
2105         if (srcPath.isAbsolute() || srcPath.size() != 1) {
2106             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath.getString(namespaces()), cache.workspaceName()));
2107         }
2108         // getLastSegment should return the only segment, since we verified that size() == 1
2109         Path.Segment sourceSegment = srcPath.getLastSegment();
2110         try {
2111             nodeInfo().getChild(sourceSegment);
2112         } catch (org.modeshape.graph.property.PathNotFoundException e) {
2113             String workspaceName = this.cache.workspaceName();
2114             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath, workspaceName));
2115         }
2116 
2117         Path.Segment destSegment = null;
2118 
2119         if (destChildRelPath != null) {
2120             Path destPath = pathFactory.create(destChildRelPath);
2121             if (destPath.isAbsolute()) {
2122                 // Not a relative path ...
2123                 throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(destChildRelPath, "relativePath"));
2124             }
2125             if (destPath.size() != 1) {
2126                 throw new ItemNotFoundException(
2127                                                 JcrI18n.pathNotFound.text(destPath.getString(namespaces()), cache.workspaceName()));
2128             }
2129 
2130             destSegment = destPath.getLastSegment();
2131 
2132             // getLastSegment should return the only segment, since we verified that size() == 1
2133             try {
2134                 nodeInfo().getChild(destSegment);
2135             } catch (org.modeshape.graph.property.PathNotFoundException e) {
2136                 String workspaceName = this.cache.session().getWorkspace().getName();
2137                 throw new ItemNotFoundException(JcrI18n.pathNotFound.text(destPath, workspaceName));
2138             }
2139         }
2140 
2141         this.editor().orderChildBefore(sourceSegment, destSegment);
2142     }
2143 
2144     protected static List<Object> createPatternsFor( String[] namePatterns ) throws RepositoryException {
2145         List<Object> patterns = new LinkedList<Object>();
2146         for (String stringPattern : namePatterns) {
2147             stringPattern = stringPattern.trim();
2148             int length = stringPattern.length();
2149             if (length == 0) continue;
2150             if (stringPattern.indexOf("*") == -1) {
2151                 // Doesn't use wildcard, so use String not Pattern
2152                 patterns.add(stringPattern);
2153             } else {
2154                 // We need to escape the regular expression characters ...
2155                 StringBuilder sb = new StringBuilder(length);
2156                 for (int i = 0; i != length; i++) {
2157                     char c = stringPattern.charAt(i);
2158                     switch (c) {
2159                         // Per the spec, the the following characters are not allowed in patterns:
2160                         case '/':
2161                         case '[':
2162                         case ']':
2163                         case '\'':
2164                         case '"':
2165                         case '|':
2166                         case '\t':
2167                         case '\n':
2168                         case '\r':
2169                             String msg = JcrI18n.invalidNamePattern.text(c, stringPattern);
2170                             throw new RepositoryException(msg);
2171                             // The following characters must be escaped when used in regular expressions ...
2172                         case '?':
2173                         case '(':
2174                         case ')':
2175                         case '$':
2176                         case '^':
2177                         case '.':
2178                         case '{':
2179                         case '}':
2180                         case '\\':
2181                             sb.append("\\");
2182                             sb.append(c);
2183                             break;
2184                         case '*':
2185                             // replace with the regular expression wildcard
2186                             sb.append(".*");
2187                             break;
2188                         default:
2189                             sb.append(c);
2190                             break;
2191                     }
2192                 }
2193                 String escapedString = sb.toString();
2194                 Pattern pattern = Pattern.compile(escapedString);
2195                 patterns.add(pattern);
2196             }
2197         }
2198         return patterns;
2199     }
2200 
2201     /**
2202      * {@inheritDoc}
2203      * 
2204      * @see javax.jcr.Item#refresh(boolean)
2205      */
2206     public void refresh( boolean keepChanges ) throws RepositoryException {
2207         checkSession();
2208         this.cache.refresh(this.nodeId, location.getPath(), keepChanges);
2209     }
2210 
2211     /**
2212      * {@inheritDoc}
2213      * 
2214      * @see javax.jcr.Item#save()
2215      */
2216     public void save() throws RepositoryException {
2217         checkSession();
2218         session().checkReferentialIntegrityOfChanges(this);
2219         cache.save(nodeId, location.getPath());
2220     }
2221 
2222     @Override
2223     public String toString() {
2224 
2225         try {
2226             PropertyIterator iter = this.getProperties();
2227             StringBuffer propertyBuff = new StringBuffer();
2228             while (iter.hasNext()) {
2229                 AbstractJcrProperty prop = (AbstractJcrProperty)iter.nextProperty();
2230                 propertyBuff.append(prop).append(", ");
2231             }
2232             return this.getPath() + " {" + propertyBuff.toString() + "}";
2233         } catch (RepositoryException re) {
2234             return re.getMessage();
2235         }
2236     }
2237 
2238     /**
2239      * {@inheritDoc}
2240      * 
2241      * @see java.lang.Object#equals(java.lang.Object)
2242      */
2243     @Override
2244     public boolean equals( Object obj ) {
2245         if (obj == this) return true;
2246         if (obj instanceof AbstractJcrNode) {
2247             AbstractJcrNode that = (AbstractJcrNode)obj;
2248             if (this.cache != that.cache) return false;
2249             return this.location.equals(that.location);
2250         }
2251         return false;
2252     }
2253 
2254     /**
2255      * {@inheritDoc}
2256      * 
2257      * @see java.lang.Object#hashCode()
2258      */
2259     @Override
2260     public int hashCode() {
2261         return HashCode.compute(cache, location.getUuid());
2262     }
2263 
2264     /**
2265      * {@inheritDoc}
2266      * 
2267      * @see javax.jcr.Node#followLifecycleTransition(java.lang.String)
2268      */
2269     @SuppressWarnings( "unused" )
2270     @Override
2271     public void followLifecycleTransition( String transition )
2272         throws UnsupportedRepositoryOperationException, InvalidLifecycleTransitionException, RepositoryException {
2273         throw new UnsupportedRepositoryOperationException();
2274     }
2275 
2276     /**
2277      * {@inheritDoc}
2278      * 
2279      * @see javax.jcr.Node#getAllowedLifecycleTransistions()
2280      */
2281     @Override
2282     public String[] getAllowedLifecycleTransistions() throws UnsupportedRepositoryOperationException, RepositoryException {
2283         throw new UnsupportedRepositoryOperationException();
2284     }
2285 
2286     /**
2287      * {@inheritDoc}
2288      * 
2289      * @see javax.jcr.Node#getSharedSet()
2290      */
2291     @Override
2292     public NodeIterator getSharedSet() throws RepositoryException {
2293         if (isShareable()) {
2294             // Find the nodes that make up this shared set ...
2295             return sharedSet();
2296         }
2297         // Otherwise, the shared set is just this node ...
2298         return new JcrSingleNodeIterator(this);
2299     }
2300 
2301     /**
2302      * Find all of the {@link javax.jcr.Node}s that make up the shared set.
2303      * 
2304      * @return the query result over the nodes in the node set; never null, but possibly empty if the node given by the identifier
2305      *         does not exist or is not a shareable node, or possibly of size 1 if the node given by the identifier does exist and
2306      *         is shareable but has no other nodes in the shared set
2307      * @throws RepositoryException if there is a problem executing the query or finding the shared set
2308      */
2309     NodeIterator sharedSet() throws RepositoryException {
2310         AbstractJcrNode original = this;
2311         String identifierOfSharedNode = getIdentifier();
2312         if (this instanceof JcrSharedNode) {
2313             original = ((JcrSharedNode)this).originalNode();
2314         }
2315         // Execute a query that will report all proxy nodes ...
2316         QueryBuilder builder = new QueryBuilder(context().getValueFactories().getTypeSystem());
2317         QueryCommand query = builder.select("jcr:primaryType")
2318                                     .from("mode:share")
2319                                     .where()
2320                                     .referenceValue("mode:share", "mode:sharedUuid")
2321                                     .isEqualTo(identifierOfSharedNode)
2322                                     .end()
2323                                     .query();
2324         Query jcrQuery = session().workspace().queryManager().createQuery(query);
2325         QueryResult result = jcrQuery.execute();
2326         // And combine the results ...
2327         return new JcrNodeIterator(original, result.getNodes());
2328     }
2329 
2330     /**
2331      * {@inheritDoc}
2332      * 
2333      * @see javax.jcr.Node#removeShare()
2334      */
2335     @Override
2336     public void removeShare() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
2337         if (isShareable()) {
2338             // Get the nodes in the shared set ...
2339             NodeIterator sharedSetNodes = sharedSet();
2340             long sharedSetSize = sharedSetNodes.getSize(); // computed w/o respect for privileges
2341             if (sharedSetSize <= 1) {
2342                 // There aren't any other nodes in the shared set, so simply remove this node ...
2343                 doRemove();
2344                 return;
2345             }
2346             // Find the second node in the shared set that is not this object ...
2347             AbstractJcrNode originalNode = (AbstractJcrNode)sharedSetNodes.nextNode();
2348             if (originalNode == this) {
2349                 // We need to move this node into the first proxy ...
2350                 JcrSharedNode firstProxy = (JcrSharedNode)sharedSetNodes.nextNode();
2351                 assert !this.isRoot();
2352                 assert !firstProxy.isRoot();
2353                 boolean sameParent = firstProxy.getParent().equals(this.getParent());
2354                 NodeEditor parentEditor = firstProxy.editorForParent();
2355                 if (sameParent) {
2356                     // Move this node to just before the other shareable node ...
2357                     parentEditor.orderChildBefore(this.segment(), firstProxy.segment());
2358                     // And finally remove the first proxy ...
2359                     firstProxy.doRemove();
2360                 } else {
2361                     // Find the node immediately following the proxy ...
2362                     Node<JcrNodePayload, JcrPropertyPayload> proxyNode = firstProxy.proxyInfo();
2363                     Node<JcrNodePayload, JcrPropertyPayload> nextChild = parentEditor.node().getChildAfter(proxyNode);
2364                     Name newName = proxyNode.getName();
2365                     // Remove the first proxy ...
2366                     firstProxy.doRemove();
2367                     // Move this node to the new parent ...
2368                     Node<JcrNodePayload, JcrPropertyPayload> newNode = parentEditor.moveToBeChild(this, newName);
2369                     if (nextChild != null) {
2370                         // And place this node where the first proxy was (just before the 'nextChild') ...
2371                         parentEditor.orderChildBefore(newNode.getSegment(), nextChild.getSegment());
2372                     }
2373                 }
2374             } else {
2375                 // We can just remove this proxy ...
2376                 doRemove();
2377             }
2378             return;
2379         }
2380         // If we get to here, either there are no other nodes in the shared set or this node is a non-shareable node,
2381         // so simply remove this node (per section 14.2 of the JCR 2.0 specification) ...
2382         doRemove();
2383     }
2384 
2385     /**
2386      * {@inheritDoc}
2387      * 
2388      * @see javax.jcr.Node#removeSharedSet()
2389      */
2390     @Override
2391     public void removeSharedSet() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
2392         if (isShareable()) {
2393             // Remove all of the node is the shared set ...
2394             NodeIterator sharedSetNodes = sharedSet();
2395             while (sharedSetNodes.hasNext()) {
2396                 AbstractJcrNode nodeInSharedSet = (AbstractJcrNode)sharedSetNodes.nextNode();
2397                 nodeInSharedSet.doRemove();
2398             }
2399         } else {
2400             // Per section 14.2 of the JCR 2.0 specification:
2401             // "In cases where the shared set consists of a single node, or when these methods are
2402             // called on a non-shareable node, their behavior is identical to Node.remove()."
2403             doRemove();
2404         }
2405     }
2406 
2407     /**
2408      * {@inheritDoc}
2409      * <p>
2410      * According to Section 14.3 of the JCR 2.0 specification, an implementation may choose whether the {@link Item#remove()}
2411      * method (and the {@link Session#removeItem(String)} method) behaves as a {@link javax.jcr.Node#removeShare()} or
2412      * {@link javax.jcr.Node#removeSharedSet()}. {@link javax.jcr.Node#removeShare()} just removes this node from the shared set,
2413      * whereas {@link javax.jcr.Node#removeSharedSet()} removes all nodes in the shared set, including the original shared node.
2414      * </p>
2415      * <p>
2416      * ModeShape implements {@link Item#remove()} of a shared node as simply removing this node from the shared set. In other
2417      * words, this method is equivalent to calling {@link #removeShare()}.
2418      * </p>
2419      * 
2420      * @see javax.jcr.Item#remove()
2421      */
2422     @Override
2423     public void remove()
2424         throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
2425         // Since this node might be shareable, we want to implement 'remove()' by calling 'removeShare()',
2426         // which will behave correctly even if it is not shareable ...
2427         removeShare();
2428     }
2429 
2430     /**
2431      * Perform a real remove of this node.
2432      * 
2433      * @throws VersionException
2434      * @throws LockException
2435      * @throws ConstraintViolationException
2436      * @throws AccessDeniedException
2437      * @throws RepositoryException
2438      */
2439     protected abstract void doRemove()
2440         throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException;
2441 
2442 }