Index: dna-graph/src/main/java/org/jboss/dna/graph/Graph.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (revision 1125) +++ dna-graph/src/main/java/org/jboss/dna/graph/Graph.java (working copy) @@ -57,6 +57,7 @@ import org.jboss.dna.graph.property.Name; import org.jboss.dna.graph.property.NameFactory; import org.jboss.dna.graph.property.Path; +import org.jboss.dna.graph.property.PathFactory; import org.jboss.dna.graph.property.PathNotFoundException; import org.jboss.dna.graph.property.Property; import org.jboss.dna.graph.property.PropertyFactory; @@ -1218,6 +1219,34 @@ }; } + public AddValue addValue( Object value ) { + return new AddValueAction(this, this.getCurrentWorkspaceName(), value) { + + @Override + protected Graph submit( String workspaceName, + Location on, + Name property, + List values ) { + requests.addValues(workspaceName, on, property, values); + return nextGraph.and(); + } + }; + } + + public RemoveValue removeValue( Object value ) { + return new RemoveValueAction(this, this.getCurrentWorkspaceName(), value) { + + @Override + protected Graph submit( String workspaceName, + Location on, + Name property, + List values ) { + requests.removeValues(workspaceName, on, property, values); + return nextGraph.and(); + } + }; + } + /** * Set the properties on a node. * @@ -3002,6 +3031,34 @@ }; } + public AddValue addValue( Object value ) { + return new AddValueAction(this, this.getCurrentWorkspaceName(), value) { + + @Override + protected Batch submit( String workspaceName, + Location on, + Name property, + List values ) { + requests.addValues(workspaceName, on, property, values); + return nextRequests.and(); + } + }; + } + + public RemoveValue removeValue( Object value ) { + return new RemoveValueAction(this, this.getCurrentWorkspaceName(), value) { + + @Override + protected Batch submit( String workspaceName, + Location on, + Name property, + List values ) { + requests.removeValues(workspaceName, on, property, values); + return nextRequests.and(); + } + }; + } + /** * Set the properties on a node. * @@ -4080,6 +4137,32 @@ } /** + * A component that defines the name of a property to which a value should be added. + * + * @param The interface that is to be returned when this request is completed + */ + public interface ToName { + + public Next to( String name ); + + public Next to( Name name ); + + } + + /** + * A component that defines the name of a property from which a value should be removed. + * + * @param The interface that is to be returned when this request is completed + */ + public interface FromName { + + public Next from( String name ); + + public Next from( Name name ); + + } + + /** * 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 @@ -4945,6 +5028,189 @@ } /** + * The interface for defining the node on which an {@link Graph#addValue(Object)} operation applies and what additional values + * (if any) should be added. + * + * @param The interface that is to be returned when the request is completed + */ + public interface AddValue extends ToName> { + /** + * Specifies an additional value to be added + * + * @param value the value to be added + * @return an object that allows additional values to be specified for removal or for their location to be specified + */ + AddValue andValue( Object value ); + + } + + /** + * The interface for defining the node on which an {@link Graph#removeValue(Object)} operation applies and what additional + * values (if any) should be removed. + * + * @param The interface that is to be returned when the request is completed + */ + public interface RemoveValue extends FromName> { + + /** + * Specifies an additional value to be removed + * + * @param value the value to be removed + * @return an object that allows additional values to be specified for removal or for their location to be specified + */ + RemoveValue andValue( Object value ); + + } + + public abstract class AddValueAction extends AbstractAction implements AddValue { + + private final String workspaceName; + private final List values = new LinkedList(); + + protected AddValueAction( T afterConjunction, + String workspaceName, + Object firstValue ) { + super(afterConjunction); + + this.workspaceName = workspaceName; + this.values.add(firstValue); + } + + public AddValue andValue( Object nextValue ) { + this.values.add(nextValue); + return this; + } + + public On to( String name ) { + NameFactory nameFactory = context.getValueFactories().getNameFactory(); + + return to(nameFactory.create(name)); + } + + public On to( final Name name ) { + return new On() { + + @Override + public T on( Iterable idProperties ) { + return on(Location.create(idProperties)); + } + + @Override + public T on( Location to ) { + return submit(workspaceName, to, name, values); + } + + @Override + public T on( Path to ) { + return on(Location.create(to)); + } + + @Override + public T on( Property firstIdProperty, + Property... additionalIdProperties ) { + return on(Location.create(firstIdProperty, additionalIdProperties)); + } + + @Override + public T on( Property idProperty ) { + return on(Location.create(idProperty)); + } + + @Override + public T on( String toPath ) { + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + return on(Location.create(pathFactory.create(toPath))); + } + + @Override + public T on( UUID to ) { + return on(Location.create(to)); + } + }; + } + + protected abstract T submit( String workspaceName, + Location on, + Name property, + List values ); + + } + + public abstract class RemoveValueAction extends AbstractAction implements RemoveValue { + + private final String workspaceName; + private final List values = new LinkedList(); + + protected RemoveValueAction( T afterConjunction, + String workspaceName, + Object firstValue ) { + super(afterConjunction); + + this.workspaceName = workspaceName; + this.values.add(firstValue); + } + + public RemoveValue andValue( Object nextValue ) { + this.values.add(nextValue); + return this; + } + + public On from( String name ) { + NameFactory nameFactory = context.getValueFactories().getNameFactory(); + + return from(nameFactory.create(name)); + } + + public On from( final Name name ) { + return new On() { + + @Override + public T on( Iterable idProperties ) { + return on(Location.create(idProperties)); + } + + @Override + public T on( Location to ) { + return submit(workspaceName, to, name, values); + } + + @Override + public T on( Path to ) { + return on(Location.create(to)); + } + + @Override + public T on( Property firstIdProperty, + Property... additionalIdProperties ) { + return on(Location.create(firstIdProperty, additionalIdProperties)); + } + + @Override + public T on( Property idProperty ) { + return on(Location.create(idProperty)); + } + + @Override + public T on( String toPath ) { + PathFactory pathFactory = context.getValueFactories().getPathFactory(); + return on(Location.create(pathFactory.create(toPath))); + } + + @Override + public T on( UUID to ) { + return on(Location.create(to)); + } + }; + } + + protected abstract T submit( String workspaceName, + Location on, + Name property, + List values ); + + } + + /** * A component used to set the values on a property. * * @param the next command Index: dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java (revision 1125) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/processor/RequestProcessor.java (working copy) @@ -23,7 +23,10 @@ */ package org.jboss.dna.graph.request.processor; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -68,6 +71,7 @@ import org.jboss.dna.graph.request.SetPropertyRequest; import org.jboss.dna.graph.request.UnsupportedRequestException; import org.jboss.dna.graph.request.UpdatePropertiesRequest; +import org.jboss.dna.graph.request.UpdateValuesRequest; import org.jboss.dna.graph.request.VerifyNodeExistsRequest; import org.jboss.dna.graph.request.VerifyWorkspaceRequest; @@ -259,6 +263,8 @@ process((CloneWorkspaceRequest)request); } else if (request instanceof DestroyWorkspaceRequest) { process((DestroyWorkspaceRequest)request); + } else if (request instanceof UpdateValuesRequest) { + process((UpdateValuesRequest)request); } else { processUnknownRequest(request); } @@ -749,6 +755,59 @@ public abstract void process( UpdatePropertiesRequest request ); /** + * Process a request to add and/or remove the specified values from a property on the given node. + *

+ * This method does nothing if the request is null. + *

+ * + * @param request the remove request + */ + public void process( UpdateValuesRequest request ) { + String workspaceName = request.inWorkspace(); + Location on = request.on(); + Name propertyName = request.property(); + + // Read in the current values + ReadPropertyRequest readProperty = new ReadPropertyRequest(on, workspaceName, propertyName); + process(readProperty); + + if (readProperty.hasError()) { + request.setError(readProperty.getError()); + return; + } + + Property property = readProperty.getProperty(); + List actualRemovedValues = new ArrayList(request.removedValues().size()); + List newValues = property == null ? new LinkedList() : new LinkedList( + Arrays.asList(property.getValuesAsArray())); + // Calculate what the new values should be + for (Object removedValue : request.removedValues()) { + for (Iterator iter = newValues.iterator(); iter.hasNext();) { + if (iter.next().equals(removedValue)) { + iter.remove(); + actualRemovedValues.add(removedValue); + break; + } + } + } + + newValues.addAll(request.addedValues()); + Property newProperty = getExecutionContext().getPropertyFactory().create(propertyName, newValues); + + // Update the current values + SetPropertyRequest setProperty = new SetPropertyRequest(on, workspaceName, newProperty); + process(setProperty); + + if (setProperty.hasError()) { + request.setError(setProperty.getError()); + } else { + // Set the actual location ... + request.setActualLocation(setProperty.getActualLocationOfNode(), request.addedValues(), actualRemovedValues); + } + + } + + /** * Process a request to rename a node specified location into a different location. *

* This method does nothing if the request is null. Unless overridden, this method converts the rename into a 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 1125) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/RequestBuilder.java (working copy) @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import org.jboss.dna.graph.Location; import org.jboss.dna.graph.NodeConflictBehavior; @@ -578,4 +579,41 @@ String workspaceName ) { return process(new DeleteBranchRequest(at, workspaceName)); } + + /** + * Add a request to add values to a property on an existing node + * + * @param workspaceName the name of the workspace containing the node; may not be null + * @param on the location of the node; may not be null + * @param property the name of the property; may not be null + * @param values the new values to add; may not be null + * @return the request; never null + */ + public UpdateValuesRequest addValues( String workspaceName, + Location on, + Name property, + List values ) { + UpdateValuesRequest request = new UpdateValuesRequest(workspaceName, on, property, values, null); + process(request); + return request; + } + + /** + * Add a request to remove values from a property on an existing node + * + * @param workspaceName the name of the workspace containing the node; may not be null + * @param on the location of the node; may not be null + * @param property the name of the property; may not be null + * @param values the new values to remove; may not be null + * @return the request; never null + */ + public UpdateValuesRequest removeValues( String workspaceName, + Location on, + Name property, + List values ) { + UpdateValuesRequest request = new UpdateValuesRequest(workspaceName, on, property, null, values); + process(request); + return request; + } + } Index: dna-graph/src/main/java/org/jboss/dna/graph/request/UpdateValuesRequest.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/request/UpdateValuesRequest.java (revision 0) +++ dna-graph/src/main/java/org/jboss/dna/graph/request/UpdateValuesRequest.java (revision 0) @@ -0,0 +1,176 @@ +package org.jboss.dna.graph.request; + +import java.util.Collections; +import java.util.List; +import org.jboss.dna.graph.GraphI18n; +import org.jboss.dna.graph.Location; +import org.jboss.dna.graph.property.Name; +import org.jboss.dna.graph.property.Path; +import org.jboss.dna.graph.property.Property; + +/** + * Instruction to update the values for a certain property on the node at the specified location. + *

+ * This request is capable of specifying specific values for the property that will be added or removed. Other values for the + * property not be affected by this request. The request contains a workspace name and a location that uniquely identify a node in + * the workspace as well as the name of property (that may or may not previously exist) on the node. The request also contains + * zero or more values to add and zero or more values to remove from the property. All values will be appended to the list of + * values. Removals are processed before additions. + *

+ *

+ * Even if the property has no values after this call, the property itself will not be removed by this request. + *

+ *

+ * Note that the number of values in a property (e.g., {@link Property#size()}, {@link Property#isEmpty()}, + * {@link Property#isSingle()}, and {@link Property#isMultiple()}) has no influence on whether the property should be removed. It + * is possible for a property to have no values. + *

+ */ +public class UpdateValuesRequest extends ChangeRequest { + + private static final long serialVersionUID = 1L; + + private final String workspaceName; + private final Location on; + private final Name propertyName; + private final List addedValues; + private final List removedValues; + + private Location actualLocation; + private List actualAddedValues; + private List actualRemovedValues; + + + public UpdateValuesRequest( String workspaceName, + Location on, + Name propertyName, + List addedValues, + List removedValues ) { + super(); + + assert workspaceName != null; + assert on != null; + assert propertyName != null; + + this.workspaceName = workspaceName; + this.on = on; + this.propertyName = propertyName; + this.addedValues = addedValues == null ? Collections.emptyList() : addedValues; + this.removedValues = removedValues == null ? Collections.emptyList() : removedValues; + } + + /** + * Get the location defining the node that is to be updated. + * + * @return the location of the node; never null + */ + public Location on() { + return on; + } + + /** + * Get the name of the property that is to be updated. + * + * @return the name of the property; never null + */ + public Name property() { + return propertyName; + } + + /** + * Get the name of the workspace in which the node exists. + * + * @return the name of the workspace; never null + */ + public String inWorkspace() { + return workspaceName; + } + + /** + * Get the list of values to be added. + * + * @return the values (if any) to be added; never null + */ + public List addedValues() { + return addedValues; + } + + /** + * Get the list of values to be removed. + * + * @return the values (if any) to be removed; never null + */ + public List removedValues() { + return removedValues; + } + + @Override + public Location changedLocation() { + return on; + } + + @Override + public String changedWorkspace() { + return workspaceName; + } + + @Override + public boolean changes( String workspace, + Path path ) { + return workspaceName.equals(workspace) && on.hasPath() && on.getPath().equals(path); + } + + @Override + public boolean isReadOnly() { + return addedValues.isEmpty() && removedValues.isEmpty(); + } + + public void setActualLocation(Location actual, List actualAddedValues, List actualRemovedValues) { + checkNotFrozen(); + if (!on.isSame(actual)) { // not same if actual is null + throw new IllegalArgumentException(GraphI18n.actualLocationIsNotSameAsInputLocation.text(actual, on)); + } + assert actual != null; + if (!actual.hasPath()) { + throw new IllegalArgumentException(GraphI18n.actualLocationMustHavePath.text(actual)); + } + this.actualLocation = actual; + assert actualLocation != null; + + assert actualAddedValues != null; + assert actualAddedValues.size() <= addedValues.size(); + assert actualRemovedValues != null; + assert actualRemovedValues.size() <= actualRemovedValues.size(); + + this.actualAddedValues = actualAddedValues; + this.actualRemovedValues = actualRemovedValues; + } + + /** + * Get the actual location of the node that was updated. + * + * @return the actual location, or null if the actual location was not set + */ + public Location getActualLocationOfNode() { + return actualLocation; + } + + /** + * Get the actual added values. This should always be identical to the list of values that were requested to be added. + * + * @return the values that were added to the node when this request was processed; never null + */ + public List getActualAddedValues() { + return actualAddedValues; + } + + /** + * Get the actual removed values. This will differ from the values that were requested to be removed if some of the values + * that were requested to be removed were not already values for the property. + * + * @return the values that were removed from the node when this request was processed; never null + */ + public List getActualRemovedValues() { + return actualRemovedValues; + } +} Property changes on: dna-graph\src\main\java\org\jboss\dna\graph\request\UpdateValuesRequest.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF 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 1125) +++ dna-graph/src/test/java/org/jboss/dna/graph/connector/test/WritableConnectorTest.java (working copy) @@ -1849,4 +1849,101 @@ "The quick brown fox jumped over the moon. What? ")); } + @Test + public void shouldAddValuesToExistingProperty() { + + String initialPath = ""; + int depth = 1; + int numChildrenPerNode = 1; + int numPropertiesPerNode = 1; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + Subgraph subgraph = graph.getSubgraphOfDepth(2).at("/"); + assertThat(subgraph, is(notNullValue())); + + assertThat(subgraph.getNode("node1"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + + graph.addValue("foo").andValue("bar").to("property1").on("node1"); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + subgraph = graph.getSubgraphOfDepth(2).at("/"); + assertThat(subgraph, is(notNullValue())); + + assertThat(subgraph.getNode("node1"), hasProperty("property1", + "The quick brown fox jumped over the moon. What? ", + "foo", + "bar")); + } + + @Test + public void shouldAddValuesToNonExistantProperty() { + + String initialPath = ""; + int depth = 1; + int numChildrenPerNode = 1; + int numPropertiesPerNode = 1; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + + graph.addValue("foo").andValue("bar").to("newProperty").on("node1"); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + Subgraph subgraph = graph.getSubgraphOfDepth(2).at("/"); + assertThat(subgraph, is(notNullValue())); + + assertThat(subgraph.getNode("node1"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + assertThat(subgraph.getNode("node1"), hasProperty("newProperty", "foo", "bar")); + + } + + @Test + public void shouldRemoveValuesFromExistingProperty() { + + String initialPath = ""; + int depth = 1; + int numChildrenPerNode = 1; + int numPropertiesPerNode = 1; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + + graph.removeValue("The quick brown fox jumped over the moon. What? ").andValue("bar").from("property1").on("node1"); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + Subgraph subgraph = graph.getSubgraphOfDepth(2).at("/"); + assertThat(subgraph, is(notNullValue())); + + assertThat(subgraph.getNode("node1"), hasProperty("property1")); + } + + @Test + public void shouldNotRemoveValuesFromNonExistantProperty() { + + String initialPath = ""; + int depth = 1; + int numChildrenPerNode = 1; + int numPropertiesPerNode = 1; + Stopwatch sw = new Stopwatch(); + boolean batch = true; + createSubgraph(graph, initialPath, depth, numChildrenPerNode, numPropertiesPerNode, batch, sw, System.out, null); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + + graph.removeValue("The quick brown fox jumped over the moon. What? ").from("noSuchProperty").on("node1"); + + assertThat(graph.getChildren().of("/"), hasChildren(segment("node1"))); + Subgraph subgraph = graph.getSubgraphOfDepth(2).at("/"); + assertThat(subgraph, is(notNullValue())); + + assertThat(subgraph.getNode("node1"), hasProperty("property1", "The quick brown fox jumped over the moon. What? ")); + } + }