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.security.AccessControlException;
30  import java.util.ArrayList;
31  import java.util.Calendar;
32  import java.util.Collection;
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.Value;
56  import javax.jcr.ValueFactory;
57  import javax.jcr.ValueFormatException;
58  import javax.jcr.Workspace;
59  import javax.jcr.lock.LockException;
60  import javax.jcr.nodetype.ConstraintViolationException;
61  import javax.jcr.query.Query;
62  import javax.jcr.query.QueryResult;
63  import javax.jcr.query.Row;
64  import javax.jcr.query.RowIterator;
65  import javax.jcr.version.VersionException;
66  import net.jcip.annotations.Immutable;
67  import net.jcip.annotations.NotThreadSafe;
68  import org.modeshape.common.util.CheckArg;
69  import org.modeshape.graph.ExecutionContext;
70  import org.modeshape.graph.Graph;
71  import org.modeshape.graph.Location;
72  import org.modeshape.graph.SecurityContext;
73  import org.modeshape.graph.property.Binary;
74  import org.modeshape.graph.property.DateTime;
75  import org.modeshape.graph.property.NamespaceRegistry;
76  import org.modeshape.graph.property.Path;
77  import org.modeshape.graph.property.PathFactory;
78  import org.modeshape.graph.property.ValueFactories;
79  import org.modeshape.graph.query.QueryBuilder;
80  import org.modeshape.graph.query.model.QueryCommand;
81  import org.modeshape.graph.query.model.TypeSystem;
82  import org.modeshape.graph.request.InvalidWorkspaceException;
83  import org.modeshape.graph.session.GraphSession;
84  import org.modeshape.jcr.JcrContentHandler.EnclosingSAXException;
85  import org.modeshape.jcr.JcrContentHandler.SaveMode;
86  import org.modeshape.jcr.JcrNamespaceRegistry.Behavior;
87  import org.modeshape.jcr.JcrRepository.Option;
88  import org.modeshape.jcr.SessionCache.JcrPropertyPayload;
89  import org.modeshape.jcr.WorkspaceLockManager.ModeShapeLock;
90  import org.xml.sax.ContentHandler;
91  import org.xml.sax.InputSource;
92  import org.xml.sax.SAXException;
93  import org.xml.sax.SAXParseException;
94  import org.xml.sax.XMLReader;
95  import org.xml.sax.helpers.XMLReaderFactory;
96  
97  /**
98   * The ModeShape implementation of a {@link Session JCR Session}.
99   */
100 @NotThreadSafe
101 class JcrSession implements Session {
102 
103     private static final String[] NO_ATTRIBUTES_NAMES = new String[] {};
104 
105     /**
106      * The repository that created this session.
107      */
108     private final JcrRepository repository;
109 
110     /**
111      * The workspace that corresponds to this session.
112      */
113     private final JcrWorkspace workspace;
114 
115     /**
116      * A JCR namespace registry that is specific to this session, with any locally-defined namespaces defined in this session.
117      * This is backed by the workspace's namespace registry.
118      */
119     private final JcrNamespaceRegistry sessionRegistry;
120 
121     /**
122      * The execution context for this session, which uses the {@link #sessionRegistry session's namespace registry}
123      */
124     protected final ExecutionContext executionContext;
125 
126     /**
127      * The session-specific attributes that came from the {@link SimpleCredentials}' {@link SimpleCredentials#getAttributeNames()}
128      */
129     private final Map<String, Object> sessionAttributes;
130 
131     /**
132      * The graph representing this session, which uses the {@link #graph session's graph}.
133      */
134     private final Graph graph;
135 
136     private final SessionCache cache;
137 
138     private final Set<String> lockTokens;
139 
140     /**
141      * A cached instance of the root path.
142      */
143     private final Path rootPath;
144 
145     private boolean isLive;
146 
147     private final boolean performReferentialIntegrityChecks;
148     /**
149      * The locations of the nodes that were (transiently) removed in this session and not yet saved.
150      */
151     private Set<Location> removedNodes = null;
152     /**
153      * The UUIDs of the mix:referenceable nodes that were (transiently) removed in this session and not yet saved.
154      */
155     private Set<String> removedReferenceableNodeUuids = null;
156 
157     JcrSession( JcrRepository repository,
158                 JcrWorkspace workspace,
159                 ExecutionContext sessionContext,
160                 NamespaceRegistry globalNamespaceRegistry,
161                 Map<String, Object> sessionAttributes ) {
162         assert repository != null;
163         assert workspace != null;
164         assert sessionAttributes != null;
165         assert sessionContext != null;
166         this.repository = repository;
167         this.sessionAttributes = sessionAttributes;
168         this.workspace = workspace;
169 
170         // Create an execution context for this session, which should use the local namespace registry ...
171         this.executionContext = sessionContext;
172         NamespaceRegistry local = sessionContext.getNamespaceRegistry();
173         this.sessionRegistry = new JcrNamespaceRegistry(Behavior.JSR170_SESSION, local, globalNamespaceRegistry, this);
174         this.rootPath = this.executionContext.getValueFactories().getPathFactory().createRootPath();
175 
176         // Set up the graph to use for this session (which uses the session's namespace registry and context) ...
177         this.graph = workspace.graph();
178 
179         this.cache = new SessionCache(this);
180         this.isLive = true;
181         this.lockTokens = new HashSet<String>();
182 
183         this.performReferentialIntegrityChecks = Boolean.valueOf(repository.getOptions()
184                                                                            .get(Option.PERFORM_REFERENTIAL_INTEGRITY_CHECKS))
185                                                         .booleanValue();
186 
187         assert this.sessionAttributes != null;
188         assert this.workspace != null;
189         assert this.repository != null;
190         assert this.executionContext != null;
191         assert this.sessionRegistry != null;
192         assert this.graph != null;
193         assert this.executionContext.getSecurityContext() != null;
194     }
195 
196     // Added to facilitate mock testing of items without necessarily requiring an entire repository structure to be built
197     final SessionCache cache() {
198         return this.cache;
199     }
200 
201     ExecutionContext getExecutionContext() {
202         return this.executionContext;
203     }
204 
205     String sessionId() {
206         return this.executionContext.getId();
207     }
208 
209     JcrNodeTypeManager nodeTypeManager() {
210         return this.workspace.nodeTypeManager();
211     }
212 
213     NamespaceRegistry namespaces() {
214         return this.executionContext.getNamespaceRegistry();
215     }
216 
217     void signalNamespaceChanges( boolean global ) {
218         nodeTypeManager().signalNamespaceChanges();
219         if (global) repository.getRepositoryTypeManager().signalNamespaceChanges();
220     }
221 
222     JcrWorkspace workspace() {
223         return this.workspace;
224     }
225 
226     JcrRepository repository() {
227         return this.repository;
228     }
229 
230     final Collection<String> lockTokens() {
231         return lockTokens;
232     }
233 
234     Graph.Batch createBatch() {
235         return graph.batch();
236     }
237 
238     Graph graph() {
239         return graph;
240     }
241 
242     String sourceName() {
243         return this.repository.getRepositorySourceName();
244     }
245 
246     /**
247      * {@inheritDoc}
248      * 
249      * @see javax.jcr.Session#getWorkspace()
250      */
251     public Workspace getWorkspace() {
252         return this.workspace;
253     }
254 
255     /**
256      * {@inheritDoc}
257      * 
258      * @see javax.jcr.Session#getRepository()
259      */
260     public Repository getRepository() {
261         return this.repository;
262     }
263 
264     /**
265      * {@inheritDoc}
266      * 
267      * @return <code>null</code>
268      * @see javax.jcr.Session#getAttribute(java.lang.String)
269      */
270     public Object getAttribute( String name ) {
271         return sessionAttributes.get(name);
272     }
273 
274     /**
275      * {@inheritDoc}
276      * 
277      * @return An empty array
278      * @see javax.jcr.Session#getAttributeNames()
279      */
280     public String[] getAttributeNames() {
281         Set<String> names = sessionAttributes.keySet();
282         if (names.isEmpty()) return NO_ATTRIBUTES_NAMES;
283         return names.toArray(new String[names.size()]);
284     }
285 
286     /**
287      * @return a copy of the session attributes for this session
288      */
289     Map<String, Object> sessionAttributes() {
290         return new HashMap<String, Object>(sessionAttributes);
291     }
292 
293     /**
294      * {@inheritDoc}
295      * 
296      * @see javax.jcr.Session#getNamespacePrefix(java.lang.String)
297      */
298     public String getNamespacePrefix( String uri ) throws RepositoryException {
299         return sessionRegistry.getPrefix(uri);
300     }
301 
302     /**
303      * {@inheritDoc}
304      * 
305      * @see javax.jcr.Session#getNamespacePrefixes()
306      */
307     public String[] getNamespacePrefixes() {
308         return sessionRegistry.getPrefixes();
309     }
310 
311     /**
312      * {@inheritDoc}
313      * 
314      * @see javax.jcr.Session#getNamespaceURI(java.lang.String)
315      */
316     public String getNamespaceURI( String prefix ) throws RepositoryException {
317         return sessionRegistry.getURI(prefix);
318     }
319 
320     /**
321      * {@inheritDoc}
322      * 
323      * @see javax.jcr.Session#setNamespacePrefix(java.lang.String, java.lang.String)
324      */
325     public void setNamespacePrefix( String newPrefix,
326                                     String existingUri ) throws NamespaceException, RepositoryException {
327         sessionRegistry.registerNamespace(newPrefix, existingUri);
328     }
329 
330     /**
331      * {@inheritDoc}
332      * 
333      * @see javax.jcr.Session#addLockToken(java.lang.String)
334      */
335     public void addLockToken( String lt ) throws LockException {
336         CheckArg.isNotNull(lt, "lock token");
337 
338         // Trivial case of giving a token back to ourself
339         if (lockTokens.contains(lt)) {
340             return;
341         }
342 
343         if (workspace().lockManager().isHeldBySession(this, lt)) {
344             throw new LockException(JcrI18n.lockTokenAlreadyHeld.text(lt));
345         }
346 
347         workspace().lockManager().setHeldBySession(this, lt, true);
348         lockTokens.add(lt);
349     }
350 
351     /**
352      * Returns whether the authenticated user has the given role.
353      * 
354      * @param roleName the name of the role to check
355      * @param workspaceName the workspace under which the user must have the role. This may be different from the current
356      *        workspace.
357      * @return true if the user has the role and is logged in; false otherwise
358      */
359     final boolean hasRole( String roleName,
360                            String workspaceName ) {
361         SecurityContext context = getExecutionContext().getSecurityContext();
362 
363         return context.hasRole(roleName) || context.hasRole(roleName + "." + this.repository.getRepositorySourceName())
364                || context.hasRole(roleName + "." + this.repository.getRepositorySourceName() + "." + workspaceName);
365     }
366 
367     /**
368      * {@inheritDoc}
369      * 
370      * @throws IllegalArgumentException if either <code>path</code> or <code>actions</code> is empty or <code>null</code>.
371      * @see javax.jcr.Session#checkPermission(java.lang.String, java.lang.String)
372      */
373     public void checkPermission( String path,
374                                  String actions ) {
375         CheckArg.isNotEmpty(path, "path");
376 
377         this.checkPermission(executionContext.getValueFactories().getPathFactory().create(path), actions);
378     }
379 
380     /**
381      * Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
382      * current workspace, otherwise returns silently.
383      * <p>
384      * The {@code path} parameter is included for future use and is currently ignored
385      * </p>
386      * 
387      * @param path the path on which the actions are occurring
388      * @param actions a comma-delimited list of actions to check
389      */
390     void checkPermission( Path path,
391                           String actions ) {
392         checkPermission(this.workspace().getName(), path, actions);
393     }
394 
395     /**
396      * Throws an {@link AccessControlException} if the current user does not have permission for all of the named actions in the
397      * named workspace, otherwise returns silently.
398      * <p>
399      * The {@code path} parameter is included for future use and is currently ignored
400      * </p>
401      * 
402      * @param workspaceName the name of the workspace in which the path exists
403      * @param path the path on which the actions are occurring
404      * @param actions a comma-delimited list of actions to check
405      */
406     void checkPermission( String workspaceName,
407                           Path path,
408                           String actions ) {
409 
410         CheckArg.isNotEmpty(actions, "actions");
411 
412         boolean hasPermission = true;
413         for (String action : actions.split(",")) {
414             if (ModeShapePermissions.READ.equals(action)) {
415                 hasPermission &= hasRole(ModeShapeRoles.READONLY, workspaceName)
416                                  || hasRole(ModeShapeRoles.READWRITE, workspaceName)
417                                  || hasRole(ModeShapeRoles.ADMIN, workspaceName);
418             } else if (ModeShapePermissions.REGISTER_NAMESPACE.equals(action)
419                        || ModeShapePermissions.REGISTER_TYPE.equals(action) || ModeShapePermissions.UNLOCK_ANY.equals(action)) {
420                 hasPermission &= hasRole(ModeShapeRoles.ADMIN, workspaceName);
421             } else {
422                 hasPermission &= hasRole(ModeShapeRoles.ADMIN, workspaceName) || hasRole(ModeShapeRoles.READWRITE, workspaceName);
423             }
424         }
425 
426         if (hasPermission) return;
427 
428         String pathAsString = path != null ? path.getString(this.namespaces()) : "<unknown>";
429         throw new AccessControlException(JcrI18n.permissionDenied.text(pathAsString, actions));
430 
431     }
432 
433     /**
434      * {@inheritDoc}
435      * 
436      * @see javax.jcr.Session#exportDocumentView(java.lang.String, org.xml.sax.ContentHandler, boolean, boolean)
437      */
438     public void exportDocumentView( String absPath,
439                                     ContentHandler contentHandler,
440                                     boolean skipBinary,
441                                     boolean noRecurse ) throws RepositoryException, SAXException {
442         CheckArg.isNotNull(absPath, "absPath");
443         CheckArg.isNotNull(contentHandler, "contentHandler");
444 
445         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
446         Node exportRootNode = getNode(exportRootPath);
447 
448         AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
449 
450         exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
451     }
452 
453     /**
454      * {@inheritDoc}
455      * 
456      * @see javax.jcr.Session#exportDocumentView(java.lang.String, java.io.OutputStream, boolean, boolean)
457      */
458     public void exportDocumentView( String absPath,
459                                     OutputStream out,
460                                     boolean skipBinary,
461                                     boolean noRecurse ) throws RepositoryException {
462         CheckArg.isNotNull(absPath, "absPath");
463         CheckArg.isNotNull(out, "out");
464 
465         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
466         Node exportRootNode = getNode(exportRootPath);
467 
468         AbstractJcrExporter exporter = new JcrDocumentViewExporter(this);
469 
470         exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
471     }
472 
473     /**
474      * {@inheritDoc}
475      * 
476      * @see javax.jcr.Session#exportSystemView(java.lang.String, org.xml.sax.ContentHandler, boolean, boolean)
477      */
478     public void exportSystemView( String absPath,
479                                   ContentHandler contentHandler,
480                                   boolean skipBinary,
481                                   boolean noRecurse ) throws RepositoryException, SAXException {
482         CheckArg.isNotNull(absPath, "absPath");
483         CheckArg.isNotNull(contentHandler, "contentHandler");
484 
485         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
486         Node exportRootNode = getNode(exportRootPath);
487 
488         AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
489 
490         exporter.exportView(exportRootNode, contentHandler, skipBinary, noRecurse);
491     }
492 
493     /**
494      * {@inheritDoc}
495      * 
496      * @see javax.jcr.Session#exportSystemView(java.lang.String, java.io.OutputStream, boolean, boolean)
497      */
498     public void exportSystemView( String absPath,
499                                   OutputStream out,
500                                   boolean skipBinary,
501                                   boolean noRecurse ) throws RepositoryException {
502         CheckArg.isNotNull(absPath, "absPath");
503         CheckArg.isNotNull(out, "out");
504 
505         Path exportRootPath = executionContext.getValueFactories().getPathFactory().create(absPath);
506         Node exportRootNode = getNode(exportRootPath);
507 
508         AbstractJcrExporter exporter = new JcrSystemViewExporter(this);
509 
510         exporter.exportView(exportRootNode, out, skipBinary, noRecurse);
511     }
512 
513     /**
514      * {@inheritDoc}
515      * 
516      * @see javax.jcr.Session#getImportContentHandler(java.lang.String, int)
517      */
518     public ContentHandler getImportContentHandler( String parentAbsPath,
519                                                    int uuidBehavior ) throws PathNotFoundException, RepositoryException {
520         Path parentPath = this.executionContext.getValueFactories().getPathFactory().create(parentAbsPath);
521 
522         return new JcrContentHandler(this, parentPath, uuidBehavior, SaveMode.SESSION);
523     }
524 
525     /**
526      * {@inheritDoc}
527      * 
528      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
529      * @see javax.jcr.Session#getItem(java.lang.String)
530      */
531     public Item getItem( String absolutePath ) throws RepositoryException {
532         CheckArg.isNotEmpty(absolutePath, "absolutePath");
533         // Return root node if path is "/"
534         Path path = executionContext.getValueFactories().getPathFactory().create(absolutePath);
535         if (path.isRoot()) {
536             return getRootNode();
537         }
538         // 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 ...
539         if (path.getLastSegment().hasIndex()) {
540             return getNode(path);
541         }
542         // We can't tell from the name, so ask for an item ...
543         try {
544             return cache.findJcrItem(null, rootPath, path.relativeTo(rootPath));
545         } catch (ItemNotFoundException e) {
546             throw new PathNotFoundException(e.getMessage(), e);
547         }
548     }
549 
550     /**
551      * {@inheritDoc}
552      * 
553      * @see javax.jcr.Session#getLockTokens()
554      */
555     public String[] getLockTokens() {
556         return lockTokens.toArray(new String[lockTokens.size()]);
557     }
558 
559     /**
560      * Find or create a JCR Node for the given path. This method works for the root node, too.
561      * 
562      * @param path the path; may not be null
563      * @return the JCR node instance for the given path; never null
564      * @throws PathNotFoundException if the path could not be found
565      * @throws RepositoryException if there is a problem
566      */
567     AbstractJcrNode getNode( Path path ) throws RepositoryException, PathNotFoundException {
568         if (path.isRoot()) return cache.findJcrRootNode();
569         try {
570             return cache.findJcrNode(null, path);
571         } catch (ItemNotFoundException e) {
572             throw new PathNotFoundException(e.getMessage());
573         }
574     }
575 
576     /**
577      * {@inheritDoc}
578      * 
579      * @see javax.jcr.Session#getNodeByUUID(java.lang.String)
580      */
581     public AbstractJcrNode getNodeByUUID( String uuid ) throws ItemNotFoundException, RepositoryException {
582         AbstractJcrNode node = cache.findJcrNode(Location.create(UUID.fromString(uuid)));
583 
584         return node;
585     }
586 
587     /**
588      * {@inheritDoc}
589      * 
590      * @see javax.jcr.Session#getRootNode()
591      */
592     public Node getRootNode() throws RepositoryException {
593         return cache.findJcrRootNode();
594     }
595 
596     /**
597      * {@inheritDoc}
598      * 
599      * @see javax.jcr.Session#getUserID()
600      * @see SecurityContext#getUserName()
601      */
602     public String getUserID() {
603         return executionContext.getSecurityContext().getUserName();
604     }
605 
606     /**
607      * {@inheritDoc}
608      * 
609      * @see javax.jcr.Session#getValueFactory()
610      */
611     public ValueFactory getValueFactory() {
612         final ValueFactories valueFactories = executionContext.getValueFactories();
613         final SessionCache sessionCache = this.cache;
614 
615         return new ValueFactory() {
616 
617             public Value createValue( String value,
618                                       int propertyType ) throws ValueFormatException {
619                 return new JcrValue(valueFactories, sessionCache, propertyType, convertValueToType(value, propertyType));
620             }
621 
622             public Value createValue( Node value ) throws RepositoryException {
623                 if (!value.isNodeType(JcrMixLexicon.REFERENCEABLE.getString(JcrSession.this.namespaces()))) {
624                     throw new RepositoryException(JcrI18n.nodeNotReferenceable.text());
625                 }
626                 String uuid = valueFactories.getStringFactory().create(value.getUUID());
627                 return new JcrValue(valueFactories, sessionCache, PropertyType.REFERENCE, uuid);
628             }
629 
630             public Value createValue( InputStream value ) {
631                 Binary binary = valueFactories.getBinaryFactory().create(value);
632                 return new JcrValue(valueFactories, sessionCache, PropertyType.BINARY, binary);
633             }
634 
635             public Value createValue( Calendar value ) {
636                 DateTime dateTime = valueFactories.getDateFactory().create(value);
637                 return new JcrValue(valueFactories, sessionCache, PropertyType.DATE, dateTime);
638             }
639 
640             public Value createValue( boolean value ) {
641                 return new JcrValue(valueFactories, sessionCache, PropertyType.BOOLEAN, value);
642             }
643 
644             public Value createValue( double value ) {
645                 return new JcrValue(valueFactories, sessionCache, PropertyType.DOUBLE, value);
646             }
647 
648             public Value createValue( long value ) {
649                 return new JcrValue(valueFactories, sessionCache, PropertyType.LONG, value);
650             }
651 
652             public Value createValue( String value ) {
653                 return new JcrValue(valueFactories, sessionCache, PropertyType.STRING, value);
654             }
655 
656             Object convertValueToType( Object value,
657                                        int toType ) throws ValueFormatException {
658                 switch (toType) {
659                     case PropertyType.BOOLEAN:
660                         try {
661                             return valueFactories.getBooleanFactory().create(value);
662                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
663                             throw new ValueFormatException(vfe);
664                         }
665 
666                     case PropertyType.DATE:
667                         try {
668                             return valueFactories.getDateFactory().create(value);
669                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
670                             throw new ValueFormatException(vfe);
671                         }
672 
673                     case PropertyType.NAME:
674                         try {
675                             return valueFactories.getNameFactory().create(value);
676                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
677                             throw new ValueFormatException(vfe);
678                         }
679 
680                     case PropertyType.PATH:
681                         try {
682                             return valueFactories.getPathFactory().create(value);
683                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
684                             throw new ValueFormatException(vfe);
685                         }
686 
687                     case PropertyType.REFERENCE:
688                         try {
689                             return valueFactories.getReferenceFactory().create(value);
690                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
691                             throw new ValueFormatException(vfe);
692                         }
693                     case PropertyType.DOUBLE:
694                         try {
695                             return valueFactories.getDoubleFactory().create(value);
696                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
697                             throw new ValueFormatException(vfe);
698                         }
699                     case PropertyType.LONG:
700                         try {
701                             return valueFactories.getLongFactory().create(value);
702                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
703                             throw new ValueFormatException(vfe);
704                         }
705 
706                         // Anything can be converted to these types
707                     case PropertyType.BINARY:
708                         try {
709                             return valueFactories.getBinaryFactory().create(value);
710                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
711                             throw new ValueFormatException(vfe);
712                         }
713                     case PropertyType.STRING:
714                         try {
715                             return valueFactories.getStringFactory().create(value);
716                         } catch (org.modeshape.graph.property.ValueFormatException vfe) {
717                             throw new ValueFormatException(vfe);
718                         }
719                     case PropertyType.UNDEFINED:
720                         return value;
721 
722                     default:
723                         assert false : "Unexpected JCR property type " + toType;
724                         // This should still throw an exception even if assertions are turned off
725                         throw new IllegalStateException("Invalid property type " + toType);
726                 }
727             }
728 
729         };
730     }
731 
732     /**
733      * {@inheritDoc}
734      * 
735      * @see javax.jcr.Session#hasPendingChanges()
736      */
737     public boolean hasPendingChanges() {
738         return cache.hasPendingChanges();
739     }
740 
741     /**
742      * {@inheritDoc}
743      * 
744      * @see javax.jcr.Session#impersonate(javax.jcr.Credentials)
745      */
746     public Session impersonate( Credentials credentials ) throws RepositoryException {
747         return repository.login(credentials, this.workspace.getName());
748     }
749 
750     /**
751      * Returns a new {@link JcrSession session} that uses the same security information to create a session that points to the
752      * named workspace.
753      * 
754      * @param workspaceName the name of the workspace to connect to
755      * @return a new session that uses the named workspace
756      * @throws RepositoryException if an error occurs creating the session
757      */
758     JcrSession with( String workspaceName ) throws RepositoryException {
759         return repository.sessionForContext(executionContext, workspaceName, sessionAttributes);
760     }
761 
762     /**
763      * {@inheritDoc}
764      * 
765      * @see javax.jcr.Session#importXML(java.lang.String, java.io.InputStream, int)
766      */
767     public void importXML( String parentAbsPath,
768                            InputStream in,
769                            int uuidBehavior ) throws IOException, InvalidSerializedDataException, RepositoryException {
770 
771         try {
772             XMLReader parser = XMLReaderFactory.createXMLReader();
773 
774             parser.setContentHandler(getImportContentHandler(parentAbsPath, uuidBehavior));
775             parser.parse(new InputSource(in));
776         } catch (EnclosingSAXException ese) {
777             Exception cause = ese.getException();
778             if (cause instanceof ItemExistsException) {
779                 throw (ItemExistsException)cause;
780             } else if (cause instanceof ConstraintViolationException) {
781                 throw (ConstraintViolationException)cause;
782             } else if (cause instanceof VersionException) {
783                 throw (VersionException)cause;
784             }
785             throw new RepositoryException(cause);
786         } catch (SAXParseException se) {
787             throw new InvalidSerializedDataException(se);
788         } catch (SAXException se) {
789             throw new RepositoryException(se);
790         }
791     }
792 
793     /**
794      * {@inheritDoc}
795      * 
796      * @see javax.jcr.Session#isLive()
797      */
798     public boolean isLive() {
799         return isLive;
800     }
801 
802     /**
803      * {@inheritDoc}
804      * 
805      * @throws IllegalArgumentException if <code>absolutePath</code> is empty or <code>null</code>.
806      * @see javax.jcr.Session#itemExists(java.lang.String)
807      */
808     public boolean itemExists( String absolutePath ) throws RepositoryException {
809         try {
810             return (getItem(absolutePath) != null);
811         } catch (PathNotFoundException error) {
812             return false;
813         }
814     }
815 
816     /**
817      * {@inheritDoc}
818      * 
819      * @see javax.jcr.Session#logout()
820      */
821     public void logout() {
822         if (!isLive()) {
823             return;
824         }
825 
826         isLive = false;
827         this.workspace().observationManager().removeAllEventListeners();
828         this.workspace().lockManager().cleanLocks(this);
829         this.repository.sessionLoggedOut(this);
830         this.executionContext.getSecurityContext().logout();
831     }
832 
833     /**
834      * {@inheritDoc}
835      * 
836      * @see javax.jcr.Session#move(java.lang.String, java.lang.String)
837      */
838     public void move( String srcAbsPath,
839                       String destAbsPath ) throws ItemExistsException, RepositoryException {
840         CheckArg.isNotNull(srcAbsPath, "srcAbsPath");
841         CheckArg.isNotNull(destAbsPath, "destAbsPath");
842 
843         PathFactory pathFactory = executionContext.getValueFactories().getPathFactory();
844         Path destPath = pathFactory.create(destAbsPath);
845 
846         Path.Segment newNodeName = destPath.getSegment(destPath.size() - 1);
847         // Doing a literal test here because the path factory will canonicalize "/node[1]" to "/node"
848         if (destAbsPath.endsWith("]")) {
849             throw new RepositoryException(JcrI18n.pathCannotHaveSameNameSiblingIndex.text(destAbsPath));
850         }
851 
852         AbstractJcrNode sourceNode = getNode(pathFactory.create(srcAbsPath));
853         AbstractJcrNode newParentNode = getNode(destPath.getParent());
854 
855         if (sourceNode.isLocked()) {
856             javax.jcr.lock.Lock sourceLock = sourceNode.getLock();
857             if (sourceLock != null && sourceLock.getLockToken() == null) {
858                 throw new LockException(JcrI18n.lockTokenNotHeld.text(srcAbsPath));
859             }
860         }
861 
862         if (newParentNode.isLocked()) {
863             javax.jcr.lock.Lock newParentLock = newParentNode.getLock();
864             if (newParentLock != null && newParentLock.getLockToken() == null) {
865                 throw new LockException(JcrI18n.lockTokenNotHeld.text(destAbsPath));
866             }
867         }
868 
869         if (!sourceNode.isCheckedOut()) {
870             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(sourceNode.getPath()));
871         }
872 
873         if (!newParentNode.isCheckedOut()) {
874             throw new VersionException(JcrI18n.nodeIsCheckedIn.text(newParentNode.getPath()));
875         }
876 
877         newParentNode.editor().moveToBeChild(sourceNode, newNodeName.getName());
878     }
879 
880     /**
881      * {@inheritDoc}
882      * 
883      * @see javax.jcr.Session#refresh(boolean)
884      */
885     public void refresh( boolean keepChanges ) {
886         this.cache.refresh(keepChanges);
887     }
888 
889     /**
890      * {@inheritDoc}
891      * 
892      * @see javax.jcr.Session#removeLockToken(java.lang.String)
893      */
894     public void removeLockToken( String lt ) {
895         CheckArg.isNotNull(lt, "lock token");
896         // A LockException is thrown if the lock associated with the specified lock token is session-scoped.
897         /*
898          * The JCR API library that we're using diverges from the spec in that it doesn't declare
899          * this method to throw a LockException.  We'll throw a runtime exception for now.
900          */
901 
902         ModeShapeLock lock = workspace().lockManager().lockFor(lt);
903         if (lock == null) {
904             // The lock is no longer valid
905             lockTokens.remove(lt);
906             return;
907         }
908 
909         if (lock.isSessionScoped()) {
910             throw new IllegalStateException(JcrI18n.cannotRemoveLockToken.text(lt));
911         }
912 
913         workspace().lockManager().setHeldBySession(this, lt, false);
914         lockTokens.remove(lt);
915     }
916 
917     void recordRemoval( Location location ) throws RepositoryException {
918         if (!performReferentialIntegrityChecks) {
919             return;
920         }
921         if (removedNodes == null) {
922             removedNodes = new HashSet<Location>();
923             removedReferenceableNodeUuids = new HashSet<String>();
924         }
925 
926         // Find the UUIDs of all of the mix:referenceable nodes that are below this node being removed ...
927         Path path = location.getPath();
928         org.modeshape.graph.property.ValueFactory<String> stringFactory = executionContext.getValueFactories().getStringFactory();
929         String pathStr = stringFactory.create(path);
930         int sns = path.getLastSegment().getIndex();
931         if (sns == Path.DEFAULT_INDEX) pathStr = pathStr + "[1]";
932 
933         TypeSystem typeSystem = executionContext.getValueFactories().getTypeSystem();
934         QueryBuilder builder = new QueryBuilder(typeSystem);
935         QueryCommand query = builder.select("jcr:uuid")
936                                     .from("mix:referenceable AS referenceable")
937                                     .where()
938                                     .path("referenceable")
939                                     .isLike(pathStr + "%")
940                                     .end()
941                                     .query();
942         JcrQueryManager queryManager = workspace().queryManager();
943         Query jcrQuery = queryManager.createQuery(query);
944         QueryResult result = jcrQuery.execute();
945         RowIterator rows = result.getRows();
946         while (rows.hasNext()) {
947             Row row = rows.nextRow();
948             String uuid = row.getValue("jcr:uuid").getString();
949             if (uuid != null) removedReferenceableNodeUuids.add(uuid);
950         }
951 
952         // Now record that this location is being removed ...
953         Set<Location> extras = null;
954         for (Location alreadyDeleted : removedNodes) {
955             Path alreadyDeletedPath = alreadyDeleted.getPath();
956             if (alreadyDeletedPath.isAtOrAbove(path)) {
957                 // Already covered by the alreadyDeleted location ...
958                 return;
959             }
960             if (alreadyDeletedPath.isDecendantOf(path)) {
961                 // The path being deleted is above the path that was already deleted, so remove the already-deleted one ...
962                 if (extras == null) {
963                     extras = new HashSet<Location>();
964                 }
965                 extras.add(alreadyDeleted);
966             }
967         }
968         // Not covered by any already-deleted location, so add it ...
969         removedNodes.add(location);
970         if (extras != null) {
971             // Remove the nodes that will be covered by the node being deleted now ...
972             removedNodes.removeAll(extras);
973         }
974     }
975 
976     boolean wasRemovedInSession( Location location ) {
977         if (removedNodes == null) return false;
978         if (removedNodes.contains(location)) return true;
979         Path path = location.getPath();
980         for (Location removed : removedNodes) {
981             if (removed.getPath().isAtOrAbove(path)) return true;
982         }
983         return false;
984     }
985 
986     boolean wasRemovedInSession( UUID uuid ) {
987         if (removedReferenceableNodeUuids == null) return false;
988         return removedReferenceableNodeUuids.contains(uuid);
989 
990     }
991 
992     /**
993      * Determine whether there is at least one other node outside this branch that has a reference to nodes within the branch
994      * rooted by this node.
995      * 
996      * @param subgraphRoot the root of the subgraph under which the references should be checked, or null if the root node should
997      *        be used (meaning all references in the workspace should be checked)
998      * @throws ReferentialIntegrityException if the changes would leave referential integrity problems
999      * @throws RepositoryException if an error occurs while obtaining the information
1000      */
1001     void checkReferentialIntegrityOfChanges( AbstractJcrNode subgraphRoot )
1002         throws ReferentialIntegrityException, RepositoryException {
1003         if (removedNodes == null) return;
1004         if (removedReferenceableNodeUuids.isEmpty()) return;
1005 
1006         if (removedNodes.size() == 1 && removedNodes.iterator().next().getPath().isRoot()) {
1007             // The root node is being removed, so there will be no referencing nodes remaining ...
1008             return;
1009         }
1010 
1011         String subgraphPath = null;
1012         if (subgraphRoot != null) {
1013             subgraphPath = subgraphRoot.getPath();
1014             if (subgraphRoot.getIndex() == Path.DEFAULT_INDEX) subgraphPath = subgraphPath + "[1]";
1015         }
1016 
1017         // Build one (or several) queries to find the first reference to any 'mix:referenceable' nodes
1018         // that have been (transiently) removed from the session ...
1019         int maxBatchSize = 100;
1020         List<Object> someUuidsInBranch = new ArrayList<Object>(maxBatchSize);
1021         Iterator<String> uuidIter = removedReferenceableNodeUuids.iterator();
1022         while (uuidIter.hasNext()) {
1023             // Accumulate the next 100 UUIDs of referenceable nodes inside this branch ...
1024             while (uuidIter.hasNext() && someUuidsInBranch.size() <= maxBatchSize) {
1025                 String uuid = uuidIter.next();
1026                 someUuidsInBranch.add(uuid);
1027             }
1028             assert !someUuidsInBranch.isEmpty();
1029             // Now issue a query to see if any nodes outside this branch references these referenceable nodes ...
1030             TypeSystem typeSystem = executionContext.getValueFactories().getTypeSystem();
1031             QueryBuilder builder = new QueryBuilder(typeSystem);
1032             QueryCommand query = null;
1033             if (subgraphPath != null) {
1034                 query = builder.select("jcr:primaryType")
1035                                .fromAllNodesAs("allNodes")
1036                                .where()
1037                                .referenceValue("allNodes")
1038                                .isIn(someUuidsInBranch)
1039                                .and()
1040                                .path("allNodes")
1041                                .isLike(subgraphPath + "%")
1042                                .end()
1043                                .query();
1044             } else {
1045                 query = builder.select("jcr:primaryType")
1046                                .fromAllNodesAs("allNodes")
1047                                .where()
1048                                .referenceValue("allNodes")
1049                                .isIn(someUuidsInBranch)
1050                                .end()
1051                                .query();
1052             }
1053             Query jcrQuery = workspace().queryManager().createQuery(query);
1054             // The nodes that have been (transiently) deleted will not appear in these results ...
1055             QueryResult result = jcrQuery.execute();
1056             NodeIterator referencingNodes = result.getNodes();
1057             while (referencingNodes.hasNext()) {
1058                 // There is at least one reference to nodes in this branch, so we can stop here ...
1059                 throw new ReferentialIntegrityException();
1060             }
1061             someUuidsInBranch.clear();
1062         }
1063     }
1064 
1065     /**
1066      * {@inheritDoc}
1067      * 
1068      * @see javax.jcr.Session#save()
1069      */
1070     public void save() throws RepositoryException {
1071         checkReferentialIntegrityOfChanges(null);
1072         removedNodes = null;
1073         cache.save();
1074     }
1075 
1076     /**
1077      * Crawl and index the content in this workspace.
1078      * 
1079      * @throws IllegalArgumentException if the workspace is null
1080      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
1081      */
1082     public void reindexContent() {
1083         repository().queryManager().reindexContent(workspace());
1084     }
1085 
1086     /**
1087      * Crawl and index the content starting at the supplied path in this workspace, to the designated depth.
1088      * 
1089      * @param path the path of the content to be indexed
1090      * @param depth the depth of the content to be indexed
1091      * @throws IllegalArgumentException if the workspace or path are null, or if the depth is less than 1
1092      * @throws InvalidWorkspaceException if there is no workspace with the supplied name
1093      */
1094     public void reindexContent( String path,
1095                                 int depth ) {
1096         repository().queryManager().reindexContent(workspace(), path, depth);
1097     }
1098 
1099     /**
1100      * Get a snapshot of the current session state. This snapshot is immutable and will not reflect any future state changes in
1101      * the session.
1102      * 
1103      * @return the snapshot; never null
1104      */
1105     public Snapshot getSnapshot() {
1106         return new Snapshot(cache.graphSession().getRoot().getSnapshot(false));
1107     }
1108 
1109     /**
1110      * {@inheritDoc}
1111      * 
1112      * @see java.lang.Object#toString()
1113      */
1114     @Override
1115     public String toString() {
1116         return getSnapshot().toString();
1117     }
1118 
1119     @Immutable
1120     public class Snapshot {
1121         private final GraphSession.StructureSnapshot<JcrPropertyPayload> rootSnapshot;
1122 
1123         protected Snapshot( GraphSession.StructureSnapshot<JcrPropertyPayload> snapshot ) {
1124             this.rootSnapshot = snapshot;
1125         }
1126 
1127         /**
1128          * {@inheritDoc}
1129          * 
1130          * @see java.lang.Object#toString()
1131          */
1132         @Override
1133         public String toString() {
1134             return rootSnapshot.toString();
1135         }
1136     }
1137 }