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.BINARY, 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 
1542         /*
1543          * Return without throwing an exception to match JR behavior.  This is also in conformance with the spec.
1544          * This is a fix for MODE-976.
1545          */
1546         return null;
1547     }
1548 
1549     /**
1550      * {@inheritDoc}
1551      * 
1552      * @see javax.jcr.Node#setProperty(java.lang.String, boolean)
1553      */
1554     public final Property setProperty( String name,
1555                                        boolean value )
1556         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1557         checkSession();
1558         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.BOOLEAN, value));
1559     }
1560 
1561     /**
1562      * {@inheritDoc}
1563      * 
1564      * @see javax.jcr.Node#setProperty(java.lang.String, Binary)
1565      */
1566     @Override
1567     public Property setProperty( String name,
1568                                  Binary value )
1569         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1570         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.BINARY, value));
1571     }
1572 
1573     /**
1574      * {@inheritDoc}
1575      * 
1576      * @see javax.jcr.Node#setProperty(java.lang.String, BigDecimal)
1577      */
1578     @Override
1579     public Property setProperty( String name,
1580                                  BigDecimal value )
1581         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1582         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.DECIMAL, value));
1583     }
1584 
1585     /**
1586      * {@inheritDoc}
1587      * 
1588      * @see javax.jcr.Node#setProperty(java.lang.String, java.util.Calendar)
1589      */
1590     public final Property setProperty( String name,
1591                                        Calendar value )
1592         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1593         checkSession();
1594 
1595         if (value == null) {
1596             return removeExistingValuedProperty(name);
1597         }
1598 
1599         return editor().setProperty(nameFrom(name), valueFrom(value));
1600     }
1601 
1602     /**
1603      * {@inheritDoc}
1604      * 
1605      * @see javax.jcr.Node#setProperty(java.lang.String, double)
1606      */
1607     public final Property setProperty( String name,
1608                                        double value )
1609         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1610         checkSession();
1611         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.DOUBLE, value));
1612     }
1613 
1614     /**
1615      * {@inheritDoc}
1616      * 
1617      * @see javax.jcr.Node#setProperty(java.lang.String, java.io.InputStream)
1618      */
1619     public final Property setProperty( String name,
1620                                        InputStream value )
1621         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1622         checkSession();
1623         if (value == null) {
1624             return removeExistingValuedProperty(name);
1625         }
1626 
1627         return editor().setProperty(nameFrom(name), valueFrom(value));
1628     }
1629 
1630     /**
1631      * {@inheritDoc}
1632      * 
1633      * @see javax.jcr.Node#setProperty(java.lang.String, long)
1634      */
1635     public final Property setProperty( String name,
1636                                        long value )
1637         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1638         checkSession();
1639         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.LONG, value));
1640     }
1641 
1642     /**
1643      * {@inheritDoc}
1644      * 
1645      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Node)
1646      */
1647     public final Property setProperty( String name,
1648                                        javax.jcr.Node value )
1649         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1650         checkSession();
1651         if (value == null) {
1652             return removeExistingValuedProperty(name);
1653         }
1654 
1655         return editor().setProperty(nameFrom(name), valueFrom(value));
1656     }
1657 
1658     /**
1659      * {@inheritDoc}
1660      * 
1661      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String)
1662      */
1663     public final Property setProperty( String name,
1664                                        String value )
1665         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1666         checkSession();
1667         if (value == null) {
1668             return removeExistingValuedProperty(name);
1669         }
1670 
1671         return editor().setProperty(nameFrom(name), valueFrom(PropertyType.STRING, value));
1672     }
1673 
1674     /**
1675      * {@inheritDoc}
1676      * 
1677      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String, int)
1678      */
1679     public final Property setProperty( String name,
1680                                        String value,
1681                                        int type )
1682         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1683         checkSession();
1684         if (value == null) {
1685             return removeExistingValuedProperty(name);
1686         }
1687 
1688         return editor().setProperty(nameFrom(name), valueFrom(type, value));
1689     }
1690 
1691     /**
1692      * {@inheritDoc}
1693      * 
1694      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[])
1695      */
1696     public final Property setProperty( String name,
1697                                        String[] values )
1698         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1699         checkSession();
1700         if (values == null) {
1701             return removeExistingValuedProperty(name);
1702         }
1703 
1704         return editor().setProperty(nameFrom(name), valuesFrom(PropertyType.STRING, values), PropertyType.UNDEFINED);
1705     }
1706 
1707     /**
1708      * {@inheritDoc}
1709      * 
1710      * @see javax.jcr.Node#setProperty(java.lang.String, java.lang.String[], int)
1711      */
1712     public final Property setProperty( String name,
1713                                        String[] values,
1714                                        int type )
1715         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1716         checkSession();
1717         if (values == null) {
1718             return removeExistingValuedProperty(name);
1719         }
1720 
1721         return editor().setProperty(nameFrom(name), valuesFrom(type, values), PropertyType.UNDEFINED);
1722     }
1723 
1724     /**
1725      * {@inheritDoc}
1726      * 
1727      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value)
1728      */
1729     public final Property setProperty( String name,
1730                                        Value value )
1731         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1732         checkSession();
1733         if (value == null) {
1734             return removeExistingValuedProperty(name);
1735         }
1736 
1737         return editor().setProperty(nameFrom(name), (JcrValue)value);
1738     }
1739 
1740     /**
1741      * {@inheritDoc}
1742      * 
1743      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value, int)
1744      */
1745     public final Property setProperty( String name,
1746                                        Value value,
1747                                        int type )
1748         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1749         checkSession();
1750         if (value == null) {
1751             return removeExistingValuedProperty(name);
1752         }
1753 
1754         return editor().setProperty(nameFrom(name), ((JcrValue)value).asType(type));
1755     }
1756 
1757     /**
1758      * {@inheritDoc}
1759      * 
1760      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[])
1761      */
1762     public final Property setProperty( String name,
1763                                        Value[] values )
1764         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1765         checkSession();
1766         if (values == null) {
1767             // If there is an existing property, then remove it ...
1768             return removeExistingValuedProperty(name);
1769         }
1770 
1771         return setProperty(name, values, PropertyType.UNDEFINED);
1772     }
1773 
1774     /**
1775      * {@inheritDoc}
1776      * 
1777      * @see javax.jcr.Node#setProperty(java.lang.String, javax.jcr.Value[], int)
1778      */
1779     public final Property setProperty( String name,
1780                                        Value[] values,
1781                                        int type )
1782         throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
1783         checkSession();
1784         if (values == null) {
1785             // If there is an existing property, then remove it ...
1786             return removeExistingValuedProperty(name);
1787         }
1788 
1789         // Set the value, perhaps to an empty array ...
1790         return editor().setProperty(nameFrom(name), values, type);
1791     }
1792 
1793     /**
1794      * Throw a {@link ConstraintViolationException} if this node is protected (based on the its node definition).
1795      * 
1796      * @throws ConstraintViolationException if this node's definition indicates that the node is protected
1797      * @throws RepositoryException if an error occurs retrieving the definition for this node
1798      */
1799     private void checkNotProtected() throws ConstraintViolationException, RepositoryException {
1800         JcrNodeDefinition nodeDefn = cache.nodeTypes().getNodeDefinition(nodeInfo().getPayload().getDefinitionId());
1801         if (nodeDefn.isProtected()) {
1802             throw new ConstraintViolationException(JcrI18n.cannotRemoveItemWithProtectedDefinition.text(getPath()));
1803         }
1804     }
1805 
1806     final JcrVersionManager versionManager() {
1807         return session().workspace().versionManager();
1808     }
1809 
1810     /**
1811      * {@inheritDoc}
1812      * 
1813      * @see javax.jcr.Node#isCheckedOut()
1814      */
1815     public final boolean isCheckedOut() throws RepositoryException {
1816         checkSession();
1817         return editor().isCheckedOut();
1818     }
1819 
1820     /**
1821      * {@inheritDoc}
1822      * 
1823      * @see javax.jcr.Node#checkin()
1824      */
1825     public final Version checkin() throws RepositoryException {
1826         return versionManager().checkin(this);
1827     }
1828 
1829     /**
1830      * {@inheritDoc}
1831      * 
1832      * @see javax.jcr.Node#checkout()
1833      */
1834     public final void checkout() throws UnsupportedRepositoryOperationException, LockException, RepositoryException {
1835         versionManager().checkout(this);
1836     }
1837 
1838     /**
1839      * {@inheritDoc}
1840      * 
1841      * @see javax.jcr.Node#merge(java.lang.String, boolean)
1842      */
1843     public final NodeIterator merge( String srcWorkspace,
1844                                      boolean bestEffort ) throws ConstraintViolationException, RepositoryException {
1845         CheckArg.isNotNull(srcWorkspace, "source workspace name");
1846 
1847         checkNotProtected();
1848 
1849         return versionManager().merge(this, srcWorkspace, bestEffort, false);
1850     }
1851 
1852     /**
1853      * {@inheritDoc}
1854      * 
1855      * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version)
1856      */
1857     public final void cancelMerge( Version version ) throws RepositoryException {
1858         versionManager().cancelMerge(this, version);
1859     }
1860 
1861     /**
1862      * {@inheritDoc}
1863      * 
1864      * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version)
1865      */
1866     public final void doneMerge( Version version ) throws RepositoryException {
1867         versionManager().doneMerge(this, version);
1868     }
1869 
1870     /**
1871      * {@inheritDoc}
1872      * 
1873      * @see javax.jcr.Node#getVersionHistory()
1874      */
1875     public final JcrVersionHistoryNode getVersionHistory() throws RepositoryException {
1876         return versionManager().getVersionHistory(this);
1877     }
1878 
1879     /**
1880      * {@inheritDoc}
1881      * 
1882      * @see javax.jcr.Node#getBaseVersion()
1883      */
1884     public final JcrVersionNode getBaseVersion() throws RepositoryException {
1885         checkSession();
1886 
1887         // This can happen if the versionable type was added to the node, but it hasn't been saved yet
1888         if (!hasProperty(JcrLexicon.BASE_VERSION)) {
1889             throw new UnsupportedRepositoryOperationException(JcrI18n.requiresVersionable.text());
1890         }
1891 
1892         return (JcrVersionNode)session().getNodeByUUID(getProperty(JcrLexicon.BASE_VERSION).getString());
1893     }
1894 
1895     /**
1896      * {@inheritDoc}
1897      * 
1898      * @see javax.jcr.Node#restore(java.lang.String, boolean)
1899      */
1900     public final void restore( String versionName,
1901                                boolean removeExisting ) throws RepositoryException {
1902         restore(getVersionHistory().getVersion(versionName), removeExisting);
1903     }
1904 
1905     /**
1906      * {@inheritDoc}
1907      * 
1908      * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean)
1909      */
1910     public final void restore( Version version,
1911                                boolean removeExisting ) throws RepositoryException {
1912         try {
1913             checkNotProtected();
1914         } catch (ConstraintViolationException cve) {
1915             throw new UnsupportedRepositoryOperationException(cve);
1916         }
1917         versionManager().restore(path(), version, null, removeExisting);
1918     }
1919 
1920     /**
1921      * {@inheritDoc}
1922      * 
1923      * @see javax.jcr.Node#restore(javax.jcr.version.Version, java.lang.String, boolean)
1924      */
1925     public final void restore( Version version,
1926                                String relPath,
1927                                boolean removeExisting ) throws RepositoryException {
1928         checkNotProtected();
1929 
1930         PathFactory pathFactory = context().getValueFactories().getPathFactory();
1931         Path relPathAsPath = pathFactory.create(relPath);
1932         if (relPathAsPath.isAbsolute()) throw new RepositoryException(JcrI18n.invalidRelativePath.text(relPath));
1933         Path actualPath = pathFactory.create(path(), relPathAsPath).getCanonicalPath();
1934 
1935         versionManager().restore(actualPath, version, null, removeExisting);
1936     }
1937 
1938     /**
1939      * {@inheritDoc}
1940      * 
1941      * @see javax.jcr.Node#restoreByLabel(java.lang.String, boolean)
1942      */
1943     public final void restoreByLabel( String versionLabel,
1944                                       boolean removeExisting ) throws RepositoryException {
1945         restore(getVersionHistory().getVersionByLabel(versionLabel), removeExisting);
1946     }
1947 
1948     /**
1949      * {@inheritDoc}
1950      * 
1951      * @return <code>false</code>
1952      * @see javax.jcr.Node#holdsLock()
1953      */
1954     public final boolean holdsLock() throws RepositoryException {
1955         checkSession();
1956         return lockManager().holdsLock(this);
1957     }
1958 
1959     /**
1960      * {@inheritDoc}
1961      * 
1962      * @return <code>false</code>
1963      * @see javax.jcr.Node#isLocked()
1964      */
1965     public final boolean isLocked() throws LockException, RepositoryException {
1966         return lockManager().isLocked(this);
1967     }
1968 
1969     /**
1970      * {@inheritDoc}
1971      * 
1972      * @see javax.jcr.Node#lock(boolean, boolean)
1973      */
1974     public final Lock lock( boolean isDeep,
1975                             boolean isSessionScoped ) throws LockException, RepositoryException {
1976         checkSession();
1977         return lockManager().lock(this, isDeep, isSessionScoped, -1L, null);
1978     }
1979 
1980     /**
1981      * {@inheritDoc}
1982      * 
1983      * @see javax.jcr.Node#unlock()
1984      */
1985     public final void unlock() throws LockException, RepositoryException {
1986         checkSession();
1987         lockManager().unlock(this);
1988     }
1989 
1990     /**
1991      * {@inheritDoc}
1992      * 
1993      * @see javax.jcr.Node#getLock()
1994      */
1995     public final Lock getLock() throws LockException, RepositoryException {
1996         checkSession();
1997         return lockManager().getLock(this);
1998     }
1999 
2000     /**
2001      * {@inheritDoc}
2002      * 
2003      * @see javax.jcr.Item#isModified()
2004      */
2005     public final boolean isModified() {
2006         try {
2007             checkSession();
2008             Node<JcrNodePayload, JcrPropertyPayload> node = nodeInfo();
2009             // Considered modified if *not* new but changed
2010             return !node.isNew() && node.isChanged(true);
2011         } catch (RepositoryException re) {
2012             throw new IllegalStateException(re);
2013         }
2014     }
2015 
2016     /**
2017      * {@inheritDoc}
2018      * 
2019      * @see javax.jcr.Item#isNew()
2020      */
2021     public final boolean isNew() {
2022         try {
2023             checkSession();
2024             return nodeInfo().isNew();
2025         } catch (RepositoryException re) {
2026             throw new IllegalStateException(re);
2027         }
2028     }
2029 
2030     /**
2031      * {@inheritDoc}
2032      * 
2033      * @see javax.jcr.Node#getCorrespondingNodePath(java.lang.String)
2034      */
2035     public final String getCorrespondingNodePath( String workspaceName )
2036         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
2037         CheckArg.isNotNull(workspaceName, "workspace name");
2038         checkSession();
2039         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
2040         return correspondingNodePath(workspaceName).getString(namespaces);
2041     }
2042 
2043     protected final Path correspondingNodePath( String workspaceName )
2044         throws NoSuchWorkspaceException, ItemNotFoundException, RepositoryException {
2045         assert workspaceName != null;
2046         NamespaceRegistry namespaces = this.context().getNamespaceRegistry();
2047 
2048         // Find the closest ancestor (including this node) that is referenceable ...
2049         AbstractJcrNode referenceableRoot = this;
2050         while (!referenceableRoot.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(namespaces))) {
2051             referenceableRoot = referenceableRoot.getParent();
2052         }
2053 
2054         // Find the relative path from the nearest referenceable node to this node (or null if this node is referenceable) ...
2055         Path relativePath = path().equals(referenceableRoot.path()) ? null : path().relativeTo(referenceableRoot.path());
2056         UUID uuid = UUID.fromString(referenceableRoot.getUUID());
2057         return this.cache.getPathForCorrespondingNode(workspaceName, uuid, relativePath);
2058     }
2059 
2060     /**
2061      * {@inheritDoc}
2062      * 
2063      * @see javax.jcr.Node#update(java.lang.String)
2064      */
2065     public final void update( String srcWorkspaceName ) throws NoSuchWorkspaceException, RepositoryException {
2066         CheckArg.isNotNull(srcWorkspaceName, "workspace name");
2067         checkSession();
2068 
2069         if (session().hasPendingChanges()) {
2070             throw new InvalidItemStateException(JcrI18n.noPendingChangesAllowed.text());
2071         }
2072 
2073         checkNotProtected();
2074 
2075         Path correspondingPath = null;
2076         try {
2077             correspondingPath = correspondingNodePath(srcWorkspaceName);
2078         } catch (ItemNotFoundException infe) {
2079             return;
2080         }
2081 
2082         // Need to force remove in case this node is not referenceable
2083         cache.graphSession().immediateClone(correspondingPath, srcWorkspaceName, path(), true, true);
2084 
2085         session().refresh(false);
2086     }
2087 
2088     /**
2089      * {@inheritDoc}
2090      * 
2091      * @throws UnsupportedRepositoryOperationException always
2092      * @see javax.jcr.Node#orderBefore(java.lang.String, java.lang.String)
2093      */
2094     public final void orderBefore( String srcChildRelPath,
2095                                    String destChildRelPath ) throws UnsupportedRepositoryOperationException, RepositoryException {
2096         checkSession();
2097         // This implementation is correct, except for not calling the SessionCache or graph layer to do the re-order
2098         if (!getPrimaryNodeType().hasOrderableChildNodes()) {
2099             throw new UnsupportedRepositoryOperationException(
2100                                                               JcrI18n.notOrderable.text(getPrimaryNodeType().getName(), getPath()));
2101         }
2102 
2103         PathFactory pathFactory = this.cache.pathFactory();
2104         Path srcPath = pathFactory.create(srcChildRelPath);
2105         if (srcPath.isAbsolute()) {
2106             // Not a relative path ...
2107             throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(srcChildRelPath, "relativePath"));
2108         }
2109         if (srcPath.isAbsolute() || srcPath.size() != 1) {
2110             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath.getString(namespaces()), cache.workspaceName()));
2111         }
2112         // getLastSegment should return the only segment, since we verified that size() == 1
2113         Path.Segment sourceSegment = srcPath.getLastSegment();
2114         try {
2115             nodeInfo().getChild(sourceSegment);
2116         } catch (org.modeshape.graph.property.PathNotFoundException e) {
2117             String workspaceName = this.cache.workspaceName();
2118             throw new ItemNotFoundException(JcrI18n.pathNotFound.text(srcPath, workspaceName));
2119         }
2120 
2121         Path.Segment destSegment = null;
2122 
2123         if (destChildRelPath != null) {
2124             Path destPath = pathFactory.create(destChildRelPath);
2125             if (destPath.isAbsolute()) {
2126                 // Not a relative path ...
2127                 throw new IllegalArgumentException(JcrI18n.invalidPathParameter.text(destChildRelPath, "relativePath"));
2128             }
2129             if (destPath.size() != 1) {
2130                 throw new ItemNotFoundException(
2131                                                 JcrI18n.pathNotFound.text(destPath.getString(namespaces()), cache.workspaceName()));
2132             }
2133 
2134             destSegment = destPath.getLastSegment();
2135 
2136             // getLastSegment should return the only segment, since we verified that size() == 1
2137             try {
2138                 nodeInfo().getChild(destSegment);
2139             } catch (org.modeshape.graph.property.PathNotFoundException e) {
2140                 String workspaceName = this.cache.session().getWorkspace().getName();
2141                 throw new ItemNotFoundException(JcrI18n.pathNotFound.text(destPath, workspaceName));
2142             }
2143         }
2144 
2145         this.editor().orderChildBefore(sourceSegment, destSegment);
2146     }
2147 
2148     protected static List<Object> createPatternsFor( String[] namePatterns ) throws RepositoryException {
2149         List<Object> patterns = new LinkedList<Object>();
2150         for (String stringPattern : namePatterns) {
2151             stringPattern = stringPattern.trim();
2152             int length = stringPattern.length();
2153             if (length == 0) continue;
2154             if (stringPattern.indexOf("*") == -1) {
2155                 // Doesn't use wildcard, so use String not Pattern
2156                 patterns.add(stringPattern);
2157             } else {
2158                 // We need to escape the regular expression characters ...
2159                 StringBuilder sb = new StringBuilder(length);
2160                 for (int i = 0; i != length; i++) {
2161                     char c = stringPattern.charAt(i);
2162                     switch (c) {
2163                         // Per the spec, the the following characters are not allowed in patterns:
2164                         case '/':
2165                         case '[':
2166                         case ']':
2167                         case '\'':
2168                         case '"':
2169                         case '|':
2170                         case '\t':
2171                         case '\n':
2172                         case '\r':
2173                             String msg = JcrI18n.invalidNamePattern.text(c, stringPattern);
2174                             throw new RepositoryException(msg);
2175                             // The following characters must be escaped when used in regular expressions ...
2176                         case '?':
2177                         case '(':
2178                         case ')':
2179                         case '$':
2180                         case '^':
2181                         case '.':
2182                         case '{':
2183                         case '}':
2184                         case '\\':
2185                             sb.append("\\");
2186                             sb.append(c);
2187                             break;
2188                         case '*':
2189                             // replace with the regular expression wildcard
2190                             sb.append(".*");
2191                             break;
2192                         default:
2193                             sb.append(c);
2194                             break;
2195                     }
2196                 }
2197                 String escapedString = sb.toString();
2198                 Pattern pattern = Pattern.compile(escapedString);
2199                 patterns.add(pattern);
2200             }
2201         }
2202         return patterns;
2203     }
2204 
2205     /**
2206      * {@inheritDoc}
2207      * 
2208      * @see javax.jcr.Item#refresh(boolean)
2209      */
2210     public void refresh( boolean keepChanges ) throws RepositoryException {
2211         checkSession();
2212         this.cache.refresh(this.nodeId, location.getPath(), keepChanges);
2213     }
2214 
2215     /**
2216      * {@inheritDoc}
2217      * 
2218      * @see javax.jcr.Item#save()
2219      */
2220     public void save() throws RepositoryException {
2221         checkSession();
2222         session().checkReferentialIntegrityOfChanges(this);
2223         cache.save(nodeId, location.getPath());
2224     }
2225 
2226     @Override
2227     public String toString() {
2228 
2229         try {
2230             PropertyIterator iter = this.getProperties();
2231             StringBuffer propertyBuff = new StringBuffer();
2232             while (iter.hasNext()) {
2233                 AbstractJcrProperty prop = (AbstractJcrProperty)iter.nextProperty();
2234                 propertyBuff.append(prop).append(", ");
2235             }
2236             return this.getPath() + " {" + propertyBuff.toString() + "}";
2237         } catch (RepositoryException re) {
2238             return re.getMessage();
2239         }
2240     }
2241 
2242     /**
2243      * {@inheritDoc}
2244      * 
2245      * @see java.lang.Object#equals(java.lang.Object)
2246      */
2247     @Override
2248     public boolean equals( Object obj ) {
2249         if (obj == this) return true;
2250         if (obj instanceof AbstractJcrNode) {
2251             AbstractJcrNode that = (AbstractJcrNode)obj;
2252             if (this.cache != that.cache) return false;
2253             return this.location.equals(that.location);
2254         }
2255         return false;
2256     }
2257 
2258     /**
2259      * {@inheritDoc}
2260      * 
2261      * @see java.lang.Object#hashCode()
2262      */
2263     @Override
2264     public int hashCode() {
2265         return HashCode.compute(cache, location.getUuid());
2266     }
2267 
2268     /**
2269      * {@inheritDoc}
2270      * 
2271      * @see javax.jcr.Node#followLifecycleTransition(java.lang.String)
2272      */
2273     @SuppressWarnings( "unused" )
2274     @Override
2275     public void followLifecycleTransition( String transition )
2276         throws UnsupportedRepositoryOperationException, InvalidLifecycleTransitionException, RepositoryException {
2277         throw new UnsupportedRepositoryOperationException();
2278     }
2279 
2280     /**
2281      * {@inheritDoc}
2282      * 
2283      * @see javax.jcr.Node#getAllowedLifecycleTransistions()
2284      */
2285     @Override
2286     public String[] getAllowedLifecycleTransistions() throws UnsupportedRepositoryOperationException, RepositoryException {
2287         throw new UnsupportedRepositoryOperationException();
2288     }
2289 
2290     /**
2291      * {@inheritDoc}
2292      * 
2293      * @see javax.jcr.Node#getSharedSet()
2294      */
2295     @Override
2296     public NodeIterator getSharedSet() throws RepositoryException {
2297         if (isShareable()) {
2298             // Find the nodes that make up this shared set ...
2299             return sharedSet();
2300         }
2301         // Otherwise, the shared set is just this node ...
2302         return new JcrSingleNodeIterator(this);
2303     }
2304 
2305     /**
2306      * Find all of the {@link javax.jcr.Node}s that make up the shared set.
2307      * 
2308      * @return the query result over the nodes in the node set; never null, but possibly empty if the node given by the identifier
2309      *         does not exist or is not a shareable node, or possibly of size 1 if the node given by the identifier does exist and
2310      *         is shareable but has no other nodes in the shared set
2311      * @throws RepositoryException if there is a problem executing the query or finding the shared set
2312      */
2313     NodeIterator sharedSet() throws RepositoryException {
2314         AbstractJcrNode original = this;
2315         String identifierOfSharedNode = getIdentifier();
2316         if (this instanceof JcrSharedNode) {
2317             original = ((JcrSharedNode)this).originalNode();
2318         }
2319         // Execute a query that will report all proxy nodes ...
2320         QueryBuilder builder = new QueryBuilder(context().getValueFactories().getTypeSystem());
2321         QueryCommand query = builder.select("jcr:primaryType")
2322                                     .from("mode:share")
2323                                     .where()
2324                                     .referenceValue("mode:share", "mode:sharedUuid")
2325                                     .isEqualTo(identifierOfSharedNode)
2326                                     .end()
2327                                     .query();
2328         Query jcrQuery = session().workspace().queryManager().createQuery(query);
2329         QueryResult result = jcrQuery.execute();
2330         // And combine the results ...
2331         return new JcrNodeIterator(original, result.getNodes());
2332     }
2333 
2334     /**
2335      * {@inheritDoc}
2336      * 
2337      * @see javax.jcr.Node#removeShare()
2338      */
2339     @Override
2340     public void removeShare() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
2341         if (isShareable()) {
2342             // Get the nodes in the shared set ...
2343             NodeIterator sharedSetNodes = sharedSet();
2344             long sharedSetSize = sharedSetNodes.getSize(); // computed w/o respect for privileges
2345             if (sharedSetSize <= 1) {
2346                 // There aren't any other nodes in the shared set, so simply remove this node ...
2347                 doRemove();
2348                 return;
2349             }
2350             // Find the second node in the shared set that is not this object ...
2351             AbstractJcrNode originalNode = (AbstractJcrNode)sharedSetNodes.nextNode();
2352             if (originalNode == this) {
2353                 // We need to move this node into the first proxy ...
2354                 JcrSharedNode firstProxy = (JcrSharedNode)sharedSetNodes.nextNode();
2355                 assert !this.isRoot();
2356                 assert !firstProxy.isRoot();
2357                 boolean sameParent = firstProxy.getParent().equals(this.getParent());
2358                 NodeEditor parentEditor = firstProxy.editorForParent();
2359                 if (sameParent) {
2360                     // Move this node to just before the other shareable node ...
2361                     parentEditor.orderChildBefore(this.segment(), firstProxy.segment());
2362                     // And finally remove the first proxy ...
2363                     firstProxy.doRemove();
2364                 } else {
2365                     // Find the node immediately following the proxy ...
2366                     Node<JcrNodePayload, JcrPropertyPayload> proxyNode = firstProxy.proxyInfo();
2367                     Node<JcrNodePayload, JcrPropertyPayload> nextChild = parentEditor.node().getChildAfter(proxyNode);
2368                     Name newName = proxyNode.getName();
2369                     // Remove the first proxy ...
2370                     firstProxy.doRemove();
2371                     // Move this node to the new parent ...
2372                     Node<JcrNodePayload, JcrPropertyPayload> newNode = parentEditor.moveToBeChild(this, newName);
2373                     if (nextChild != null) {
2374                         // And place this node where the first proxy was (just before the 'nextChild') ...
2375                         parentEditor.orderChildBefore(newNode.getSegment(), nextChild.getSegment());
2376                     }
2377                 }
2378             } else {
2379                 // We can just remove this proxy ...
2380                 doRemove();
2381             }
2382             return;
2383         }
2384         // If we get to here, either there are no other nodes in the shared set or this node is a non-shareable node,
2385         // so simply remove this node (per section 14.2 of the JCR 2.0 specification) ...
2386         doRemove();
2387     }
2388 
2389     /**
2390      * {@inheritDoc}
2391      * 
2392      * @see javax.jcr.Node#removeSharedSet()
2393      */
2394     @Override
2395     public void removeSharedSet() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
2396         if (isShareable()) {
2397             // Remove all of the node is the shared set ...
2398             NodeIterator sharedSetNodes = sharedSet();
2399             while (sharedSetNodes.hasNext()) {
2400                 AbstractJcrNode nodeInSharedSet = (AbstractJcrNode)sharedSetNodes.nextNode();
2401                 nodeInSharedSet.doRemove();
2402             }
2403         } else {
2404             // Per section 14.2 of the JCR 2.0 specification:
2405             // "In cases where the shared set consists of a single node, or when these methods are
2406             // called on a non-shareable node, their behavior is identical to Node.remove()."
2407             doRemove();
2408         }
2409     }
2410 
2411     /**
2412      * {@inheritDoc}
2413      * <p>
2414      * According to Section 14.3 of the JCR 2.0 specification, an implementation may choose whether the {@link Item#remove()}
2415      * method (and the {@link Session#removeItem(String)} method) behaves as a {@link javax.jcr.Node#removeShare()} or
2416      * {@link javax.jcr.Node#removeSharedSet()}. {@link javax.jcr.Node#removeShare()} just removes this node from the shared set,
2417      * whereas {@link javax.jcr.Node#removeSharedSet()} removes all nodes in the shared set, including the original shared node.
2418      * </p>
2419      * <p>
2420      * ModeShape implements {@link Item#remove()} of a shared node as simply removing this node from the shared set. In other
2421      * words, this method is equivalent to calling {@link #removeShare()}.
2422      * </p>
2423      * 
2424      * @see javax.jcr.Item#remove()
2425      */
2426     @Override
2427     public void remove()
2428         throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException {
2429         // Since this node might be shareable, we want to implement 'remove()' by calling 'removeShare()',
2430         // which will behave correctly even if it is not shareable ...
2431         removeShare();
2432     }
2433 
2434     /**
2435      * Perform a real remove of this node.
2436      * 
2437      * @throws VersionException
2438      * @throws LockException
2439      * @throws ConstraintViolationException
2440      * @throws AccessDeniedException
2441      * @throws RepositoryException
2442      */
2443     protected abstract void doRemove()
2444         throws VersionException, LockException, ConstraintViolationException, AccessDeniedException, RepositoryException;
2445 
2446 }