Index: modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (revision 1790) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/AbstractJcrNode.java (working copy) @@ -1213,6 +1213,61 @@ abstract class AbstractJcrNode extends AbstractJcrItem implements javax.jcr.Node return editor.createChild(childName, desiredUuid, childPrimaryTypeName); } + /** + * Performs a "best effort" check on whether a node can be added at the given relative path from this node with the given + * primary node type (if one is specified). + *

+ * Note that a result of {@code true} from this method does not guarantee that a call to {@code #addNode(String, String)} with + * the same arguments will succeed, but a result of {@code false} guarantees that it would fail (assuming that the current + * repository state does not change). + *

+ * + * @param relPath the relative path at which the node would be added; may not be null + * @param primaryNodeTypeName the primary type that would be used for the node; null indicates that a default primary type + * should be used if possible + * @return false if the node could not be added for any reason; true if the node might be able to be added + * @throws RepositoryException if an error occurs accessing the repository + */ + final boolean canAddNode( String relPath, + String primaryNodeTypeName ) throws RepositoryException { + CheckArg.isNotEmpty(relPath, relPath); + + if (isLocked() && !holdsLock()) { + return false; + } + + // Determine the path ... + Path path = null; + try { + path = cache.pathFactory().create(relPath); + } catch (org.modeshape.graph.property.ValueFormatException e) { + return false; + } + if (path.size() == 0) { + return false; + } + if (path.getLastSegment().getIndex() > 1 || relPath.endsWith("]")) { + return false; + } + if (path.size() > 1) { + // The only segment in the path is the child name ... + Path parentPath = path.getParent(); + try { + // Find the parent node ... + cache.findNode(nodeId, location.getPath(), parentPath); + } catch (RepositoryException e) { + return false; + } + } + + // Determine the name for the primary node type + if (primaryNodeTypeName != null) { + if (!session().nodeTypeManager().hasNodeType(primaryNodeTypeName)) return false; + } + + return true; + } + protected final Property removeExistingValuedProperty( String name ) throws ConstraintViolationException, RepositoryException { AbstractJcrProperty property = cache.findJcrProperty(nodeId, location.getPath(), nameFrom(name)); if (property != null) { Index: modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java =================================================================== --- modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (revision 1790) +++ modeshape-jcr/src/main/java/org/modeshape/jcr/JcrSession.java (working copy) @@ -457,6 +457,45 @@ class JcrSession implements Session { } /** + * Makes a "best effort" determination of whether the given method can be successfully called on the given target with the + * given arguments. A return value of {@code false} indicates that the method would not succeed. A return value of {@code + * true} indicates that the method might succeed. + * + * @param methodName the method to invoke; may not be null + * @param target the object on which to invoke it; may not be null + * @param arguments the arguments to pass to the method; varies depending on the method + * @return + * @throws IllegalArgumentException + * @throws RepositoryException + */ + public boolean hasCapability( String methodName, + Object target, + Object[] arguments ) throws IllegalArgumentException, RepositoryException { + CheckArg.isNotEmpty(methodName, "methodName"); + CheckArg.isNotNull(target, "target"); + + if (target instanceof AbstractJcrNode) { + AbstractJcrNode node = (AbstractJcrNode)target; + if ("addNode".equals(methodName)) { + CheckArg.hasSizeOfAtLeast(arguments, 1, "arguments"); + CheckArg.hasSizeOfAtMost(arguments, 2, "arguments"); + CheckArg.isInstanceOf(arguments[0], String.class, "arguments[0]"); + + String relPath = (String) arguments[0]; + String primaryNodeTypeName = null; + if (arguments.length > 1) { + CheckArg.isInstanceOf(arguments[1], String.class, "arguments[1]"); + primaryNodeTypeName = (String) arguments[1]; + } + + return node.canAddNode(relPath, primaryNodeTypeName); + } + } + + return true; + } + + /** * {@inheritDoc} * * @see javax.jcr.Session#exportDocumentView(java.lang.String, org.xml.sax.ContentHandler, boolean, boolean) Index: modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java =================================================================== --- modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java (revision 1790) +++ modeshape-jcr/src/test/java/org/modeshape/jcr/JcrSessionTest.java (working copy) @@ -467,4 +467,17 @@ public class JcrSessionTest extends AbstractSessionTest { Node fileNode = folderNode.addNode("fileNode", "nt:file"); assertThat(fileNode.hasProperty("jcr:created"), is(true)); } + + @Test + public void shouldHaveCapabilityToPerformValidAddNode() throws Exception { + assertTrue(session.hasCapability("addNode", session.getRootNode(), new String[] {"someNewNode"})); + assertTrue(session.hasCapability("addNode", session.getRootNode(), new String[] {"someNewNode", "nt:unstructured"})); + } + + @Test + public void shouldNotHaveCapabilityToPerformInvalidAddNode() throws Exception { + assertTrue(!session.hasCapability("addNode", session.getRootNode(), new String[] {"someNewNode[2]"})); + assertTrue(!session.hasCapability("addNode", session.getRootNode(), new String[] {"someNewNode", "nt:invalidType"})); + } + }