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.IOException;
27  import java.io.InputStream;
28  import java.io.OutputStream;
29  import java.math.BigDecimal;
30  import java.security.AccessControlException;
31  import java.util.Calendar;
32  import java.util.HashMap;
33  import java.util.HashSet;
34  import java.util.Iterator;
35  import java.util.Map;
36  import java.util.Set;
37  import java.util.UUID;
38  import javax.jcr.Credentials;
39  import javax.jcr.InvalidSerializedDataException;
40  import javax.jcr.Item;
41  import javax.jcr.ItemExistsException;
42  import javax.jcr.ItemNotFoundException;
43  import javax.jcr.NamespaceException;
44  import javax.jcr.Node;
45  import javax.jcr.NodeIterator;
46  import javax.jcr.PathNotFoundException;
47  import javax.jcr.Property;
48  import javax.jcr.PropertyIterator;
49  import javax.jcr.PropertyType;
50  import javax.jcr.ReferentialIntegrityException;
51  import javax.jcr.Repository;
52  import javax.jcr.RepositoryException;
53  import javax.jcr.Session;
54  import javax.jcr.SimpleCredentials;
55  import javax.jcr.UnsupportedRepositoryOperationException;
56  import javax.jcr.Value;
57  import javax.jcr.ValueFactory;
58  import javax.jcr.ValueFormatException;
59  import javax.jcr.Workspace;
60  import javax.jcr.lock.LockException;
61  import javax.jcr.nodetype.ConstraintViolationException;
62  import javax.jcr.query.Query;
63  import javax.jcr.query.QueryResult;
64  import javax.jcr.query.Row;
65  import javax.jcr.query.RowIterator;
66  import javax.jcr.retention.RetentionManager;
67  import javax.jcr.security.AccessControlManager;
68  import javax.jcr.version.VersionException;
69  import net.jcip.annotations.Immutable;
70  import net.jcip.annotations.NotThreadSafe;
71  import org.modeshape.common.util.CheckArg;
72  import org.modeshape.graph.ExecutionContext;
73  import org.modeshape.graph.Graph;
74  import org.modeshape.graph.GraphI18n;
75  import org.modeshape.graph.Location;
76  import org.modeshape.graph.SecurityContext;
77  import org.modeshape.graph.property.Binary;
78  import org.modeshape.graph.property.DateTime;
79  import org.modeshape.graph.property.NamespaceRegistry;
80  import org.modeshape.graph.property.Path;
81  import org.modeshape.graph.property.PathFactory;
82  import org.modeshape.graph.property.Reference;
83  import org.modeshape.graph.property.ReferenceFactory;
84  import org.modeshape.graph.property.ValueFactories;
85  import org.modeshape.graph.property.Path.Segment;
86  import org.modeshape.graph.query.QueryBuilder;
87  import org.modeshape.graph.query.model.QueryCommand;
88  import org.modeshape.graph.query.model.TypeSystem;
89  import org.modeshape.graph.request.InvalidWorkspaceException;
90  import org.modeshape.graph.session.GraphSession;
91  import org.modeshape.jcr.JcrContentHandler.EnclosingSAXException;
92  import org.modeshape.jcr.JcrContentHandler.SaveMode;
93  import org.modeshape.jcr.JcrNamespaceRegistry.Behavior;
94  import org.modeshape.jcr.JcrRepository.Option;
95  import org.modeshape.jcr.SessionCache.JcrPropertyPayload;
96  import org.xml.sax.ContentHandler;
97  import org.xml.sax.InputSource;
98  import org.xml.sax.SAXException;
99  import org.xml.sax.SAXParseException;
100 import org.xml.sax.XMLReader;
101 import org.xml.sax.helpers.XMLReaderFactory;
102 
103 /**
104  * The ModeShape implementation of a {@link Session JCR Session}.
105  */
106 @NotThreadSafe
107 class JcrSession implements Session {
108 
109     private static final String[] NO_ATTRIBUTES_NAMES = new String[] {};
110 
111     /**
112      * The repository that created this session.
113      */
114     private final JcrRepository repository;
115 
116     /**
117      * The workspace that corresponds to this session.
118      */
119     private final JcrWorkspace workspace;
120 
121     /**
122      * A JCR namespace registry that is specific to this session, with any locally-defined namespaces defined in this session.
123      * This is backed by the workspace's namespace registry.
124      */
125     private final JcrNamespaceRegistry sessionRegistry;
126 
127     /**
128      * The execution context for this session, which uses the {@link #sessionRegistry session's namespace registry}
129      */
130     private ExecutionContext executionContext;
131 
132     /**
133      * The session-specific attributes that came from the {@link SimpleCredentials}' {@link SimpleCredentials#getAttributeNames()}
134      */
135     private final Map<String, Object> sessionAttributes;
136 
137     /**
138      * The graph representing this session, which uses the {@link #graph session's graph}.
139      */
140     private final JcrGraph graph;
141 
142     private final SessionCache cache;
143 
144     /**
145      * A cached instance of the root path.
146      */
147     private final Path rootPath;
148 
149     private boolean isLive;
150 
151     private final boolean performReferentialIntegrityChecks;
152     /**
153      * The locations of the nodes that were (transiently) removed in this session and not yet saved.
154      */
155     private Set<Location> removedNodes = null;
156     /**
157      * The UUIDs of the mix:referenceable nodes that were (transiently) removed in this session and not yet saved.
158      */
159     private Set<String> removedReferenceableNodeUuids = null;
160 
161     JcrSession( JcrRepository repository,
162                 JcrWorkspace workspace,
163                 ExecutionContext sessionContext,
164                 NamespaceRegistry globalNamespaceRegistry,
165                 Map<String, Object> sessionAttributes ) {
166         assert repository != null;
167         assert workspace != null;
168         assert sessionAttributes != null;
169         assert sessionContext != null;
170         this.repository = repository;
171         this.sessionAttributes = sessionAttributes;
172         this.workspace = workspace;
173 
174         // Create an execution context for this session, which should use the local namespace registry ...
175         this.executionContext = sessionContext;
176         NamespaceRegistry local = sessionContext.getNamespaceRegistry();
177         this.sessionRegistry = new JcrNamespaceRegistry(Behavior.SESSION, local, globalNamespaceRegistry, this);
178         this.rootPath = this.executionContext.getValueFactories().getPathFactory().createRootPath();
179 
180         // Set up the graph to use for this session (which uses the session's namespace registry and context) ...
181         this.graph = workspace.graph();
182 
183         this.cache = new SessionCache(this);
184         this.isLive = true;
185 
186         this.performReferentialIntegrityChecks = Boolean.valueOf(repository.getOptions()
187                                                                            .get(Option.PERFORM_REFERENTIAL_INTEGRITY_CHECKS))
188                                                         .booleanValue();
189 
190         assert this.sessionAttributes != null;
191         assert this.workspace != null;
192         assert this.repository != null;
193         assert this.executionContext != null;
194         assert this.sessionRegistry != null;
195         assert this.graph != null;
196         assert this.executionContext.getSecurityContext() != null;
197     }
198 
199     // Added to facilitate mock testing of items without necessarily requiring an entire repository structure to be built
200     final SessionCache cache() {
201         return this.cache;
202     }
203 
204     /**
205      * {@inheritDoc}
206      * 
207      * @see javax.jcr.Session#isLive()
208      */
209     public boolean isLive() {
210         return isLive;
211     }
212 
213     /**
214      * Method that verifies that this session is still {@link #isLive() live}.
215      * 
216      * @throws RepositoryException if session has been closed and is no longer usable.
217      */
218     final void checkLive() throws RepositoryException {
219         if (!isLive()) {
220             throw new RepositoryException(JcrI18n.sessionIsNotActive.text(sessionId()));
221         }
222     }
223 
224     ExecutionContext getExecutionContext() {
225         return this.executionContext;
226     }
227 
228     void setSessionData( String key,
229                          String value ) {
230         // This returns the same instance iff the <key,value> would not alter the current context ...
231         this.executionContext = this.executionContext.with(key, value);
232         this.graph.setContext(this.executionContext);
233     }
234 
235     String sessionId() {
236         return this.executionContext.getId();
237     }
238 
239     JcrLockManager lockManager() {
240         return workspace.lockManager();
241     }
242 
243     JcrNodeTypeManager nodeTypeManager() {
244         return this.workspace.nodeTypeManager();
245     }
246 
247     NamespaceRegistry namespaces() {
248         return this.executionContext.getNamespaceRegistry();
249     }
250 
251     void signalNamespaceChanges( boolean global ) {
252         nodeTypeManager().signalNamespaceChanges();
253         if (global) repository.getRepositoryTypeManager().signalNamespaceChanges();
254     }
255 
256     JcrWorkspace workspace() {
257         return this.workspace;
258     }
259 
260     JcrRepository repository() {
261         return this.repository;
262     }
263 
264     Graph.Batch createBatch() {
265         return graph.batch();
266     }
267 
268     Graph graph() {
269         return graph;
270     }
271 
272     String sourceName() {
273         return this.repository.getRepositorySourceName();
274     }
275 
276     Path pathFor( String path,
277                   String parameterName ) throws RepositoryException {
278         try {
279             return this.executionContext.getValueFactories().getPathFactory().create(path);
280 
281         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
282             throw new RepositoryException(JcrI18n.invalidPathParameter.text(path, parameterName), vfe);
283         }
284     }
285 
286     /**
287      * {@inheritDoc}
288      * 
289      * @see javax.jcr.Session#getWorkspace()
290      */
291     public Workspace getWorkspace() {
292         return this.workspace;
293     }
294 
295     /**
296      * {@inheritDoc}
297      * 
298      * @see javax.jcr.Session#getRepository()
299      */
300     public Repository getRepository() {
301         return this.repository;
302     }
303 
304     /**
305      * {@inheritDoc}
306      * 
307      * @return <code>null</code>
308      * @see javax.jcr.Session#getAttribute(java.lang.String)
309      */
310     public Object getAttribute( String name ) {
311         return sessionAttributes.get(name);
312     }
313 
314     /**
315      * {@inheritDoc}
316      * 
317      * @return An empty array
318      * @see javax.jcr.Session#getAttributeNames()
319      */
320     public String[] getAttributeNames() {
321         Set<String> names = sessionAttributes.keySet();
322         if (names.isEmpty()) return NO_ATTRIBUTES_NAMES;
323         return names.toArray(new String[names.size()]);
324     }
325 
326     /**
327      * @return a copy of the session attributes for this session
328      */
329     Map<String, Object> sessionAttributes() {
330         return new HashMap<String, Object>(sessionAttributes);
331     }
332 
333     /**
334      * {@inheritDoc}
335      * 
336      * @see javax.jcr.Session#getNamespacePrefix(java.lang.String)
337      */
338     public String getNamespacePrefix( String uri ) throws RepositoryException {
339         return sessionRegistry.getPrefix(uri);
340     }
341 
342     /**
343      * {@inheritDoc}
344      * 
345      * @see javax.jcr.Session#getNamespacePrefixes()
346      */
347     public String[] getNamespacePrefixes() throws RepositoryException {
348         return sessionRegistry.getPrefixes();
349     }
350 
351     /**
352      * {@inheritDoc}
353      * 
354      * @see javax.jcr.Session#getNamespaceURI(java.lang.String)
355      */
356     public String getNamespaceURI( String prefix ) throws RepositoryException {
357         return sessionRegistry.getURI(prefix);
358     }
359 
360     /**
361      * {@inheritDoc}
362      * 
363      * @see javax.jcr.Session#setNamespacePrefix(java.lang.String, java.lang.String)
364      */
365     public void setNamespacePrefix( String newPrefix,
366                                     String existingUri ) throws NamespaceException, RepositoryException {
367         sessionRegistry.registerNamespace(newPrefix, existingUri);
368     }
369 
370     /**
371      * {@inheritDoc}
372      * 
373      * @see javax.jcr.Session#addLockToken(java.lang.String)
374      */
375     public void addLockToken( String lt ) {
376         CheckArg.isNotNull(lt, "lock token");
377 
378         try {
379             lockManager().addLockToken(lt);
380         } catch (LockException le) {
381             // For backwards compatibility (and API compatibility), the LockExceptions from the LockManager need to get swallowed
382         }
383     }
384 
385     /**
386      * Returns whether the authenticated user has the given role.
387      * 
388      * @param roleName the name of the role to check
389      * @param workspaceName the workspace under which the user must have the role. This may be different from the current
390      *        workspace.
391      * @return true if the user has the role and is logged in; false otherwise
392      */
393     final boolean hasRole( String roleName,
394                            String workspaceName ) {
395         SecurityContext context = getExecutionContext().getSecurityContext();
396         if (context.hasRole(roleName)) return true;
397         roleName = roleName + "." + this.repository.getRepositorySourceName();
398         if (context.hasRole(roleName)) return true;
399         roleName = roleName + "." + workspaceName;
400         return context.hasRole(roleName);
401     }
402 
403     /**
404      * {@inheritDoc}
405      * 
406      * @throws IllegalArgumentException if either <code>path</code> or <code>actions</code> is empty or <code>null</code>.
407      * @see javax.jcr.Session#checkPermission(java.lang.String, java.lang.String)
408      */
409     public void checkPermission( String path,
410                                  String actions ) {
411         CheckArg.isNotEmpty(path, "path");
412 
413         this.checkPermission(executionContext.getValueFactories().getPathFactory().create(path), actions);
414     }
415 
416     /**
417      * Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
418      * current workspace, otherwise returns silently.
419      * <p>
420      * The {@code path} parameter is included for future use and is currently ignored
421      * </p>
422      * 
423      * @param path the path on which the actions are occurring
424      * @param actions a comma-delimited list of actions to check
425      */
426     void checkPermission( Path path,
427                           String actions ) {
428         checkPermission(this.workspace().getName(), path, actions);
429     }
430 
431     /**
432      * Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
433      * named workspace, otherwise returns silently.
434      * <p>
435      * The {@code path} parameter is included for future use and is currently ignored
436      * </p>
437      * 
438      * @param workspaceName the name of the workspace in which the path exists
439      * @param path the path on which the actions are occurring
440      * @param actions a comma-delimited list of actions to check
441      */
442     void checkPermission( String workspaceName,
443                           Path path,
444                           String actions ) {
445         if (hasPermission(workspaceName, path, actions)) return;
446 
447         String pathAsString = path != null ? path.getString(this.namespaces()) : "<unknown>";
448         throw new AccessControlException(JcrI18n.permissionDenied.text(pathAsString, actions));
449     }
450 
451     /**
452      * A companion method to {@link #checkPermission(String, String)} that returns false (instead of throwing an exception) if the
453      * current session doesn't have sufficient privileges to perform the given list of actions at the given path.
454      * 
455      * @param path the path at which the privileges are to be checked
456      * @param actions a comma-delimited list of actions to check
457      * @return true if the current session has sufficient privileges to perform all of the actions on the the given path; false
458      *         otherwise
459      * @see javax.jcr.Session#checkPermission(java.lang.String, java.lang.String)
460      */
461     public boolean hasPermission( String path,
462                                   String actions ) {
463         CheckArg.isNotEmpty(path, "path");
464 
465         return hasPermission(this.workspace().getName(),
466                              executionContext.getValueFactories().getPathFactory().create(path),
467                              actions);
468     }
469 
470     private boolean hasPermission( String workspaceName,
471                                    Path path,
472                                    String actions ) {
473         CheckArg.isNotEmpty(actions, "actions");
474 
475         boolean hasPermission = true;
476         for (String action : actions.split(",")) {
477             if (ModeShapePermissions.READ.equals(action)) {
478                 hasPermission &= hasRole(ModeShapeRoles.READONLY, workspaceName)
479                                  || hasRole(ModeShapeRoles.READWRITE, workspaceName)
480                                  || hasRole(ModeShapeRoles.ADMIN, workspaceName);
481             } else if (ModeShapePermissions.REGISTER_NAMESPACE.equals(action)
482                        || ModeShapePermissions.REGISTER_TYPE.equals(action) || ModeShapePermissions.UNLOCK_ANY.equals(action)
483                        || ModeShapePermissions.CREATE_WORKSPACE.equals(action)
484                        || ModeShapePermissions.DELETE_WORKSPACE.equals(action)) {
485                 hasPermission &= hasRole(ModeShapeRoles.ADMIN, workspaceName);
486             } else {
487                 hasPermission &= hasRole(ModeShapeRoles.ADMIN, workspaceName) || hasRole(ModeShapeRoles.READWRITE, workspaceName);
488             }
489         }
490         return hasPermission;
491     }
492 
493     /**
494      * Makes a "best effort" determination of whether the given method can be successfully called on the given target with the
495      * given arguments. A return value of {@code false} indicates that the method would not succeed. A return value of {@code
496      * true} indicates that the method <i>might</i> succeed.
497      * 
498      * @param methodName the method to invoke; may not be null
499      * @param target the object on which to invoke it; may not be null
500      * @param arguments the arguments to pass to the method; varies depending on the method
501      * @return true if the given method can be determined to be supported, or false otherwise
502      * @throws IllegalArgumentException
503      * @throws RepositoryException
504      */
505     public boolean hasCapability( String methodName,
506                                   Object target,
507                                   Object[] arguments ) throws IllegalArgumentException, RepositoryException {
508         CheckArg.isNotEmpty(methodName, "methodName");
509         CheckArg.isNotNull(target, "target");
510 
511         if (target instanceof AbstractJcrNode) {
512             AbstractJcrNode node = (AbstractJcrNode)target;
513             if ("addNode".equals(methodName)) {
514                 CheckArg.hasSizeOfAtLeast(arguments, 1, "arguments");
515                 CheckArg.hasSizeOfAtMost(arguments, 2, "arguments");
516                 CheckArg.isInstanceOf(arguments[0], String.class, "arguments[0]");
517 
518                 String relPath = (String)arguments[0];
519                 String primaryNodeTypeName = null;
520                 if (arguments.length > 1) {
521                     CheckArg.isInstanceOf(arguments[1], String.class, "arguments[1]");
522                     primaryNodeTypeName = (String)arguments[1];
523                 }
524                 return node.canAddNode(relPath, primaryNodeTypeName);
525             }
526         }
527         return true;
528     }
529 
530     /**
531      * {@inheritDoc}
532      * 
533      * @see javax.jcr.Session#exportDocumentView(java.lang.String, org.xml.sax.ContentHandler, boolean, boolean)
534      */
535     public void exportDocumentView( String absPath,
536                                     ContentHandler contentHandler,
537                                     boolean skipBinary,
538                                     boolean noRecurse ) throws RepositoryException, SAXException {
539         CheckArg.isNotNull(absPath, "absPath");
540         CheckArg.isNotNull(contentHandler, "contentHandler");
541         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
542         Node exportRootNode = getNode(exportRootPath);
543         AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
544         exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
545     }
546 
547     /**
548      * {@inheritDoc}
549      * 
550      * @see javax.jcr.Session#exportDocumentView(java.lang.String, java.io.OutputStream, boolean, boolean)
551      */
552     public void exportDocumentView( String absPath,
553                                     OutputStream out,
554                                     boolean skipBinary,
555                                     boolean noRecurse ) throws RepositoryException {
556         CheckArg.isNotNull(absPath, "absPath");
557         CheckArg.isNotNull(out, "out");
558         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
559         Node exportRootNode = getNode(exportRootPath);
560         AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
561         exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
562     }
563 
564     /**
565      * {@inheritDoc}
566      * 
567      * @see javax.jcr.Session#exportSystemView(java.lang.String, org.xml.sax.ContentHandler, boolean, boolean)
568      */
569     public void exportSystemView( String absPath,
570                                   ContentHandler contentHandler,
571                                   boolean skipBinary,
572                                   boolean noRecurse ) throws RepositoryException, SAXException {
573         CheckArg.isNotNull(absPath, "absPath");
574         CheckArg.isNotNull(contentHandler, "contentHandler");
575         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
576         Node exportRootNode = getNode(exportRootPath);
577         AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
578         exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
579     }
580 
581     /**
582      * {@inheritDoc}
583      * 
584      * @see javax.jcr.Session#exportSystemView(java.lang.String, java.io.OutputStream, boolean, boolean)
585      */
586     public void exportSystemView( String absPath,
587                                   OutputStream out,
588                                   boolean skipBinary,
589                                   boolean noRecurse ) throws RepositoryException {
590         CheckArg.isNotNull(absPath, "absPath");
591         CheckArg.isNotNull(out, "out");
592         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
593         Node exportRootNode = getNode(exportRootPath);
594         AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
595         exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
596     }
597 
598     /**
599      * {@inheritDoc}
600      * 
601      * @see javax.jcr.Session#getImportContentHandler(java.lang.String, int)
602      */
603     public ContentHandler getImportContentHandler( String parentAbsPath,
604                                                    int uuidBehavior ) throws PathNotFoundException, RepositoryException {
605         Path parentPath = this.executionContext.getValueFactories().getPathFactory().create(parentAbsPath);
606         return new JcrContentHandler(this, parentPath, uuidBehavior, SaveMode.SESSION);
607     }
608 
609     /**
610      * {@inheritDoc}
611      * 
612      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
613      * @see javax.jcr.Session#getItem(java.lang.String)
614      */
615     public Item getItem( String absolutePath ) throws RepositoryException {
616         CheckArg.isNotEmpty(absolutePath, "absolutePath");
617         // Return root node if path is "/"
618         Path path = executionContext.getValueFactories().getPathFactory().create(absolutePath);
619         if (path.isRoot()) {
620             return getRootNode();
621         }
622         // Since we don't know whether path refers to a node or a property, look to see if we can tell it's a node ...
623         if (path.isIdentifier() || path.getLastSegment().hasIndex()) {
624             return getNode(path);
625         }
626         // We can't tell from the name, so ask for an item ...
627         try {
628             return cache.findJcrItem(null, rootPath, path.relativeTo(rootPath));
629         } catch (ItemNotFoundException e) {
630             throw new PathNotFoundException(e.getMessage(), e);
631         }
632     }
633 
634     /**
635      * @param absolutePath an absolute path
636      * @return the specified node
637      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
638      * @throws PathNotFoundException If no accessible node is found at the specifed path
639      * @throws RepositoryException if another error occurs
640      * @see javax.jcr.Session#getItem(java.lang.String)
641      */
642     public AbstractJcrNode getNode( String absolutePath ) throws PathNotFoundException, RepositoryException {
643         CheckArg.isNotEmpty(absolutePath, "absolutePath");
644         // Return root node if path is "/"
645         Path path = executionContext.getValueFactories().getPathFactory().create(absolutePath);
646         if (path.isRoot()) {
647             return getRootNode();
648         }
649         return getNode(path);
650     }
651 
652     /**
653      * Returns true if a node exists at the given path and is accessible to the current user.
654      * 
655      * @param absolutePath the absolute path to the node
656      * @return true if a node exists at absolute path and is accessible to the current user.
657      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
658      * @throws PathNotFoundException If no accessible node is found at the specifed path
659      * @throws RepositoryException if another error occurs
660      */
661     public boolean nodeExists( String absolutePath ) throws PathNotFoundException, RepositoryException {
662         CheckArg.isNotEmpty(absolutePath, "absolutePath");
663         // Return root node if path is "/"
664         Path path = executionContext.getValueFactories().getPathFactory().create(absolutePath);
665         if (path.isRoot()) {
666             return true;
667         }
668         try {
669             cache.findJcrNode(null, path);
670             return true;
671         } catch (ItemNotFoundException e) {
672             return false;
673         }
674     }
675 
676     /**
677      * @param absolutePath an absolute path
678      * @return the specified node
679      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
680      * @throws PathNotFoundException If no accessible node is found at the specifed path
681      * @throws RepositoryException if another error occurs
682      * @see javax.jcr.Session#getItem(java.lang.String)
683      */
684     public AbstractJcrProperty getProperty( String absolutePath ) throws PathNotFoundException, RepositoryException {
685         CheckArg.isNotEmpty(absolutePath, "absolutePath");
686         // Return root node if path is "/"
687         Path path = pathFor(absolutePath, "absolutePath");
688         if (path.isRoot()) {
689             throw new PathNotFoundException(JcrI18n.rootNodeIsNotProperty.text());
690         }
691         if (path.isIdentifier()) {
692             throw new PathNotFoundException(JcrI18n.identifierPathNeverReferencesProperty.text());
693         }
694 
695         Segment lastSegment = path.getLastSegment();
696         if (lastSegment.hasIndex()) {
697             throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(absolutePath));
698         }
699 
700         // This will throw a PNFE if the parent path does not exist
701         AbstractJcrNode parentNode = getNode(path.getParent());
702         AbstractJcrProperty property = parentNode.getProperty(lastSegment.getName());
703 
704         if (property == null) {
705             throw new PathNotFoundException(GraphI18n.pathNotFoundExceptionLowestExistingLocationFound.text(absolutePath,
706                                                                                                             parentNode.getPath()));
707         }
708         return property;
709     }
710 
711     /**
712      * Returns true if a property exists at the given path and is accessible to the current user.
713      * 
714      * @param absolutePath the absolute path to the property
715      * @return true if a property exists at absolute path and is accessible to the current user.
716      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
717      * @throws RepositoryException if another error occurs
718      */
719     public boolean propertyExists( String absolutePath ) throws RepositoryException {
720         CheckArg.isNotEmpty(absolutePath, "absolutePath");
721         // Return root node if path is "/"
722         Path path = pathFor(absolutePath, "absolutePath");
723         if (path.isRoot() || path.isIdentifier()) {
724             return false;
725         }
726 
727         Segment lastSegment = path.getLastSegment();
728         if (lastSegment.hasIndex()) {
729             throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(absolutePath));
730         }
731 
732         try {
733             // This will throw a PNFE if the parent path does not exist
734             AbstractJcrNode parentNode = getNode(path.getParent());
735             return parentNode.hasProperty(lastSegment.getName());
736         } catch (PathNotFoundException pnfe) {
737             return false;
738         }
739     }
740 
741     public void removeItem( String absolutePath ) throws RepositoryException {
742         Item item = getItem(absolutePath);
743         item.remove();
744     }
745 
746     /**
747      * {@inheritDoc}
748      * 
749      * @see javax.jcr.Session#getLockTokens()
750      */
751     public String[] getLockTokens() {
752         return lockManager().getLockTokens();
753     }
754 
755     /**
756      * Find or create a JCR Node for the given path. This method works for the root node, too.
757      * 
758      * @param path the path; may not be null
759      * @return the JCR node instance for the given path; never null
760      * @throws PathNotFoundException if the path could not be found
761      * @throws RepositoryException if there is a problem
762      */
763     AbstractJcrNode getNode( Path path ) throws RepositoryException, PathNotFoundException {
764         if (path.isRoot()) return cache.findJcrRootNode();
765         try {
766             if (path.isIdentifier()) {
767                 // Convert the path to a UUID ...
768                 try {
769                     UUID uuid = executionContext.getValueFactories().getUuidFactory().create(path);
770                     return cache.findJcrNode(Location.create(uuid));
771                 } catch (org.modeshape.graph.property.ValueFormatException e) {
772                     // The identifier path didn't contain a UUID (but another identifier form) ...
773                     String pathStr = executionContext.getValueFactories().getStringFactory().create(path);
774                     throw new PathNotFoundException(JcrI18n.identifierPathContainedUnsupportedIdentifierFormat.text(pathStr));
775                 }
776             }
777             return cache.findJcrNode(null, path);
778         } catch (ItemNotFoundException e) {
779             throw new PathNotFoundException(e.getMessage());
780         }
781     }
782 
783     /**
784      * {@inheritDoc}
785      * 
786      * @see javax.jcr.Session#getNodeByUUID(java.lang.String)
787      */
788     public AbstractJcrNode getNodeByUUID( String uuid ) throws ItemNotFoundException, RepositoryException {
789         return cache.findJcrNode(Location.create(UUID.fromString(uuid)));
790     }
791 
792     /**
793      * {@inheritDoc}
794      * 
795      * @see javax.jcr.Session#getNodeByIdentifier(java.lang.String)
796      */
797     @Override
798     public AbstractJcrNode getNodeByIdentifier( String id ) throws ItemNotFoundException, RepositoryException {
799         // Attempt to create a UUID from the identifier ...
800         try {
801             return cache.findJcrNode(Location.create(UUID.fromString(id)));
802         } catch (IllegalArgumentException e) {
803             try {
804                 // See if it's a path ...
805                 PathFactory pathFactory = executionContext.getValueFactories().getPathFactory();
806                 Path path = pathFactory.create(id);
807                 return getNode(path);
808             } catch (org.modeshape.graph.property.ValueFormatException e2) {
809                 // It's not a path either ...
810                 throw new RepositoryException(JcrI18n.identifierPathContainedUnsupportedIdentifierFormat.text(id));
811             }
812         }
813     }
814 
815     /**
816      * {@inheritDoc}
817      * 
818      * @see javax.jcr.Session#getRootNode()
819      */
820     public AbstractJcrNode getRootNode() throws RepositoryException {
821         return cache.findJcrRootNode();
822     }
823 
824     /**
825      * {@inheritDoc}
826      * 
827      * @see javax.jcr.Session#getUserID()
828      * @see SecurityContext#getUserName()
829      */
830     public String getUserID() {
831         return executionContext.getSecurityContext().getUserName();
832     }
833 
834     /**
835      * {@inheritDoc}
836      * 
837      * @see javax.jcr.Session#getValueFactory()
838      */
839     public ValueFactory getValueFactory() {
840         final ValueFactories valueFactories = executionContext.getValueFactories();
841         final SessionCache sessionCache = this.cache;
842 
843         return new ValueFactory() {
844 
845             @Override
846             public Value createValue( String value,
847                                       int propertyType ) throws ValueFormatException {
848                 return new JcrValue(valueFactories, sessionCache, propertyType, convertValueToType(value, propertyType));
849             }
850 
851             @Override
852             public Value createValue( Node value ) throws RepositoryException {
853                 if (!value.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(JcrSession.this.namespaces()))) {
854                     throw new RepositoryException(JcrI18n.nodeNotReferenceable.text());
855                 }
856                 Reference ref = valueFactories.getReferenceFactory().create(value.getIdentifier());
857                 return new JcrValue(valueFactories, sessionCache, PropertyType.REFERENCE, ref);
858             }
859 
860             @Override
861             public Value createValue( Node value,
862                                       boolean weak ) throws RepositoryException {
863                 if (!value.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(JcrSession.this.namespaces()))) {
864                     throw new RepositoryException(JcrI18n.nodeNotReferenceable.text());
865                 }
866                 ReferenceFactory factory = weak ? valueFactories.getWeakReferenceFactory() : valueFactories.getReferenceFactory();
867                 int refType = weak ? PropertyType.WEAKREFERENCE : PropertyType.REFERENCE;
868                 Reference ref = factory.create(value.getIdentifier());
869                 return new JcrValue(valueFactories, sessionCache, refType, ref);
870             }
871 
872             @Override
873             public Value createValue( javax.jcr.Binary value ) {
874                 return new JcrValue(valueFactories, sessionCache, PropertyType.BINARY, value);
875             }
876 
877             @Override
878             public Value createValue( InputStream value ) {
879                 Binary binary = valueFactories.getBinaryFactory().create(value);
880                 return new JcrValue(valueFactories, sessionCache, PropertyType.BINARY, binary);
881             }
882 
883             @Override
884             public javax.jcr.Binary createBinary( InputStream value ) {
885                 Binary binary = valueFactories.getBinaryFactory().create(value);
886                 return new JcrBinary(binary);
887             }
888 
889             @Override
890             public Value createValue( Calendar value ) {
891                 DateTime dateTime = valueFactories.getDateFactory().create(value);
892                 return new JcrValue(valueFactories, sessionCache, PropertyType.DATE, dateTime);
893             }
894 
895             @Override
896             public Value createValue( boolean value ) {
897                 return new JcrValue(valueFactories, sessionCache, PropertyType.BOOLEAN, value);
898             }
899 
900             @Override
901             public Value createValue( double value ) {
902                 return new JcrValue(valueFactories, sessionCache, PropertyType.DOUBLE, value);
903             }
904 
905             @Override
906             public Value createValue( long value ) {
907                 return new JcrValue(valueFactories, sessionCache, PropertyType.LONG, value);
908             }
909 
910             @Override
911             public Value createValue( String value ) {
912                 return new JcrValue(valueFactories, sessionCache, PropertyType.STRING, value);
913             }
914 
915             @Override
916             public Value createValue( BigDecimal value ) {
917                 return new JcrValue(valueFactories, sessionCache, PropertyType.DECIMAL, value);
918             }
919 
920             Object convertValueToType( Object value,
921                                        int toType ) throws ValueFormatException {
922                 switch (toType) {
923                     case PropertyType.BOOLEAN:
924                         try {
925                             return valueFactories.getBooleanFactory().create(value);
926                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
927                             throw new ValueFormatException(vfe);
928                         }
929 
930                     case PropertyType.DATE:
931                         try {
932                             return valueFactories.getDateFactory().create(value);
933                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
934                             throw new ValueFormatException(vfe);
935                         }
936 
937                     case PropertyType.NAME:
938                         try {
939                             return valueFactories.getNameFactory().create(value);
940                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
941                             throw new ValueFormatException(vfe);
942                         }
943 
944                     case PropertyType.PATH:
945                         try {
946                             return valueFactories.getPathFactory().create(value);
947                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
948                             throw new ValueFormatException(vfe);
949                         }
950 
951                     case PropertyType.REFERENCE:
952                     case PropertyType.WEAKREFERENCE:
953                         try {
954                             return valueFactories.getReferenceFactory().create(value);
955                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
956                             throw new ValueFormatException(vfe);
957                         }
958                     case PropertyType.DOUBLE:
959                         try {
960                             return valueFactories.getDoubleFactory().create(value);
961                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
962                             throw new ValueFormatException(vfe);
963                         }
964                     case PropertyType.LONG:
965                         try {
966                             return valueFactories.getLongFactory().create(value);
967                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
968                             throw new ValueFormatException(vfe);
969                         }
970                     case PropertyType.DECIMAL:
971                         try {
972                             return valueFactories.getDecimalFactory().create(value);
973                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
974                             throw new ValueFormatException(vfe);
975                         }
976                     case PropertyType.URI:
977                         try {
978                             return valueFactories.getUriFactory().create(value);
979                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
980                             throw new ValueFormatException(vfe);
981                         }
982 
983                         // Anything can be converted to these types
984                     case PropertyType.BINARY:
985                         try {
986                             return valueFactories.getBinaryFactory().create(value);
987                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
988                             throw new ValueFormatException(vfe);
989                         }
990                     case PropertyType.STRING:
991                         try {
992                             return valueFactories.getStringFactory().create(value);
993                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
994                             throw new ValueFormatException(vfe);
995                         }
996                     case PropertyType.UNDEFINED:
997                         return value;
998 
999                     default:
1000                         assert false : "Unexpected JCR property type " + toType;
1001                         // This should still throw an exception even if assertions are turned off
1002                         throw new IllegalStateException("Invalid property type " + toType);
1003                 }
1004             }
1005 
1006         };
1007     }
1008 
1009     /**
1010      * {@inheritDoc}
1011      * 
1012      * @see javax.jcr.Session#hasPendingChanges()
1013      */
1014     public boolean hasPendingChanges() {
1015         return cache.hasPendingChanges();
1016     }
1017 
1018     /**
1019      * {@inheritDoc}
1020      * 
1021      * @see javax.jcr.Session#impersonate(javax.jcr.Credentials)
1022      */
1023     public Session impersonate( Credentials credentials ) throws RepositoryException {
1024         return repository.login(credentials, this.workspace.getName());
1025     }
1026 
1027     /**
1028      * Returns a new {@link JcrSession session} that uses the same security information to create a session that points to the
1029      * named workspace.
1030      * 
1031      * @param workspaceName the name of the workspace to connect to
1032      * @return a new session that uses the named workspace
1033      * @throws RepositoryException if an error occurs creating the session
1034      */
1035     JcrSession with( String workspaceName ) throws RepositoryException {
1036         return repository.sessionForContext(executionContext, workspaceName, sessionAttributes);
1037     }
1038 
1039     /**
1040      * {@inheritDoc}
1041      * 
1042      * @see javax.jcr.Session#importXML(java.lang.String, java.io.InputStream, int)
1043      */
1044     public void importXML( String parentAbsPath,
1045                            InputStream in,
1046                            int uuidBehavior ) throws IOException, InvalidSerializedDataException, RepositoryException {
1047 
1048         try {
1049             XMLReader parser = XMLReaderFactory.createXMLReader();
1050 
1051             parser.setContentHandler(getImportContentHandler(parentAbsPath, uuidBehavior));
1052             parser.parse(new InputSource(in));
1053         } catch (EnclosingSAXException ese) {
1054             Exception cause = ese.getException();
1055             if (cause instanceof ItemExistsException) {
1056                 throw (ItemExistsException)cause;
1057             } else if (cause instanceof ConstraintViolationException) {
1058                 throw (ConstraintViolationException)cause;
1059             } else if (cause instanceof VersionException) {
1060                 throw (VersionException)cause;
1061             }
1062             throw new RepositoryException(cause);
1063         } catch (SAXParseException se) {
1064             throw new InvalidSerializedDataException(se);
1065         } catch (SAXException se) {
1066             throw new RepositoryException(se);
1067         }
1068     }
1069 
1070     /**
1071      * {@inheritDoc}
1072      * 
1073      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
1074      * @see javax.jcr.Session#itemExists(java.lang.String)
1075      */
1076     public boolean itemExists( String absolutePath ) throws RepositoryException {
1077         try {
1078             return (getItem(absolutePath) != null);
1079         } catch (PathNotFoundException error) {
1080             return false;
1081         }
1082     }
1083 
1084     /**
1085      * {@inheritDoc}
1086      * 
1087      * @see javax.jcr.Session#logout()
1088      */
1089     public void logout() {
1090         terminate(true);
1091     }
1092 
1093     /**
1094      * This method is called by {@link #logout()} and by {@link JcrRepository#terminateAllSessions()}. It should not be called
1095      * from anywhere else.
1096      * 
1097      * @param removeFromActiveSession true if the session should be removed from the active session list
1098      */
1099     void terminate( boolean removeFromActiveSession ) {
1100         if (!isLive()) {
1101             return;
1102         }
1103 
1104         isLive = false;
1105         this.workspace().observationManager().removeAllEventListeners();
1106         this.lockManager().cleanLocks();
1107         if (removeFromActiveSession) this.repository.sessionLoggedOut(this);
1108         this.executionContext.getSecurityContext().logout();
1109     }
1110 
1111     /**
1112      * {@inheritDoc}
1113      * 
1114      * @see javax.jcr.Session#move(java.lang.String, java.lang.String)
1115      */
1116     public void move( String srcAbsPath,
1117                       String destAbsPath ) throws ItemExistsException, RepositoryException {
1118         CheckArg.isNotNull(srcAbsPath, "srcAbsPath");
1119         CheckArg.isNotNull(destAbsPath, "destAbsPath");
1120 
1121         PathFactory pathFactory = executionContext.getValueFactories().getPathFactory();
1122         Path destPath = pathFactory.create(destAbsPath);
1123 
1124         // Doing a literal test here because the path factory will canonicalize "/node[1]" to "/node"
1125         if (destAbsPath.endsWith("]")) {
1126             throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(destAbsPath));
1127         }
1128 
1129         Path.Segment newNodeName = null;
1130         AbstractJcrNode sourceNode = getNode(pathFactory.create(srcAbsPath));
1131         AbstractJcrNode newParentNode = null;
1132         if (destPath.isIdentifier()) {
1133             AbstractJcrNode existingDestNode = getNode(destPath);
1134             newParentNode = existingDestNode.getParent();
1135             newNodeName = existingDestNode.segment();
1136         } else {
1137             newParentNode = getNode(destPath.getParent());
1138             newNodeName = destPath.getSegment(destPath.size() - 1);
1139         }
1140 
1141         if (sourceNode.isLocked() && !sourceNode.getLock().isLockOwningSession()) {
1142             javax.jcr.lock.Lock sourceLock = sourceNode.getLock();
1143             if (sourceLock != null && sourceLock.getLockToken() == null) {
1144                 throw new LockException(JcrI18n.lockTokenNotHeld.text(srcAbsPath));
1145             }
1146         }
1147 
1148         if (newParentNode.isLocked() && !newParentNode.getLock().isLockOwningSession()) {
1149             javax.jcr.lock.Lock newParentLock = newParentNode.getLock();
1150             if (newParentLock != null && newParentLock.getLockToken() == null) {
1151                 throw new LockException(JcrI18n.lockTokenNotHeld.text(destAbsPath));
1152             }
1153         }
1154 
1155         if (!sourceNode.getParent().isCheckedOut()) {
1156             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(sourceNode.getPath()));
1157         }
1158 
1159         if (!newParentNode.isCheckedOut()) {
1160             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(newParentNode.getPath()));
1161         }
1162 
1163         newParentNode.editor().moveToBeChild(sourceNode, newNodeName.getName());
1164     }
1165 
1166     /**
1167      * {@inheritDoc}
1168      * 
1169      * @see javax.jcr.Session#refresh(boolean)
1170      */
1171     public void refresh( boolean keepChanges ) {
1172         this.cache.refresh(keepChanges);
1173     }
1174 
1175     /**
1176      * {@inheritDoc}
1177      * 
1178      * @see javax.jcr.Session#removeLockToken(java.lang.String)
1179      */
1180     public void removeLockToken( String lockToken ) {
1181         CheckArg.isNotNull(lockToken, "lock token");
1182         // A LockException is thrown if the lock associated with the specified lock token is session-scoped.
1183         try {
1184             lockManager().removeLockToken(lockToken);
1185         } catch (LockException le) {
1186             // For backwards compatibility (and API compatibility), the LockExceptions from the LockManager need to get swallowed
1187         }
1188     }
1189 
1190     void recordRemoval( Location location ) throws RepositoryException {
1191         if (!performReferentialIntegrityChecks) {
1192             return;
1193         }
1194         if (removedNodes == null) {
1195             removedNodes = new HashSet<Location>();
1196             removedReferenceableNodeUuids = new HashSet<String>();
1197         }
1198 
1199         // Find the UUIDs of all of the mix:referenceable nodes that are below this node being removed ...
1200         Path path = location.getPath();
1201         org.modeshape.graph.property.ValueFactory<String> stringFactory = executionContext.getValueFactories().getStringFactory();
1202         String pathStr = stringFactory.create(path);
1203         int sns = path.getLastSegment().getIndex();
1204         if (sns == Path.DEFAULT_INDEX) pathStr = pathStr + "[1]";
1205 
1206         TypeSystem typeSystem = executionContext.getValueFactories().getTypeSystem();
1207         QueryBuilder builder = new QueryBuilder(typeSystem);
1208         QueryCommand query = builder.select("jcr:uuid")
1209                                     .from("mix:referenceable AS referenceable")
1210                                     .where()
1211                                     .path("referenceable")
1212                                     .isLike(pathStr + "%")
1213                                     .end()
1214                                     .query();
1215         JcrQueryManager queryManager = workspace().queryManager();
1216         Query jcrQuery = queryManager.createQuery(query);
1217         QueryResult result = jcrQuery.execute();
1218         RowIterator rows = result.getRows();
1219         while (rows.hasNext()) {
1220             Row row = rows.nextRow();
1221             String uuid = row.getValue("jcr:uuid").getString();
1222             if (uuid != null) removedReferenceableNodeUuids.add(uuid);
1223         }
1224 
1225         // Now record that this location is being removed ...
1226         Set<Location> extras = null;
1227         for (Location alreadyDeleted : removedNodes) {
1228             Path alreadyDeletedPath = alreadyDeleted.getPath();
1229             if (alreadyDeletedPath.isAtOrAbove(path)) {
1230                 // Already covered by the alreadyDeleted location ...
1231                 return;
1232             }
1233             if (alreadyDeletedPath.isDecendantOf(path)) {
1234                 // The path being deleted is above the path that was already deleted, so remove the already-deleted one ...
1235                 if (extras == null) {
1236                     extras = new HashSet<Location>();
1237                 }
1238                 extras.add(alreadyDeleted);
1239             }
1240         }
1241         // Not covered by any already-deleted location, so add it ...
1242         removedNodes.add(location);
1243         if (extras != null) {
1244             // Remove the nodes that will be covered by the node being deleted now ...
1245             removedNodes.removeAll(extras);
1246         }
1247     }
1248 
1249     boolean wasRemovedInSession( Location location ) {
1250         if (removedNodes == null) return false;
1251         if (removedNodes.contains(location)) return true;
1252         Path path = location.getPath();
1253         for (Location removed : removedNodes) {
1254             if (removed.getPath().isAtOrAbove(path)) return true;
1255         }
1256         return false;
1257     }
1258 
1259     boolean wasRemovedInSession( UUID uuid ) {
1260         if (removedReferenceableNodeUuids == null) return false;
1261         return removedReferenceableNodeUuids.contains(uuid);
1262 
1263     }
1264 
1265     /**
1266      * Determine whether there is at least one other node outside this branch that has a reference to nodes within the branch
1267      * rooted by this node.
1268      * 
1269      * @param subgraphRoot the root of the subgraph under which the references should be checked, or null if the root node should
1270      *        be used (meaning all references in the workspace should be checked)
1271      * @throws ReferentialIntegrityException if the changes would leave referential integrity problems
1272      * @throws RepositoryException if an error occurs while obtaining the information
1273      */
1274     void checkReferentialIntegrityOfChanges( AbstractJcrNode subgraphRoot )
1275         throws ReferentialIntegrityException, RepositoryException {
1276         if (removedNodes == null) return;
1277         if (removedReferenceableNodeUuids.isEmpty()) return;
1278 
1279         if (removedNodes.size() == 1 && removedNodes.iterator().next().getPath().isRoot()) {
1280             // The root node is being removed, so there will be no referencing nodes remaining ...
1281             return;
1282         }
1283 
1284         String subgraphPath = null;
1285         if (subgraphRoot != null) {
1286             subgraphPath = subgraphRoot.getPath();
1287             if (subgraphRoot.getIndex() == Path.DEFAULT_INDEX) subgraphPath = subgraphPath + "[1]";
1288         }
1289 
1290         // Build one (or several) queries to find the first reference to any 'mix:referenceable' nodes
1291         // that have been (transiently) removed from the session ...
1292         int maxBatchSize = 100;
1293         Set<Object> someUuidsInBranch = new HashSet<Object>();
1294         Iterator<String> uuidIter = removedReferenceableNodeUuids.iterator();
1295         while (uuidIter.hasNext()) {
1296             // Accumulate the next 100 UUIDs of referenceable nodes inside this branch ...
1297             while (uuidIter.hasNext() && someUuidsInBranch.size() <= maxBatchSize) {
1298                 String uuid = uuidIter.next();
1299                 someUuidsInBranch.add(uuid);
1300             }
1301             assert !someUuidsInBranch.isEmpty();
1302             // Now issue a query to see if any nodes outside this branch references these referenceable nodes ...
1303             TypeSystem typeSystem = executionContext.getValueFactories().getTypeSystem();
1304             QueryBuilder builder = new QueryBuilder(typeSystem);
1305             QueryCommand query = null;
1306             if (subgraphPath != null) {
1307                 query = builder.select("jcr:primaryType")
1308                                .fromAllNodesAs("allNodes")
1309                                .where()
1310                                .strongReferenceValue("allNodes")
1311                                .isIn(someUuidsInBranch)
1312                                .and()
1313                                .path("allNodes")
1314                                .isLike(subgraphPath + "%")
1315                                .end()
1316                                .query();
1317             } else {
1318                 query = builder.select("jcr:primaryType")
1319                                .fromAllNodesAs("allNodes")
1320                                .where()
1321                                .strongReferenceValue("allNodes")
1322                                .isIn(someUuidsInBranch)
1323                                .end()
1324                                .query();
1325             }
1326             Query jcrQuery = workspace().queryManager().createQuery(query);
1327             // The nodes that have been (transiently) deleted will not appear in these results ...
1328             QueryResult result = jcrQuery.execute();
1329             NodeIterator referencingNodes = result.getNodes();
1330             while (referencingNodes.hasNext()) {
1331                 // The REFERENCE property (or properties) may have been removed in this session,
1332                 // so check whether they referencing nodes have been loaded into the session ...
1333                 AbstractJcrNode referencingNode = (AbstractJcrNode)referencingNodes.nextNode();
1334                 if (!referencingNode.nodeInfo().isChanged(false)) {
1335                     // This node has not changed, so there is at least one reference; we can stop here ...
1336                     throw new ReferentialIntegrityException();
1337                 }
1338                 // This node has changed. This node is okay as long as the node no longer
1339                 // contains a REFERENCE property to any of the removed nodes...
1340                 PropertyIterator propIter = referencingNode.getProperties();
1341                 while (propIter.hasNext()) {
1342                     Property property = propIter.nextProperty();
1343                     if (property.getType() != PropertyType.REFERENCE) return;
1344                     if (property.isMultiple()) {
1345                         for (Value value : property.getValues()) {
1346                             String referencedUuid = value.getString();
1347                             if (removedReferenceableNodeUuids.contains(referencedUuid)) {
1348                                 // This node still has a reference to a node being removed ...
1349                                 throw new ReferentialIntegrityException();
1350                             }
1351                         }
1352                     } else {
1353                         String referencedUuid = property.getValue().getString();
1354                         if (removedReferenceableNodeUuids.contains(referencedUuid)) {
1355                             // This node still has a reference to a node being removed ...
1356                             throw new ReferentialIntegrityException();
1357                         }
1358                     }
1359                 }
1360             }
1361             someUuidsInBranch.clear();
1362         }
1363     }
1364 
1365     /**
1366      * {@inheritDoc}
1367      * 
1368      * @see javax.jcr.Session#save()
1369      */
1370     public void save() throws RepositoryException {
1371         checkReferentialIntegrityOfChanges(null);
1372         removedNodes = null;
1373         cache.save();
1374     }
1375 
1376     /**
1377      * Crawl and index the content in this workspace.
1378      * 
1379      * @throws IllegalArgumentException if the workspace is null
1380      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
1381      */
1382     public void reindexContent() {
1383         repository().queryManager().reindexContent(workspace());
1384     }
1385 
1386     /**
1387      * Crawl and index the content starting at the supplied path in this workspace, to the designated depth.
1388      * 
1389      * @param path the path of the content to be indexed
1390      * @param depth the depth of the content to be indexed
1391      * @throws IllegalArgumentException if the workspace or path are null, or if the depth is less than 1
1392      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
1393      */
1394     public void reindexContent( String path,
1395                                 int depth ) {
1396         repository().queryManager().reindexContent(workspace(), path, depth);
1397     }
1398 
1399     /**
1400      * Get a snapshot of the current session state. This snapshot is immutable and will not reflect any future state changes in
1401      * the session.
1402      * 
1403      * @return the snapshot; never null
1404      */
1405     public Snapshot getSnapshot() {
1406         return new Snapshot(cache.graphSession().getRoot().getSnapshot(false));
1407     }
1408 
1409     /**
1410      * {@inheritDoc}
1411      * 
1412      * @see java.lang.Object#toString()
1413      */
1414     @Override
1415     public String toString() {
1416         return getSnapshot().toString();
1417     }
1418 
1419     /**
1420      * {@inheritDoc}
1421      * 
1422      * @see javax.jcr.Session#getAccessControlManager()
1423      */
1424     @Override
1425     public AccessControlManager getAccessControlManager() throws UnsupportedRepositoryOperationException, RepositoryException {
1426         throw new UnsupportedRepositoryOperationException();
1427     }
1428 
1429     /**
1430      * {@inheritDoc}
1431      * 
1432      * @see javax.jcr.Session#getRetentionManager()
1433      */
1434     @Override
1435     public RetentionManager getRetentionManager() throws UnsupportedRepositoryOperationException, RepositoryException {
1436         throw new UnsupportedRepositoryOperationException();
1437     }
1438 
1439     @Immutable
1440     public class Snapshot {
1441         private final GraphSession.StructureSnapshot<JcrPropertyPayload> rootSnapshot;
1442 
1443         protected Snapshot( GraphSession.StructureSnapshot<JcrPropertyPayload> snapshot ) {
1444             this.rootSnapshot = snapshot;
1445         }
1446 
1447         /**
1448          * {@inheritDoc}
1449          * 
1450          * @see java.lang.Object#toString()
1451          */
1452         @Override
1453         public String toString() {
1454             return rootSnapshot.toString();
1455         }
1456     }
1457 }