Index: docs/reference/src/main/docbook/en-US/content/jcr/web_access.xml
===================================================================
--- docs/reference/src/main/docbook/en-US/content/jcr/web_access.xml (revision 2654)
+++ docs/reference/src/main/docbook/en-US/content/jcr/web_access.xml (working copy)
@@ -684,7 +684,7 @@ POST http://www.example.com/resources/modeshape%3arepository/default/items/newNo
proper determination of whether the values are binary; see the
next section"").
+ The JSON request can even contain a properties container:
+
+
+
+ A subgraph can be updated all at once using a PUT against a URI of the top node in the subgraph. Note that
+ in this case, very node in the subgraph must be provided in the JSON request (any node not in the request will be removed).
+ This method will attempt to set all of the properties to the new value(s) as specified in the JSON request,
+ plus any descendant node in the JSON request that doesn't reflect an existing node will be created while any
+ existing node not reflected in the JSON request will be removed. (Any specifications of "jcr:primaryType" are ignored
+ if the node already exists.) In other words, the request only needs to contain the properties that are changed.
+ Of course, if a node is being added, all of its properties need to be included in the request.
+
+
+ Here is an example:
+
+ This will update the existing node at "/some/existing/node" with the specified properties, and ensure
+ that it contains one child node named "childNode". Note that the body of this request is identical in structure
+ to that of the POST requests.
Index: web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/RestClientI18n.java
===================================================================
--- web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/RestClientI18n.java (revision 2654)
+++ web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/RestClientI18n.java (working copy)
@@ -43,6 +43,7 @@ public final class RestClientI18n {
// JsonRestClient messages
public static I18n connectionErrorMsg;
public static I18n createFileFailedMsg;
+ public static I18n updateFileFailedMsg;
public static I18n createFolderFailedMsg;
public static I18n getRepositoriesFailedMsg;
public static I18n getWorkspacesFailedMsg;
Index: web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/json/JsonRestClient.java
===================================================================
--- web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/json/JsonRestClient.java (revision 2654)
+++ web/modeshape-web-jcr-rest-client/src/main/java/org/modeshape/web/jcr/rest/client/json/JsonRestClient.java (working copy)
@@ -122,6 +122,44 @@ public final class JsonRestClient implements IRestClient {
}
/**
+ * Creates a file node in the specified repository. Note: All parent folders are assumed to already exist.
+ *
+ * @param workspace the workspace where the file node is being created
+ * @param path the path in the workspace to the folder where the file node is being created
+ * @param file the file whose contents will be contained in the file node being created
+ * @throws Exception if there is a problem creating the file
+ */
+ private void updateFileNode( Workspace workspace,
+ String path,
+ File file ) throws Exception {
+ LOGGER.trace("updateFileNode: workspace={0}, path={1}, file={2}", workspace.getName(), path, file.getAbsolutePath());
+ FileNode fileNode = new FileNode(workspace, path, file);
+ URL fileNodeUrl = fileNode.getUrl();
+ URL fileNodeUrlWithTerseResponse = new URL(fileNodeUrl.toString() + "?mode:includeNode=false");
+ HttpClientConnection connection = connect(workspace.getServer(), fileNodeUrlWithTerseResponse, RequestMethod.PUT);
+
+ try {
+ LOGGER.trace("updateFileNode: create node={0}", fileNode);
+ connection.write(fileNode.getContent());
+
+ // make sure node was created
+ int responseCode = connection.getResponseCode();
+
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ // node was not updated
+ LOGGER.error(RestClientI18n.connectionErrorMsg, responseCode, "updateFileNode");
+ String msg = RestClientI18n.updateFileFailedMsg.text(file.getName(), path, workspace.getName(), responseCode);
+ throw new RuntimeException(msg);
+ }
+ } finally {
+ if (connection != null) {
+ LOGGER.trace("updateFileNode: leaving");
+ connection.disconnect();
+ }
+ }
+ }
+
+ /**
* Creates a folder node in the specified workspace. Note: All parent folders are assumed to already exist.
*
* @param workspace the workspace where the folder node is being created
@@ -237,27 +275,27 @@ public final class JsonRestClient implements IRestClient {
public Map getNodeTypes( Repository repository ) throws Exception {
assert repository != null;
LOGGER.trace("getNodeTypes: workspace={0}", repository);
-
+
// because the http:// needs the workspace when it appends the depth option
// this logic must be used to obtain one.
Collection workspaces = getWorkspaces(repository);
Workspace workspace = null;
Workspace systemWs = null;
for (Workspace wspace : workspaces) {
- if (wspace.getName().equalsIgnoreCase("default")) {
- workspace = wspace;
- break;
- }
- if (workspace == null && !wspace.getName().equalsIgnoreCase("system")) {
- workspace = wspace;
- }
-
- if (wspace.getName().equalsIgnoreCase("system")) {
- systemWs = wspace;
- }
+ if (wspace.getName().equalsIgnoreCase("default")) {
+ workspace = wspace;
+ break;
+ }
+ if (workspace == null && !wspace.getName().equalsIgnoreCase("system")) {
+ workspace = wspace;
+ }
+
+ if (wspace.getName().equalsIgnoreCase("system")) {
+ systemWs = wspace;
+ }
}
if (workspace == null) {
- workspace = systemWs;
+ workspace = systemWs;
}
NodeTypeNode nodetypeNode = new NodeTypeNode(workspace);
@@ -412,16 +450,16 @@ public final class JsonRestClient implements IRestClient {
LOGGER.trace("publish: workspace={0}, path={1}, file={2}", workspace.getName(), path, file.getAbsolutePath());
try {
- // first delete if file exists at that path
if (pathExists(workspace, path, file)) {
- unpublish(workspace, path, file);
+ // Update it ...
+ updateFileNode(workspace, path, file);
} else {
// doesn't exist so make sure the parent path exists
ensureFolderExists(workspace, path);
+ // publish file
+ createFileNode(workspace, path, file);
}
- // publish file
- createFileNode(workspace, path, file);
} catch (Exception e) {
String msg = RestClientI18n.publishFailedMsg.text(file.getAbsolutePath(), path, workspace.getName());
return new Status(Severity.ERROR, msg, e);
Index: web/modeshape-web-jcr-rest-client/src/main/resources/org/modeshape/web/jcr/rest/client/RestClientI18n.properties
===================================================================
--- web/modeshape-web-jcr-rest-client/src/main/resources/org/modeshape/web/jcr/rest/client/RestClientI18n.properties (revision 2654)
+++ web/modeshape-web-jcr-rest-client/src/main/resources/org/modeshape/web/jcr/rest/client/RestClientI18n.properties (working copy)
@@ -40,6 +40,7 @@ unableToConvertValue = Unable to convert '{0}' from type {1} to {2}
connectionErrorMsg = response code={0} method={1}
createFileFailedMsg = Creating the "{0}" file node in folder "{1}" in workspace "{2}" failed with HTTP response code of "{3}"
+updateFileFailedMsg = Updating the "{0}" file node in folder "{1}" in workspace "{2}" failed with HTTP response code of "{3}"
createFolderFailedMsg = Creating the "{0}" folder node in workspace "{1}" failed with HTTP response code of "{2}"
getRepositoriesFailedMsg = Obtaining the repositories from server "{0}" failed with HTTP response code of "{1}"
getWorkspacesFailedMsg = Obtaining the workspaces from repository "{0}" at server "{1}" failed with HTTP response code of "{2}"
Index: web/modeshape-web-jcr-rest/src/main/java/org/modeshape/web/jcr/rest/ItemHandler.java
===================================================================
--- web/modeshape-web-jcr-rest/src/main/java/org/modeshape/web/jcr/rest/ItemHandler.java (revision 2654)
+++ web/modeshape-web-jcr-rest/src/main/java/org/modeshape/web/jcr/rest/ItemHandler.java (working copy)
@@ -4,9 +4,13 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import javax.jcr.Binary;
import javax.jcr.Item;
@@ -504,8 +508,7 @@ class ItemHandler extends AbstractHandler {
assert rawWorkspaceName != null;
Session session = getSession(request, rawRepositoryName, rawWorkspaceName);
- Node node;
- Item item;
+ Item item = null;
if ("".equals(path) || "/".equals(path)) {
item = session.getRootNode();
} else {
@@ -516,30 +519,119 @@ class ItemHandler extends AbstractHandler {
}
}
+ Node node = updateItem(item, requestContent);
+ node.getSession().save();
+ return jsonFor(node, 0).toString();
+ }
+
+ /**
+ * Updates the existing item based upon the supplied JSON content.
+ *
+ * @param item the node or property to be updated
+ * @param requestContent the JSON-encoded representation of the item(s) to be updated
+ * @return the node that was updated; never null
+ * @throws JSONException if there is an error encoding the node
+ * @throws RepositoryException if any other error occurs
+ */
+ private Node updateItem( Item item,
+ String requestContent ) throws RepositoryException, JSONException {
if (item instanceof Node) {
- JSONObject properties = new JSONObject(requestContent);
- node = (Node)item;
+ JSONObject jsonNode = new JSONObject(requestContent);
+ return updateNode((Node)item, jsonNode);
+ }
+
+ // Otherwise the incoming content should be a JSON object containing the property name and
+ // a value that is either a JSON string or a JSON array.
+ Property property = (Property)item;
+ String propertyName = property.getName();
+ JSONObject jsonProperty = new JSONObject(requestContent);
+ String jsonPropertyName = jsonProperty.has(propertyName) ? propertyName : propertyName + BASE64_ENCODING_SUFFIX;
+ Node node = property.getParent();
+ setPropertyOnNode(node, jsonPropertyName, jsonProperty.get(jsonPropertyName));
+ return node;
+ }
+
+ /**
+ * Updates the existing node with the properties (and optionally children) as described by {@code jsonNode}.
+ *
+ * @param node the node to be updated
+ * @param jsonNode the JSON-encoded representation of the node or nodes to be updated.
+ * @return the Node that was updated; never null
+ * @throws JSONException if there is an error encoding the node
+ * @throws RepositoryException if any other error occurs
+ */
+ private Node updateNode( Node node,
+ JSONObject jsonNode ) throws RepositoryException, JSONException {
+ // If the JSON object has a properties holder, then this is likely a subgraph ...
+ JSONObject properties = jsonNode;
+ if (jsonNode.has(PROPERTIES_HOLDER)) {
+ properties = jsonNode.getJSONObject(PROPERTIES_HOLDER);
+ }
+
+ // Check out the node if it is versionable ...
+ boolean versionable = node.isNodeType("mix:versionable");
+ if (versionable) {
+ node.getSession().getWorkspace().getVersionManager().checkout(node.getPath());
+ // If this fails, we don't need to do a checkin ...
+ }
+
+ try {
for (Iterator> iter = properties.keys(); iter.hasNext();) {
String key = (String)iter.next();
-
+ if (PRIMARY_TYPE_PROPERTY.equals(key)) continue; // can't change the primary type
setPropertyOnNode(node, key, properties.get(key));
}
- } else {
- /*
- * The incoming content should be a JSON object containing the property name and a value that is either a JSON
- * string or a JSON array.
- */
- Property property = (Property)item;
- String propertyName = property.getName();
- JSONObject jsonProperty = new JSONObject(requestContent);
- String jsonPropertyName = jsonProperty.has(propertyName) ? propertyName : propertyName + BASE64_ENCODING_SUFFIX;
- node = property.getParent();
- setPropertyOnNode(node, jsonPropertyName, jsonProperty.get(jsonPropertyName));
+ // If the JSON object has a children holder, then we need to update the list of children and child nodes ...
+ if (jsonNode.has(CHILD_NODE_HOLDER)) {
+ Node parent = node;
+ JSONObject children = jsonNode.getJSONObject(CHILD_NODE_HOLDER);
+
+ // Get the existing children ...
+ Map existingChildNames = new LinkedHashMap();
+ NodeIterator childIter = parent.getNodes();
+ while (childIter.hasNext()) {
+ Node child = childIter.nextNode();
+ existingChildNames.put(nameOf(child), child);
+ }
+
+ for (Iterator> iter = children.keys(); iter.hasNext();) {
+ String childName = (String)iter.next();
+ JSONObject child = children.getJSONObject(childName);
+ // Find the existing node ...
+ if (parent.hasNode(childName)) {
+ // The node exists, so get it and update it ...
+ Node childNode = parent.getNode(childName);
+ updateNode(childNode, child);
+ existingChildNames.remove(nameOf(childNode));
+ } else {
+ // Have to add the new child ...
+ addNode(parent, childName, child);
+ }
+ }
+
+ // Remove the children in reverse order (starting with the last child to be removed) ...
+ LinkedList childNodes = new LinkedList(existingChildNames.values());
+ Collections.reverse(childNodes);
+ while (!childNodes.isEmpty()) {
+ Node child = childNodes.removeLast();
+ child.remove();
+ }
+ }
+ } finally {
+ if (versionable) {
+ node.getSession().getWorkspace().getVersionManager().checkin(node.getPath());
+ }
}
- node.getSession().save();
- return jsonFor(node, 0).toString();
+
+ return node;
+ }
+
+ private String nameOf( Node node ) throws RepositoryException {
+ int index = node.getIndex();
+ String childName = node.getName();
+ return index == 1 ? childName : childName + "[" + index + "]";
}
}