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