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 + "]"; } }