Index: dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/connector/federation/ForkRequestProcessor.java (working copy) @@ -1129,6 +1129,7 @@ if (projectedFromNode == null) return; ProjectedNode projectedIntoNode = project(request.into(), request.inWorkspace(), request, true); if (projectedIntoNode == null) return; + ProjectedNode projectedBeforeNode = request.before() != null ? project(request.before(), request.inWorkspace(), request, true) : null; // Limitation: only able to project the move if the 'from' and 'into' are in the same source & projection ... while (projectedFromNode != null) { @@ -1160,11 +1161,12 @@ ProxyNode fromProxy = projectedFromNode.asProxy(); ProxyNode intoProxy = projectedIntoNode.asProxy(); + ProxyNode beforeProxy = request.before() != null ? projectedBeforeNode.asProxy() : null; assert fromProxy.projection().getSourceName().equals(intoProxy.projection().getSourceName()); boolean sameLocation = fromProxy.isSameLocationAsOriginal() && intoProxy.isSameLocationAsOriginal(); // Create the pushed-down request ... - MoveBranchRequest pushDown = new MoveBranchRequest(fromProxy.location(), intoProxy.location(), intoProxy.workspaceName(), + MoveBranchRequest pushDown = new MoveBranchRequest(fromProxy.location(), intoProxy.location(), beforeProxy.location(), intoProxy.workspaceName(), request.desiredName(), request.conflictBehavior()); // Create the federated request ... FederatedRequest federatedRequest = new FederatedRequest(request); Index: dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepository.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepository.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepository.java (working copy) @@ -351,16 +351,19 @@ * @param desiredNewName the new name for the node, if it is to be changed; may be null * @param newWorkspace the workspace containing the new parent node * @param newParent the new parent; may not be the {@link Workspace#getRoot() root} + * @param beforeNode the node before which this new node should be placed */ public void moveNode( ExecutionContext context, InMemoryNode node, Name desiredNewName, Workspace newWorkspace, - InMemoryNode newParent ) { + InMemoryNode newParent, + InMemoryNode beforeNode) { assert context != null; assert newParent != null; assert node != null; - assert newWorkspace.getRoot().equals(newParent) != true; +// Why was this restriction here? -- BRC +// assert newWorkspace.getRoot().equals(newParent) != true; assert this.getRoot().equals(node) != true; InMemoryNode oldParent = node.getParent(); Name oldName = node.getName().getName(); @@ -377,9 +380,16 @@ newName = desiredNewName; node.setName(context.getValueFactories().getPathFactory().createSegment(desiredNewName, 1)); } - newParent.getChildren().add(node); + + if (beforeNode == null) { + newParent.getChildren().add(node); + } + else { + int index = newParent.getChildren().indexOf(beforeNode); + newParent.getChildren().add(index, node); + } correctSameNameSiblingIndexes(context, newParent, newName); - + // If the node was moved to a new workspace... if (!this.equals(newWorkspace)) { // We need to remove the node from this workspace's map of nodes ... Index: dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/connector/inmemory/InMemoryRequestProcessor.java (working copy) @@ -217,12 +217,31 @@ InMemoryRepository.Workspace workspace = getWorkspace(request, request.inWorkspace()); if (workspace == null) return; + InMemoryNode beforeNode = request.before() != null ? workspace.getNode(request.before().getPath()) : null; InMemoryNode node = getTargetNode(workspace, request, request.from()); if (node == null) return; // Look up the new parent, which must exist ... - Path newParentPath = request.into().getPath(); + Path newParentPath; + + if (request.into() != null) { + newParentPath = request.into().getPath(); + } + else { + // into or before cannot both be null + assert beforeNode != null; + + // Build the path from the before node to the root. + LinkedList segments = new LinkedList(); + InMemoryNode current = beforeNode.getParent(); + while (current != workspace.getRoot()) { + segments.addFirst(current.getName()); + current = current.getParent(); + } + newParentPath = getExecutionContext().getValueFactories().getPathFactory().createAbsolutePath(segments); + } + InMemoryNode newParent = workspace.getNode(newParentPath); - workspace.moveNode(getExecutionContext(), node, request.desiredName(), workspace, newParent); + workspace.moveNode(getExecutionContext(), node, request.desiredName(), workspace, newParent, beforeNode); assert node.getParent() == newParent; Path newPath = getExecutionContext().getValueFactories().getPathFactory().create(newParentPath, node.getName()); Location oldLocation = getActualLocation(request.from().getPath(), node); Index: dna-graph/src/main/java/org/jboss/dna/graph/Graph.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (working copy) @@ -437,10 +437,11 @@ @Override protected Conjunction submit( Locations from, Location into, + Location before, Name newName ) { String workspaceName = getCurrentWorkspaceName(); do { - requests.moveBranch(from.getLocation(), into, workspaceName, newName); + requests.moveBranch(from.getLocation(), into, before, workspaceName, newName); } while ((from = from.next()) != null); return and(); } @@ -2204,10 +2205,11 @@ @Override protected BatchConjunction submit( Locations from, Location into, + Location before, Name newName ) { String workspaceName = getCurrentWorkspaceName(); do { - requestQueue.moveBranch(from.getLocation(), into, workspaceName, newName); + requestQueue.moveBranch(from.getLocation(), into, before, workspaceName, newName); } while ((from = from.next()) != null); return and(); } @@ -3712,6 +3714,86 @@ } /** + * A component that defines the location before which a node should be copied or moved. This is similar to an + * {@link Into}, but it allows for placing a node at a particular location within the new destination, rather than + * always placing the moved or copied node as the last child of the new parent. + * + * @param The interface that is to be returned when this request is completed + * @author Randall Hauch + */ + public interface Before { + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). If you want to control the name of the node for the newly copied/moved node, use + * {@link To#to(Location)} instead. + * + * @param parentLocation the location of the new parent + * @return the interface for additional requests or actions + * @see To#to(Location) + */ + Next before( Location parentLocation ); + + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). If you want to control the name of the node for the newly copied/moved node, use + * {@link To#to(String)} instead. + * + * @param parentPath the path of the new parent + * @return the interface for additional requests or actions + * @see To#to(String) + */ + Next before( String parentPath ); + + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). If you want to control the name of the node for the newly copied/moved node, use + * {@link To#to(Path)} instead. + * + * @param parentPath the path of the new parent + * @return the interface for additional requests or actions + * @see To#to(Path) + */ + Next before( Path parentPath ); + + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). + * + * @param parentUuid the UUID of the new parent + * @return the interface for additional requests or actions + */ + Next before( UUID parentUuid ); + + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). + * + * @param parentIdProperty the property that uniquely identifies the new parent + * @return the interface for additional requests or actions + */ + Next before( Property parentIdProperty ); + + /** + * Finish the request by specifying the location of the node before which the node should be copied/moved. This operation + * will result in the copied/moved node having the same name as the original (but with the appropriately-determined + * same-name-sibling index). + * + * @param firstParentIdProperty the first property that, with the additionalIdProperties, uniquely identifies + * the new parent + * @param additionalParentIdProperties the additional properties that, with the additionalIdProperties, + * uniquely identifies the new parent + * @return the interface for additional requests or actions + */ + Next before( Property firstParentIdProperty, + Property... additionalParentIdProperties ); + } + + /** * A component that defines the location to which a node should be copied or moved. * * @param The interface that is to be returned when this request is completed @@ -3850,7 +3932,7 @@ * @param The interface that is to be returned when this request is completed * @author Randall Hauch */ - public interface Move extends AsName>, Into, And> { + public interface Move extends AsName>, Into, Before, And> { } /** @@ -5508,15 +5590,31 @@ * * @param from the location(s) that are being moved; never null * @param into the parent location + * @param before the location of the child of the parent before which this node should be placed * @param newName the new name for the node being moved; may be null * @return this object, for method chaining */ protected abstract T submit( Locations from, Location into, + Location before, Name newName ); + /** + * Submit any requests to move the targets into the supplied parent location + * + * @param from the location(s) that are being moved; never null + * @param into the parent location + * @param newName the new name for the node being moved; may be null + * @return this object, for method chaining + */ + protected T submit( Locations from, + Location into, + Name newName ) { + return submit(from, into, null, newName); + } + public T into( Location into ) { - return submit(from, into, newName); + return submit(from, into, null, newName); } public T into( Path into ) { @@ -5539,6 +5637,31 @@ public T into( String into ) { return submit(from, Location.create(createPath(into)), newName); } + + public T before( Location before ) { + return submit(from, null, before, newName); + } + + public T before( Path before ) { + return submit(from, null, Location.create(before), newName); + } + + public T before( UUID before ) { + return submit(from, null, Location.create(before), newName); + } + + public T before( Property firstIdProperty, + Property... additionalIdProperties ) { + return submit(from, null, Location.create(firstIdProperty, additionalIdProperties), newName); + } + + public T before( Property before ) { + return submit(from, null, Location.create(before), newName); + } + + public T before( String before ) { + return submit(from, null, Location.create(createPath(before)), newName); + } } @NotThreadSafe Index: dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/BatchRequestBuilder.java (working copy) @@ -627,7 +627,7 @@ Location into, String workspaceName, Name newNameForNode ) { - return add(new MoveBranchRequest(from, into, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); + return add(new MoveBranchRequest(from, into, null, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); } /** @@ -635,7 +635,26 @@ * * @param from the location of the top node in the existing branch that is to be moved * @param into the location of the existing node into which the branch should be moved + * @param before the location of the node before which the branch should be moved; may be null * @param workspaceName the name of the workspace + * @param newNameForNode the new name for the node being moved, or null if the name of the original should be used + * @return this builder for method chaining; never null + * @throws IllegalArgumentException if any of the parameters are null + */ + public BatchRequestBuilder moveBranch( Location from, + Location into, + Location before, + String workspaceName, + Name newNameForNode ) { + return add(new MoveBranchRequest(from, into, before, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); + } + + /** + * Create a request to move a branch from one location into another. + * + * @param from the location of the top node in the existing branch that is to be moved + * @param into the location of the existing node into which the branch should be moved + * @param workspaceName the name of the workspace * @param conflictBehavior the expected behavior if an equivalently-named child already exists at the into * location * @return this builder for method chaining; never null Index: dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/MoveBranchRequest.java (working copy) @@ -44,6 +44,7 @@ private final Location from; private final Location into; + private final Location before; private final String workspaceName; private final Name desiredNameForNode; private final NodeConflictBehavior conflictBehavior; @@ -61,7 +62,7 @@ public MoveBranchRequest( Location from, Location into, String workspaceName ) { - this(from, into, workspaceName, null, DEFAULT_CONFLICT_BEHAVIOR); + this(from, into, null, workspaceName, null, DEFAULT_CONFLICT_BEHAVIOR); } /** @@ -77,7 +78,7 @@ Location into, String workspaceName, Name newNameForMovedNode ) { - this(from, into, workspaceName, newNameForMovedNode, DEFAULT_CONFLICT_BEHAVIOR); + this(from, into, null, workspaceName, newNameForMovedNode, DEFAULT_CONFLICT_BEHAVIOR); } /** @@ -94,7 +95,7 @@ Location into, String workspaceName, NodeConflictBehavior conflictBehavior ) { - this(from, into, workspaceName, null, conflictBehavior); + this(from, into, null, workspaceName, null, conflictBehavior); } /** @@ -102,6 +103,8 @@ * * @param from the location of the top node in the existing branch that is to be moved * @param into the location of the existing node into which the branch should be moved + * @param before the location of the child of the {@code into} node that the branch should be placed before; null indicates + * that the branch should be the last child of its new parent * @param workspaceName the name of the workspace * @param newNameForMovedNode the new name for the node being moved, or null if the name of the original should be used * @param conflictBehavior the expected behavior if an equivalently-named child already exists at the into @@ -110,15 +113,17 @@ */ public MoveBranchRequest( Location from, Location into, + Location before, String workspaceName, Name newNameForMovedNode, NodeConflictBehavior conflictBehavior ) { CheckArg.isNotNull(from, "from"); - CheckArg.isNotNull(into, "into"); +// CheckArg.isNotNull(into, "into"); CheckArg.isNotNull(workspaceName, "workspaceName"); CheckArg.isNotNull(conflictBehavior, "conflictBehavior"); this.from = from; this.into = into; + this.before = before; this.workspaceName = workspaceName; this.desiredNameForNode = newNameForMovedNode; this.conflictBehavior = conflictBehavior; @@ -143,6 +148,15 @@ } /** + * Get the location defining the node before which the branch is to be placed + * + * @return the to location; null indicates that the branch should be the last child node of its new parent + */ + public Location before() { + return before; + } + + /** * Get the name of the workspace in which the branch exists. * * @return the name of the workspace containing the branch; never null @@ -197,9 +211,10 @@ * @return true if this move request really doesn't change the parent of the node, or false if it cannot be determined */ public boolean hasNoEffect() { - if (into.hasPath() && into.hasIdProperties() == false && from.hasPath()) { + if (into != null && into.hasPath() && into.hasIdProperties() == false && from.hasPath()) { if (!from.getPath().getParent().equals(into.getPath())) return false; if (desiredName() != null && !desiredName().equals(from.getPath().getLastSegment().getName())) return false; + if (before != null) return false; return true; } // Can't be determined for certain @@ -232,7 +247,7 @@ if (!newLocation.hasPath()) { throw new IllegalArgumentException(GraphI18n.actualNewLocationMustHavePath.text(newLocation)); } - if (into().hasPath() && !newLocation.getPath().getParent().isSameAs(into.getPath())) { + if (into() != null && into().hasPath() && !newLocation.getPath().getParent().isSameAs(into.getPath())) { throw new IllegalArgumentException(GraphI18n.actualLocationIsNotSameAsInputLocation.text(newLocation, into)); } Name actualNewName = newLocation.getPath().getLastSegment().getName(); Index: dna-graph/src/main/java/org/jboss/dna/graph/request/RequestBuilder.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/request/RequestBuilder.java (revision 954) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/RequestBuilder.java (working copy) @@ -474,9 +474,28 @@ Location into, String workspaceName, Name newNameForNode ) { - return process(new MoveBranchRequest(from, into, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); + return process(new MoveBranchRequest(from, into, null, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); } + /** + * Create a request to move a branch from one location into another before the given child node of the new location. + * + * @param from the location of the top node in the existing branch that is to be moved + * @param into the location of the existing node into which the branch should be moved + * @param before the location of the node before which the branch should be moved; may be null + * @param workspaceName the name of the workspace + * @param newNameForNode the new name for the node being moved, or null if the name of the original should be used + * @return the request; never null + * @throws IllegalArgumentException if any of the parameters are null + */ + public MoveBranchRequest moveBranch( Location from, + Location into, + Location before, + String workspaceName, + Name newNameForNode ) { + return process(new MoveBranchRequest(from, into, before, workspaceName, newNameForNode, MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR)); + } + /** * Create a request to move a branch from one location into another. * * @param from the location of the top node in the existing branch that is to be moved @@ -494,7 +513,7 @@ Name newNameForNode, NodeConflictBehavior conflictBehavior ) { if (conflictBehavior == null) conflictBehavior = MoveBranchRequest.DEFAULT_CONFLICT_BEHAVIOR; - return process(new MoveBranchRequest(from, into, workspaceName, newNameForNode, conflictBehavior)); + return process(new MoveBranchRequest(from, into, null, workspaceName, newNameForNode, conflictBehavior)); } /** Index: dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepositoryWorkspaceTest.java =================================================================== --- dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepositoryWorkspaceTest.java (revision 954) +++ dna-graph/src/test/java/org/jboss/dna/graph/connector/inmemory/InMemoryRepositoryWorkspaceTest.java (working copy) @@ -241,7 +241,7 @@ assertThat(workspace.getNode(pathFactory.create("/d/e")), is(sameInstance(node_e))); assertThat(workspace.getNode(pathFactory.create("/d/b")), is(sameInstance(node_b2))); - workspace.moveNode(context, node_b, null, workspace, node_d); + workspace.moveNode(context, node_b, null, workspace, node_d, null); assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); assertThat(workspace.getNode(pathFactory.create("/a")), is(sameInstance(node_a))); @@ -251,7 +251,7 @@ assertThat(workspace.getNode(pathFactory.create("/d/b[2]")), is(sameInstance(node_b))); assertThat(workspace.getNode(pathFactory.create("/d/b[2]/c")), is(sameInstance(node_c))); - workspace.moveNode(context, node_b, null, workspace, node_e); + workspace.moveNode(context, node_b, null, workspace, node_e, null); assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); assertThat(workspace.getNode(pathFactory.create("/a")), is(sameInstance(node_a))); @@ -263,6 +263,55 @@ } @Test + public void shouldMoveNodeBeforeAnother() { + InMemoryNode root = workspace.getRoot(); + InMemoryNode node_a = workspace.createNode(context, root, nameFactory.create("a"), null); + InMemoryNode node_b = workspace.createNode(context, node_a, nameFactory.create("b"), null); + InMemoryNode node_c = workspace.createNode(context, node_b, nameFactory.create("c"), null); + InMemoryNode node_d = workspace.createNode(context, root, nameFactory.create("d"), null); + InMemoryNode node_e = workspace.createNode(context, node_d, nameFactory.create("e"), null); + InMemoryNode node_b2 = workspace.createNode(context, node_d, nameFactory.create("b"), null); + Name propName = nameFactory.create("prop"); + node_b.setProperty(propertyFactory.create(propName, "node_b")); + node_b2.setProperty(propertyFactory.create(propName, "node_b2")); + + assertThat(workspace.getNodesByUuid().size(), is(7)); + assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); + assertThat(workspace.getNode(pathFactory.create("/a")), is(sameInstance(node_a))); + assertThat(workspace.getNode(pathFactory.create("/a/b")), is(sameInstance(node_b))); + assertThat(workspace.getNode(pathFactory.create("/a/b/c")), is(sameInstance(node_c))); + assertThat(workspace.getNode(pathFactory.create("/d")), is(sameInstance(node_d))); + assertThat(workspace.getNode(pathFactory.create("/d/e")), is(sameInstance(node_e))); + assertThat(workspace.getNode(pathFactory.create("/d/b")), is(sameInstance(node_b2))); + assertThat(workspace.getNode(pathFactory.create("/a/b")).getProperty(propName).getFirstValue().toString(), is("node_b")); + assertThat(workspace.getNode(pathFactory.create("/d/b")).getProperty(propName).getFirstValue().toString(), is("node_b2")); + + // Move before a node with the same name + workspace.moveNode(context, node_b, null, workspace, node_d, node_b2); + + assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); + assertThat(workspace.getNode(pathFactory.create("/a")), is(sameInstance(node_a))); + assertThat(workspace.getNode(pathFactory.create("/d")), is(sameInstance(node_d))); + assertThat(workspace.getNode(pathFactory.create("/d/e")), is(sameInstance(node_e))); + assertThat(workspace.getNode(pathFactory.create("/d/b[2]")), is(sameInstance(node_b2))); + assertThat(workspace.getNode(pathFactory.create("/d/b[1]")), is(sameInstance(node_b))); + assertThat(workspace.getNode(pathFactory.create("/d/b[1]/c")), is(sameInstance(node_c))); + assertThat(workspace.getNode(pathFactory.create("/d/b[1]")).getProperty(propName).getFirstValue().toString(), is("node_b")); + assertThat(workspace.getNode(pathFactory.create("/d/b[2]")).getProperty(propName).getFirstValue().toString(), is("node_b2")); + + // Move after the last node + workspace.moveNode(context, node_b, null, workspace, root, null); + + assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); + assertThat(workspace.getNode(pathFactory.create("/a")), is(sameInstance(node_a))); + assertThat(workspace.getNode(pathFactory.create("/d")), is(sameInstance(node_d))); + assertThat(workspace.getNode(pathFactory.create("/d/e")), is(sameInstance(node_e))); + assertThat(workspace.getNode(pathFactory.create("/b")), is(sameInstance(node_b))); + assertThat(workspace.getNode(pathFactory.create("/b/c")), is(sameInstance(node_c))); + assertThat(workspace.getNode(pathFactory.create("/d/b")), is(sameInstance(node_b2))); + } + + @Test public void shouldMoveNodesFromOneWorkspaceToAnother() { // Populate the workspace with some content ... InMemoryNode root = workspace.getRoot(); @@ -304,7 +353,7 @@ assertThat(new_workspace.getNode(pathFactory.create("/d/b")), is(sameInstance(new_node_b2))); // Move 'workspace::/a/b' into 'newWorkspace::/d' - workspace.moveNode(context, node_b, null, new_workspace, new_node_d); + workspace.moveNode(context, node_b, null, new_workspace, new_node_d, null); assertThat(workspace.getNodesByUuid().size(), is(5)); assertThat(workspace.getNode(pathFactory.create("/")), is(sameInstance(workspace.getRoot()))); Index: dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WritableConnectorTest.java =================================================================== --- dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WritableConnectorTest.java (revision 954) +++ dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WritableConnectorTest.java (working copy) @@ -1386,4 +1386,184 @@ "The quick brown fox jumped over the moon. What? ")); } + @Test + public void shouldMoveAndRenameNodesToNameWithSameNameSibling() { + // Create the tree (at total of 40 nodes, plus the extra 6 added later)... + // / + // /node1 + // /node1/node1 + // /node1/node1/node1 + // /node1/node1/node2 + // /node1/node1/node3 + // /node1/node2 + // /node1/node2/node1 + // /node1/node2/node2 + // /node1/node2/node3 + // /node1/node3 + // /node1/node3/node1 + // /node1/node3/node2 + // /node1/node3/node3 + // /node2 + // /node2/node1 + // /node2/node1/node1 + // /node2/node1/node2 + // /node2/node1/node3 + // /node2/node2 + // /node2/node2/node1 + // /node2/node2/node2 + // /node2/node2/node3 + // /node2/node3 + // /node2/node3/node1 + // /node2/node3/node2 + // /node2/node3/node3 + // /node3 + // /node3/node1 + // /node3/node1/node1 + // /node3/node1/node2 + // /node3/node1/node3 + // /node3/node2 + // /node3/node2/node1 + // /node3/node2/node2 + // /node3/node2/node3 + // /node3/node3 + // /node3/node3/node1 + // /node3/node3/node2 + // /node3/node3/node3 + // /secondBranch1 + // /secondBranch1/secondBranch1 + // /secondBranch1/secondBranch2 + // /secondBranch2 + // /secondBranch2/secondBranch1 + // /secondBranch2/secondBranch2 + + String initialPath = ""; + int depth = 3; + int numChildrenPerNode = 3; + int numPropertiesPerNode = 3; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + // Delete two branches ... + graph.move("/node2").before("/node3/node2"); + + // Now assert the structure ... + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"), segment("node3"))); + assertThat(graph.getChildren().of("/node1"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node1/node1"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node1/node2"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node1/node3"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node1/node3/node1"), hasChildren()); + + assertThat(graph.getChildren().of("/node3"), hasChildren(segment("node1"), + segment("node2[1]"), + segment("node3"), + segment("node2[2]"))); + assertThat(graph.getChildren().of("/node3/node2[1]"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node3"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node3/node1"), hasChildren()); + assertThat(graph.getChildren().of("/node3/node2[1]"), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node2[1]/node1"), + hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node2[1]/node2"), + hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node2[1]/node3"), + hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(graph.getChildren().of("/node3/node2[1]/node1/node1"), hasChildren()); + + Subgraph subgraph = graph.getSubgraphOfDepth(4).at("/node3"); + assertThat(subgraph, is(notNullValue())); + assertThat(subgraph.getNode(".").getChildren(), hasChildren(segment("node2"), segment("node3"))); + assertThat(subgraph.getNode("."), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("."), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("."), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[2]").getChildren(), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(subgraph.getNode("node2[2]"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[2]"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[2]"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node3").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node3"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node3"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node3"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]").getChildren(), hasChildren(segment("node1"), segment("node2"), segment("node3"))); + assertThat(subgraph.getNode("node2[1]"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1").getChildren(), hasChildren(segment("node1"), + segment("node2"), + segment("node3"))); + assertThat(subgraph.getNode("node2[1]/node1"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node1").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node1/node1"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node1"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node1"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node2").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node1/node2"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node2"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node2"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node3").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node1/node3"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node3"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node1/node3"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node1").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node2/node1"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node1"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node1"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node2").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node2/node2"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node2"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node2"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node3").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node2/node3"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node3"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node2/node3"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3"), hasProperty("property2", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3"), hasProperty("property3", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node1").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node3/node1"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node1"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node1"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node2").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node3/node2"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node2"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node2"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node3").getChildren(), isEmpty()); + assertThat(subgraph.getNode("node2[1]/node3/node3"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node3"), hasProperty("property2", + "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node2[1]/node3/node3"), hasProperty("property3", + "The quick brown fox jumped over the moon. What? ")); + } + } Index: dna-graph/src/test/java/org/jboss/dna/graph/request/MoveBranchRequestTest.java =================================================================== --- dna-graph/src/test/java/org/jboss/dna/graph/request/MoveBranchRequestTest.java (revision 954) +++ dna-graph/src/test/java/org/jboss/dna/graph/request/MoveBranchRequestTest.java (working copy) @@ -27,6 +27,9 @@ import static org.hamcrest.core.IsNull.nullValue; import static org.hamcrest.core.IsSame.sameInstance; import static org.junit.Assert.assertThat; +import org.jboss.dna.graph.NodeConflictBehavior; +import org.jboss.dna.graph.property.Name; +import org.jboss.dna.graph.property.basic.BasicName; import org.junit.Before; import org.junit.Test; @@ -53,8 +56,8 @@ new MoveBranchRequest(null, validPathLocation2, workspace2); } - @Test( expected = IllegalArgumentException.class ) - public void shouldNotAllowCreatingRequestWithNullToLocation() { + @Test + public void shouldAllowCreatingRequestWithNullToLocation() { new MoveBranchRequest(validPathLocation1, null, workspace2); } @@ -74,6 +77,18 @@ } @Test + public void shouldCreateValidRequestWithValidFromLocationAndValidToLocationAndValidBeforeLocation() { + Name newName = new BasicName("", "newName"); + request = new MoveBranchRequest(validPathLocation1, validPathLocation2, validPathLocation, workspace1, newName, NodeConflictBehavior.DO_NOT_REPLACE); + assertThat(request.from(), is(sameInstance(validPathLocation1))); + assertThat(request.into(), is(sameInstance(validPathLocation2))); + assertThat(request.before(), is(sameInstance(validPathLocation))); + assertThat(request.inWorkspace(), is(sameInstance(workspace1))); + assertThat(request.hasError(), is(false)); + assertThat(request.getError(), is(nullValue())); + } + + @Test public void shouldConsiderEqualTwoRequestsWithSameLocations() { request = new MoveBranchRequest(validPathLocation1, validPathLocation2, workspace2); MoveBranchRequest request2 = new MoveBranchRequest(validPathLocation1, validPathLocation2, workspace2); Index: extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatingRequestProcessor.java =================================================================== --- extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatingRequestProcessor.java (revision 954) +++ extensions/dna-connector-federation/src/main/java/org/jboss/dna/connector/federation/FederatingRequestProcessor.java (working copy) @@ -408,12 +408,15 @@ intoProjection.projection.getRules()); request.setError(new UnsupportedRequestException(msg)); } + SingleProjection beforeProjection = request.before() != null ? asSingleProjection(workspace, request.before(), request) : null; + // Push down the request ... Location fromLocation = Location.create(fromProjection.pathInSource); Location intoLocation = Location.create(intoProjection.pathInSource); + Location beforeLocation = beforeProjection != null ? Location.create(beforeProjection.pathInSource) : null; String workspaceName = fromProjection.projection.getWorkspaceName(); - MoveBranchRequest sourceRequest = new MoveBranchRequest(fromLocation, intoLocation, workspaceName, request.desiredName(), + MoveBranchRequest sourceRequest = new MoveBranchRequest(fromLocation, intoLocation, beforeLocation, workspaceName, request.desiredName(), request.conflictBehavior()); execute(sourceRequest, fromProjection.projection); Index: extensions/dna-connector-jbosscache/src/main/java/org/jboss/dna/connector/jbosscache/JBossCacheRequestProcessor.java =================================================================== --- extensions/dna-connector-jbosscache/src/main/java/org/jboss/dna/connector/jbosscache/JBossCacheRequestProcessor.java (revision 954) +++ extensions/dna-connector-jbosscache/src/main/java/org/jboss/dna/connector/jbosscache/JBossCacheRequestProcessor.java (working copy) @@ -163,7 +163,7 @@ // Update the children to account for same-name siblings. // This not only updates the FQN of the child nodes, but it also sets the property that stores the // the array of Path.Segment for the children (since the cache doesn't maintain order). - Path.Segment newSegment = updateChildList(cache, parentNode, request.named(), getExecutionContext(), true); + Path.Segment newSegment = updateChildList(cache, parentNode, request.named(), null, getExecutionContext(), true); Node node = parentNode.addChild(Fqn.fromElements(newSegment)); assert checkChildren(parentNode); @@ -244,6 +244,7 @@ node, newParent, desiredName, + null, true, useSameUuids, newNodeUuid, @@ -286,13 +287,32 @@ Path nodePath = request.from().getPath(); Node node = getNode(request, cache, nodePath); if (node == null) return; - Path newParentPath = request.into().getPath(); + Path newParentPath; + + if (request.into() != null) { + newParentPath = request.into().getPath(); + } else { + // into() and before() can't both be null + assert request.before() != null; + newParentPath = request.before().getPath().getParent(); + } + + Path.Segment beforeNodeName = request.before() != null ? request.before().getPath().getLastSegment() : null; Node newParent = getNode(request, cache, newParentPath); if (newParent == null) return; // Copy the branch and use the same UUIDs ... Name desiredName = request.desiredName(); - Path.Segment newSegment = copyNode(cache, node, newParent, desiredName, true, true, null, null, getExecutionContext()); + Path.Segment newSegment = copyNode(cache, + node, + newParent, + desiredName, + beforeNodeName, + true, + true, + null, + null, + getExecutionContext()); // Now delete the old node ... Node oldParent = node.getParent(); @@ -415,7 +435,7 @@ // Loop over each child and copy it ... for (Node child : fromRoot.getChildren()) { - copyNode(intoCache, child, intoRoot, null, true, true, null, null, context); + copyNode(intoCache, child, intoRoot, null, null, true, true, null, null, context); } // Copy the list of child segments in the root (this maintains the order of the children) ... @@ -518,10 +538,24 @@ } + /** + * @param newCache the cache into which the node is to be copied + * @param original the node to be copied + * @param newParent the new parent of the node to be copied + * @param desiredName the desired name of the node in the new location + * @param beforeNodeName the node before which the new node should be placed + * @param recursive if this is a deep copy + * @param reuseOriginalUuids indicates whether the original UUIDs should be used for the copies or new UUIDs should be used + * @param uuidForCopyOfOriginal pre-determined UUID for copy of node; ignored if reuseOriginalUuids is true + * @param count the count of nodes affected by the operation + * @param context the execution context that provides the path factory to be used to create the new path name + * @return the path segment that identifies the new node under its new parent + */ protected Path.Segment copyNode( Cache newCache, Node original, Node newParent, Name desiredName, + Path.Segment beforeNodeName, boolean recursive, boolean reuseOriginalUuids, UUID uuidForCopyOfOriginal, @@ -530,13 +564,12 @@ assert original != null; assert newParent != null; // Get or create the new node ... - Path.Segment name = desiredName != null ? context.getValueFactories().getPathFactory().createSegment(desiredName) : (Path.Segment)original.getFqn() - .getLastElement(); + Path.Segment name = desiredName != null ? context.getValueFactories().getPathFactory().createSegment(desiredName) : (Path.Segment)original.getFqn().getLastElement(); // Update the children to account for same-name siblings. // This not only updates the FQN of the child nodes, but it also sets the property that stores the // the array of Path.Segment for the children (since the cache doesn't maintain order). - Path.Segment newSegment = updateChildList(newCache, newParent, name.getName(), context, true); + Path.Segment newSegment = updateChildList(newCache, newParent, name.getName(), beforeNodeName, context, true); Node copy = newParent.addChild(getFullyQualifiedName(newSegment)); assert checkChildren(newParent); // Copy the properties ... @@ -553,7 +586,7 @@ if (recursive) { // Loop over each child and call this method ... for (Node child : original.getChildren()) { - copyNode(newCache, child, copy, null, true, reuseOriginalUuids, null, count, context); + copyNode(newCache, child, copy, null, null, true, reuseOriginalUuids, null, count, context); } } return newSegment; @@ -569,6 +602,8 @@ * @param parent the parent node; may not be null * @param changedName the name that should be compared to the existing node siblings to determine whether the same-name * sibling indexes should be updated; may not be null + * @param beforeNodeName the name of the node before which this node should be placed; null indicates that this node should be + * added as the last child under the node * @param context the execution context; may not be null * @param addChildWithName true if a new child with the supplied name is to be added to the children (but which does not yet * exist in the node's children) @@ -577,6 +612,7 @@ protected Path.Segment updateChildList( Cache cache, Node parent, Name changedName, + Path.Segment beforeNodeName, ExecutionContext context, boolean addChildWithName ) { assert parent != null; @@ -589,14 +625,25 @@ List childrenWithChangedName = new LinkedList(); Path.Segment[] childNames = (Path.Segment[])parent.get(JBossCacheLexicon.CHILD_PATH_SEGMENT_LIST); int index = 0; + int snsIndex = 0; + boolean foundBeforeNode = false; if (childNames != null) { for (Path.Segment childName : childNames) { + if (childName.equals(beforeNodeName)) { + foundBeforeNode = true; + // And add a child info for the new node ... + ChildInfo info = new ChildInfo(null, snsIndex++); + childrenWithChangedName.add(info); + } if (childName.getName().equals(changedName)) { - ChildInfo info = new ChildInfo(childName, index); + ChildInfo info = new ChildInfo(childName, snsIndex); childrenWithChangedName.add(info); } - index++; + + snsIndex++; + if (!foundBeforeNode) index++; } + } if (addChildWithName) { // Make room for the new child at the end of the array ... @@ -605,15 +652,22 @@ } else { int numExisting = childNames.length; Path.Segment[] newChildNames = new Path.Segment[numExisting + 1]; - System.arraycopy(childNames, 0, newChildNames, 0, numExisting); + System.arraycopy(childNames, 0, newChildNames, 0, index); + + if (index != numExisting) { + System.arraycopy(childNames, index, newChildNames, index + 1, numExisting - index); + } childNames = newChildNames; } - // And add a child info for the new node ... - ChildInfo info = new ChildInfo(null, index); - childrenWithChangedName.add(info); + if (!foundBeforeNode) { + // Make sure that we add a record for the new node if it hasn't previously been added + ChildInfo info = new ChildInfo(null, index); + childrenWithChangedName.add(info); + + } Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(changedName); - childNames[index++] = newSegment; + childNames[index] = newSegment; } assert childNames != null; @@ -657,7 +711,7 @@ if (addChildWithName) { // Return the segment for the new node ... - return childNames[childNames.length - 1]; + return childNames[index]; } return null; } @@ -706,9 +760,8 @@ // don't copy ... } else { // Append an updated segment ... - Path.Segment newSegment = context.getValueFactories() - .getPathFactory() - .createSegment(childName.getName(), childName.getIndex() - 1); + Path.Segment newSegment = context.getValueFactories().getPathFactory().createSegment(childName.getName(), + childName.getIndex() - 1); newChildNames[index] = newSegment; // Replace the child with the correct FQN ... changeNodeName(cache, parent, childName, newSegment, context); @@ -753,6 +806,10 @@ this.segment = childSegment; this.childIndex = childIndex; } + + public String toString() { + return (segment != null ? segment.getString() : "null") + "@" + childIndex; + } } Index: extensions/dna-connector-store-jpa/src/main/java/org/jboss/dna/connector/store/jpa/model/basic/BasicRequestProcessor.java =================================================================== --- extensions/dna-connector-store-jpa/src/main/java/org/jboss/dna/connector/store/jpa/model/basic/BasicRequestProcessor.java (revision 954) +++ extensions/dna-connector-store-jpa/src/main/java/org/jboss/dna/connector/store/jpa/model/basic/BasicRequestProcessor.java (working copy) @@ -1360,6 +1360,7 @@ * * @see org.jboss.dna.graph.request.processor.RequestProcessor#process(org.jboss.dna.graph.request.MoveBranchRequest) */ + @SuppressWarnings( "unchecked" ) @Override public void process( MoveBranchRequest request ) { logger.trace(request.toString()); @@ -1390,6 +1391,20 @@ // Find the actual new location ... Location toLocation = request.into(); + Location beforeLocation = request.before(); + + if (beforeLocation != null) { + if (beforeLocation.hasPath()) { + toLocation = Location.create(beforeLocation.getPath().getParent()); + } else { + ActualLocation actualBeforeLocation = getActualLocation(workspaceId, beforeLocation); + + // Ensure that the beforeLocation has a path - actualBeforeLocation has a path + beforeLocation = actualBeforeLocation.location; + toLocation = Location.create(actualBeforeLocation.location.getPath().getParent()); + } + } + String toUuidString = null; if (request.hasNoEffect()) { actualNewLocation = actualOldLocation; @@ -1416,28 +1431,69 @@ ns = fromEntity.getChildNamespace(); } - // Find the largest SNS index in the existing ChildEntity objects with the same name ... - Query query = entities.createNamedQuery("ChildEntity.findMaximumSnsIndex"); - query.setParameter("workspaceId", workspaceId); - query.setParameter("parentUuid", toUuidString); - query.setParameter("ns", ns.getId()); - query.setParameter("childName", childLocalName); int nextSnsIndex = 1; - try { - Integer index = (Integer)query.getSingleResult(); - if (index != null) nextSnsIndex = index.intValue() + 1; - } catch (NoResultException e) { - } + int nextIndexInParent = 1; + if (beforeLocation == null) { + // Find the largest SNS index in the existing ChildEntity objects with the same name ... + Query query = entities.createNamedQuery("ChildEntity.findMaximumSnsIndex"); + query.setParameter("workspaceId", workspaceId); + query.setParameter("parentUuid", toUuidString); + query.setParameter("ns", ns.getId()); + query.setParameter("childName", childLocalName); + try { + Integer index = (Integer)query.getSingleResult(); + if (index != null) nextSnsIndex = index.intValue() + 1; + } catch (NoResultException e) { + } - // Find the largest child index in the existing ChildEntity objects ... - query = entities.createNamedQuery("ChildEntity.findMaximumChildIndex"); - query.setParameter("workspaceId", workspaceId); - query.setParameter("parentUuid", toUuidString); - int nextIndexInParent = 1; - try { - Integer index = (Integer)query.getSingleResult(); - if (index != null) nextIndexInParent = index + 1; - } catch (NoResultException e) { + // Find the largest child index in the existing ChildEntity objects ... + query = entities.createNamedQuery("ChildEntity.findMaximumChildIndex"); + query.setParameter("workspaceId", workspaceId); + query.setParameter("parentUuid", toUuidString); + try { + Integer index = (Integer)query.getSingleResult(); + if (index != null) nextIndexInParent = index + 1; + } catch (NoResultException e) { + } + } else { + /* + * This is a sub-optimal approach, particularly for inserts to the front + * of a long list of child nodes, but it guarantees that we won't have + * the JPA-cached entities and the database out of sync. + */ + + Query query = entities.createNamedQuery("ChildEntity.findAllUnderParent"); + query.setParameter("workspaceId", workspaceId); + query.setParameter("parentUuidString", toUuidString); + try { + List children = query.getResultList(); + Path beforePath = beforeLocation.getPath(); + Path.Segment beforeSegment = beforePath.getLastSegment(); + + boolean foundBefore = false; + for (ChildEntity child : children) { + NamespaceEntity namespace = child.getChildNamespace(); + if (namespace.getUri().equals(ns.getUri()) + && child.getChildName().equals(childLocalName) + && child.getSameNameSiblingIndex() == beforeSegment.getIndex()) { + foundBefore = true; + nextIndexInParent = child.getIndexInParent(); + nextSnsIndex = beforeSegment.getIndex(); + } + + if (foundBefore) { + child.setIndexInParent(child.getIndexInParent() + 1); + if (child.getChildName().equals(childLocalName) + && namespace.getUri().equals(ns.getUri())) { + child.setSameNameSiblingIndex(child.getSameNameSiblingIndex() + 1); + } + entities.persist(child); + } + } + + } catch (NoResultException e) { + } + } ChildId movedId = new ChildId(workspaceId, toUuidString, fromUuidString);