Index: dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java =================================================================== --- dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java (revision 923) +++ dna-graph/src/main/java/org/jboss/dna/graph/ExecutionContext.java (working copy) @@ -42,6 +42,7 @@ import org.jboss.dna.common.component.StandardClassLoaderFactory; import org.jboss.dna.common.util.CheckArg; import org.jboss.dna.common.util.Logger; +import org.jboss.dna.common.util.Reflection; import org.jboss.dna.graph.connector.federation.FederatedLexicon; import org.jboss.dna.graph.mimetype.ExtensionBasedMimeTypeDetector; import org.jboss.dna.graph.mimetype.MimeTypeDetector; @@ -528,6 +529,9 @@ * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[]) */ public void handle( Callback[] callbacks ) throws UnsupportedCallbackException, IOException { + boolean userSet = false; + boolean passwordSet = false; + for (int i = 0; i < callbacks.length; i++) { if (callbacks[i] instanceof TextOutputCallback) { @@ -563,6 +567,7 @@ } nc.setName(this.userId); + userSet = true; } else if (callbacks[i] instanceof PasswordCallback) { @@ -573,9 +578,32 @@ System.out.flush(); } pc.setPassword(this.password); + passwordSet = true; } else { - throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback"); + /* + * Jetty uses its own callback for setting the password. Since we're using Jetty for integration + * testing of the web project(s), we have to accomodate this. Rather than introducing a direct + * dependency, we'll add code to handle the case of unexpected callback handlers with a setObject method. + */ + try { + // Assume that a callback chain will ask for the user before the password + if (!userSet) { + new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object", callbacks[i], this.userId); + userSet = true; + } + else if (!passwordSet) { + // Jetty also seems to eschew passing passwords as char arrays + new Reflection(callbacks[i].getClass()).invokeSetterMethodOnTarget("object", callbacks[i], new String(this.password)); + passwordSet = true; + } + // It worked - need to continue processing the callbacks + continue; + } catch (Exception ex) { + // If the property cannot be set, fall through to the failure + } + throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback: " + + callbacks[i].getClass().getName()); } } Index: dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java =================================================================== --- dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java (revision 923) +++ dna-jcr/src/main/java/org/jboss/dna/jcr/JcrEngine.java (working copy) @@ -23,7 +23,10 @@ */ package org.jboss.dna.jcr; +import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; @@ -44,6 +47,7 @@ import org.jboss.dna.graph.property.Property; import org.jboss.dna.jcr.JcrRepository.Option; import org.jboss.dna.repository.DnaEngine; +import org.jboss.dna.repository.RepositoryLibrary; import org.jboss.dna.repository.RepositoryService; import org.jboss.dna.repository.sequencer.SequencingService; @@ -103,6 +107,24 @@ } /** + * Returns a list of the names of all available JCR repositories. + *

+ * In a {@code JcrEngine}, the available repositories are {@link RepositoryLibrary#getSourceNames() all repositories} except + * for the {@link RepositoryService#getConfigurationSourceName() the configuration repository}. + *

+ * + * @return a list of all repository names. + */ + public final Collection getJcrRepositoryNames() { + List jcrRepositories = new ArrayList(); + jcrRepositories.addAll(getRepositoryService().getRepositoryLibrary().getSourceNames()); + + jcrRepositories.remove(getRepositoryService().getConfigurationSourceName()); + + return jcrRepositories; + } + + /** * Get the {@link Repository} implementation for the named repository. * * @param repositoryName the name of the repository, which corresponds to the name of a configured {@link RepositorySource} Index: dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java =================================================================== --- dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java (revision 923) +++ dna-jcr/src/main/java/org/jboss/dna/jcr/JcrRepository.java (working copy) @@ -415,4 +415,10 @@ JcrWorkspace workspace = new JcrWorkspace(this, workspaceName, execContext, sessionAttributes); return workspace.getSession(); } + + public Set getAvailableWorkspaces() { + Graph graph = Graph.create(sourceName, connectionFactory, executionContext); + return graph.getWorkspaces(); + + } } Index: extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java =================================================================== --- extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java (revision 923) +++ extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/JcrResources.java (working copy) @@ -23,9 +23,53 @@ */ package org.jboss.dna.web.jcr.rest; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import javax.jcr.Credentials; +import javax.jcr.Item; +import javax.jcr.LoginException; +import javax.jcr.NoSuchWorkspaceException; +import javax.jcr.Node; +import javax.jcr.NodeIterator; +import javax.jcr.Property; +import javax.jcr.PropertyIterator; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.jcr.Session; +import javax.jcr.SimpleCredentials; +import javax.jcr.Value; +import javax.jcr.nodetype.PropertyDefinition; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.jboss.dna.common.text.UrlEncoder; +import org.jboss.dna.jcr.JcrRepository; +import org.jboss.dna.web.jcr.rest.model.RepositoryEntry; +import org.jboss.dna.web.jcr.rest.model.WorkspaceEntry; +import org.jboss.resteasy.spi.NotFoundException; +import org.jboss.resteasy.spi.UnauthorizedException; /** * RESTEasy handler to provide the JCR resources at the URIs below. Please note that these URIs assume a context of {@code @@ -42,45 +86,23 @@ * GET * * - * /resources/repositories - * returns a list of accessible repositories - * GET - * - * * /resources/{repositoryName} * returns a list of accessible workspaces within that repository * GET * * - * /resources/{repositoryName}/workspaces - * returns a list of accessible workspaces within that repository - * GET - * - * * /resources/{repositoryName}/{workspaceName} * returns a list of operations within the workspace * GET * * * /resources/{repositoryName}/{workspaceName}/item/{path} - * accesses the node at the path + * accesses the item (node or property) at the path * ALL * - * - * /resources/{repositoryName}/{workspaceName}/item/{path}/@{propertyName} - * accesses the named property at the path - * ALL (except PUT) - * - * - * /resources/{repositoryName}/{workspaceName}/item/{path}/@{propertyName} - * adds the value from the body to the named property at the path - * PUT - * - * - * /resources/{repositoryName}/{workspaceName}/uuid/{uuid} + * /resources/{repositoryName}/{workspaceName}/node/{uuid} * accesses the node with the given UUID - * ALL - * + * ALL * * /resources/{repositoryName}/{workspaceName}/lock/{path} * locks the node at the path @@ -96,25 +118,572 @@ @Path( "/" ) public class JcrResources { + private static final UrlEncoder URL_ENCODER = new UrlEncoder(); + + private static final String PROPERTIES_HOLDER = "properties"; + private static final String CHILD_NODE_HOLDER = "children"; + + private static final String PRIMARY_TYPE_PROPERTY = "jcr:primaryType"; + private static final String MIXIN_TYPES_PROPERTY = "jcr:mixinTypes"; + + /** Name to be used when the repository name is empty string as {@code "//"} is not a valid path. */ + public static final String EMPTY_REPOSITORY_NAME = ""; + /** Name to be used when the workspace name is empty string as {@code "//"} is not a valid path. */ + public static final String EMPTY_WORKSPACE_NAME = ""; + /** + * Returns a reference to the named repository, if it exists. + * + * @param uri the full URI for the request used to load the repository; only used for error message + * @param repositoryName the name of the repository to load + * @return the repository + * @throws NotFoundException if there is no repository with the given name + * @throws RepositoryException if any other error occurs + */ + private Repository getRepository( String uri, + String repositoryName ) throws NotFoundException, RepositoryException { + try { + return RepositoryFactory.getRepository(repositoryName); + } catch (RepositoryException re) { + throw new NotFoundException(uri); + } + } + + /** + * Returns an active session for the given workspace name in the named repository. + * + * @param uri the full URI for the request used to load the repository; only used for error message + * @param repositoryName the name of the repository in which the session is created + * @param workspaceName the name of the workspace to which the session should be connected + * @return an active session with the given workspace in the named repository + * @throws NotFoundException if no repository with the given name exists, no workspace with the given workspace name exists, + * or the workspace exists but the user does not have access to it. + * @throws RepositoryException if any other error occurs + */ + private Session getSession( String uri, + String repositoryName, + String workspaceName ) throws NotFoundException, RepositoryException { + Repository repository = getRepository(uri, repositoryName); + + Credentials credentials = new SimpleCredentials("dnauser", "password".toCharArray()); + + try { + Session session = repository.login(credentials, workspaceName); + + return session; + } catch (NoSuchWorkspaceException ne) { + throw new NotFoundException(uri); + } catch (LoginException le) { + throw new UnauthorizedException(uri); + } + } + + /** + * Returns the URI for the request relative to the servlet context. + * + * @param request the request + * @return the component of the request URI beginning after the servlet context path. E.g., the request URI {@code + * "/resources/repository/workspace"} would become {@code "/repository/workspace"}. + */ + private String relativeUriFor( HttpServletRequest request ) { + return request.getRequestURI().substring(request.getContextPath().length()); + } + + private String workspaceNameFor( String rawWorkspaceName ) { + String workspaceName = URL_ENCODER.decode(rawWorkspaceName); + + if (EMPTY_WORKSPACE_NAME.equals(workspaceName)) { + workspaceName = ""; + } + + return workspaceName; + } + + private String repositoryNameFor( String rawRepositoryName ) { + String repositoryName = URL_ENCODER.decode(rawRepositoryName); + + if (EMPTY_REPOSITORY_NAME.equals(repositoryName)) { + repositoryName = ""; + } + + return repositoryName; + } + + /** * Returns the list of JCR repositories available on this server + * + * @param request the servlet request; may not be null * @return the list of JCR repositories available on this server */ @GET - @Path( "/repositories" ) - public String repositories() { - return "Hello, DNA!"; + @Path( "/" ) + @Produces( "application/json" ) + public Map getRepositories( @Context HttpServletRequest request ) { + assert request != null; + + Map repositories = new HashMap(); + + for (String name : RepositoryFactory.getJcrRepositoryNames()) { + if (name.trim().length() == 0) { + name = EMPTY_REPOSITORY_NAME; + } + name = URL_ENCODER.encode(name); + repositories.put(name, new RepositoryEntry(request.getContextPath(), name)); + } + + return repositories; } /** * Returns the list of workspaces available to this user within the named repository. - * @param repositoryName the name of the repository + * + * @param rawRepositoryName the name of the repository; may not be null + * @param request the servlet request; may not be null * @return the list of workspaces available to this user within the named repository. + * @throws IOException if the given repository name does not map to any repositories and there is an error writing the error + * code to the response. + * @throws RepositoryException if there is any other error accessing the list of available workspaces for the repository */ @GET - @Path( "/{repositoryName}/workspaces" ) - public String workspaces( @PathParam( "repositoryName" ) String repositoryName ) { - return repositoryName; + @Path( "/{repositoryName}" ) + @Produces( "application/json" ) + public Map getWorkspaces( @Context HttpServletRequest request, + @PathParam( "repositoryName" ) String rawRepositoryName ) + throws RepositoryException, IOException { + + assert request != null; + assert rawRepositoryName != null; + + // Need to handle URL decoding + String repositoryName = repositoryNameFor(rawRepositoryName); + JcrRepository repository = (JcrRepository)getRepository(relativeUriFor(request), repositoryName); + + Map workspaces = new HashMap(); + repositoryName = URL_ENCODER.encode(repositoryName); + + for (String name : repository.getAvailableWorkspaces()) { + if (name.trim().length() == 0) { + name = EMPTY_WORKSPACE_NAME; + } + name = URL_ENCODER.encode(name); + workspaces.put(name, new WorkspaceEntry(request.getContextPath(), repositoryName, name)); + } + + return workspaces; } + /** + * Handles GET requests for the root item in a workspace. + * + * @param request the servlet request; may not be null + * @param rawRepositoryName the URL-encoded repository name + * @param rawWorkspaceName the URL-encoded workspace name + * @param depth the depth of the node graph that should be returned. @{code 0} means return the requested node only. A + * negative value indicates that the full subgraph under the node should be returned. This parameter defaults to + * {@code 0}. + * @return the JSON-encoded version of the root item (and its subgraph, depending on the value of {@code depth}) + * @throws NotFoundException if the named repository does not exists, the named workspace does not exist, or the user does not + * have access to the named workspace + * @throws UnauthorizedException if the given login information is invalid + * @throws RepositoryException if any other error occurs + * @see #EMPTY_REPOSITORY_NAME + * @see #EMPTY_WORKSPACE_NAME + * @see Session#getRootNode() + */ + @GET + @Path( "/{repositoryName}/{workspaceName}/items" ) + @Produces( "application/json" ) + public String getItem( @Context HttpServletRequest request, + @PathParam( "repositoryName" ) String rawRepositoryName, + @PathParam( "workspaceName" ) String rawWorkspaceName, + @QueryParam( "dna:depth" ) @DefaultValue( "0" ) int depth ) + throws NotFoundException, UnauthorizedException, RepositoryException { + return wrapItemWithJson(request, rawRepositoryName, rawWorkspaceName, null, depth); + } + + /** + * Handles GET requests for an item in a workspace. + * + * @param request the servlet request; may not be null + * @param rawRepositoryName the URL-encoded repository name + * @param rawWorkspaceName the URL-encoded workspace name + * @param path the path to the item + * @param depth the depth of the node graph that should be returned if {@code path} refers to a node. @{code 0} means return + * the requested node only. A negative value indicates that the full subgraph under the node should be returned. This + * parameter defaults to {@code 0} and is ignored if {@code path} refers to a property. + * @return the JSON-encoded version of the item (and, if the item is a node, its subgraph, depending on the value of {@code + * depth}) + * @throws NotFoundException if the named repository does not exists, the named workspace does not exist, or the user does not + * have access to the named workspace + * @throws UnauthorizedException if the given login information is invalid + * @throws RepositoryException if any other error occurs + * @see #EMPTY_REPOSITORY_NAME + * @see #EMPTY_WORKSPACE_NAME + * @see Session#getItem(String) + */ + @GET + @Path( "/{repositoryName}/{workspaceName}/items/{path:.*}" ) + @Produces( "application/json" ) + public String getItem( @Context HttpServletRequest request, + @PathParam( "repositoryName" ) String rawRepositoryName, + @PathParam( "workspaceName" ) String rawWorkspaceName, + @PathParam( "path" ) String path, + @QueryParam( "dna:depth" ) @DefaultValue( "0" ) int depth ) + throws NotFoundException, UnauthorizedException, RepositoryException { + return wrapItemWithJson(request, rawRepositoryName, rawWorkspaceName, path, depth); + } + + /** + * Looks up the node at {@code path} in the named workspace and repository and wraps it in a JSON string. + * + * @param request the servlet request; may not be null + * @param rawRepositoryName the URL-encoded repository name + * @param rawWorkspaceName the URL-encoded workspace name + * @param path the path to the item + * @param depth the depth of the node graph that should be returned if {@code path} refers to a node. @{code 0} means return + * the requested node only. A negative value indicates that the full subgraph under the node should be returned. This + * parameter defaults to {@code 0} and is ignored if {@code path} refers to a property. + * @return the JSON-encoded version of the item (and, if the item is a node, its subgraph, depending on the value of {@code + * depth}) + * @throws NotFoundException if the named repository does not exists, the named workspace does not exist, or the user does not + * have access to the named workspace + * @throws UnauthorizedException if the given login information is invalid + * @throws RepositoryException if any other error occurs + * @see #repositoryNameFor(String) + * @see #workspaceNameFor(String) + * @see Session#getItem(String) + */ + private String wrapItemWithJson( HttpServletRequest request, + String rawRepositoryName, + String rawWorkspaceName, + String path, + int depth ) throws NotFoundException, UnauthorizedException, RepositoryException { + assert rawRepositoryName != null; + assert rawWorkspaceName != null; + + String uri = relativeUriFor(request); + String repositoryName = repositoryNameFor(rawRepositoryName); + String workspaceName = workspaceNameFor(rawWorkspaceName); + + Session session = getSession(relativeUriFor(request), repositoryName, workspaceName); + try { + Item item; + if (path == null) { + item = session.getRootNode(); + } else { + item = session.getItem("/" + path); + } + + if (item instanceof Node) { + return jsonFor((Node)item, depth).toString(); + } + return jsonStringFor((Property)item); + } catch (JSONException je) { + throw new NotFoundException(uri); + } catch (RepositoryException re) { + throw new NotFoundException(uri); + } + + } + + /** + * Returns the JSON-encoded version of the given property. If the property is single-valued, the returned string is {@code + * property.getValue().getString()} encoded as a JSON string. If the property is multi-valued with {@code N} values, this + * method returns a JSON array containing {@code property.getValues()[N].getString()} for all values of {@code N}. + * + * @param property the property to be encoded + * @return the JSON-encoded version of the property + * @throws RepositoryException if an error occurs accessing the property, its values, or its definition. + * @see Property#getDefinition() + * @see PropertyDefinition#isMultiple() + */ + private String jsonStringFor( Property property ) throws RepositoryException { + if (property.getDefinition().isMultiple()) { + Value[] values = property.getValues(); + List list = new ArrayList(values.length); + for (int i = 0; i < values.length; i++) { + list.add(values[i].getString()); + } + return new JSONArray(list).toString(); + } + return JSONObject.quote(property.getValue().getString()); + } + + /** + * Recursively returns the JSON-encoding of a node and its children to depth {@code toDepth}. + * + * @param node the node to be encoded + * @param toDepth the depth to which the recursion should extend; {@code 0} means no further recursion should occur. + * @return the JSON-encoding of a node and its children to depth {@code toDepth}. + * @throws JSONException if there is an error encoding the node + * @throws RepositoryException if any other error occurs + */ + private JSONObject jsonFor( Node node, + int toDepth ) throws JSONException, RepositoryException { + JSONObject jsonNode = new JSONObject(); + + JSONObject properties = new JSONObject(); + + for (PropertyIterator iter = node.getProperties(); iter.hasNext();) { + Property prop = iter.nextProperty(); + String propName = prop.getName(); + + if (prop.getDefinition().isMultiple()) { + Value[] values = prop.getValues(); + JSONArray array = new JSONArray(); + for (int i = 0; i < values.length; i++) { + array.put(values[i].getString()); + } + properties.put(propName, array); + + } else { + properties.put(propName, prop.getValue().getString()); + } + + } + if (properties.length() > 0) { + jsonNode.put(PROPERTIES_HOLDER, properties); + } + + if (toDepth == 0) { + List children = new ArrayList(); + + for (NodeIterator iter = node.getNodes(); iter.hasNext();) { + Node child = iter.nextNode(); + + children.add(child.getName()); + } + + if (children.size() > 0) { + jsonNode.put(CHILD_NODE_HOLDER, new JSONArray(children)); + } + } else { + JSONObject children = new JSONObject(); + + for (NodeIterator iter = node.getNodes(); iter.hasNext();) { + Node child = iter.nextNode(); + + children.put(child.getName(), jsonFor(child, toDepth - 1)); + } + + if (children.length() > 0) { + jsonNode.put(CHILD_NODE_HOLDER, children); + } + } + + return jsonNode; + } + + /** + * Adds the content of the request as a node (or subtree of nodes) at the location specified by {@code path}. + *

+ * The primary type and mixin type(s) may optionally be specified through the {@code jcr:primaryType} and {@code + * jcr:mixinTypes} properties. + *

+ * + * @param request the servlet request; may not be null + * @param rawRepositoryName the URL-encoded repository name + * @param rawWorkspaceName the URL-encoded workspace name + * @param path the path to the item + * @param requestContent the JSON-encoded representation of the node or nodes to be added + * @return the JSON-encoded representation of the node or nodes that were added. This will differ from {@code requestContent} + * in that auto-created and protected properties (e.g., jcr:uuid) will be populated. + * @throws NotFoundException if the parent of the item to be added does not exist + * @throws UnauthorizedException if the user does not have the access required to create the node at this path + * @throws JSONException if there is an error encoding the node + * @throws RepositoryException if any other error occurs + */ + @POST + @Path( "/{repositoryName}/{workspaceName}/items/{path:.*}" ) + @Consumes( "application/json" ) + // @Produces( "application/json" ) + public Response postItem( @Context HttpServletRequest request, + @Context HttpServletResponse response, + @PathParam( "repositoryName" ) String rawRepositoryName, + @PathParam( "workspaceName" ) String rawWorkspaceName, + @PathParam( "path" ) String path, + String requestContent ) + throws NotFoundException, UnauthorizedException, RepositoryException, JSONException { + + assert request != null; + assert rawRepositoryName != null; + assert rawWorkspaceName != null; + assert path != null; + JSONObject body = new JSONObject(requestContent); + + String uri = relativeUriFor(request); + String repositoryName = repositoryNameFor(rawRepositoryName); + String workspaceName = workspaceNameFor(rawWorkspaceName); + + int lastSlashInd = path.lastIndexOf('/'); + String parentPath = lastSlashInd == -1 ? "/" : "/" + path.substring(0, lastSlashInd); + String newNodeName = lastSlashInd == -1 ? path : path.substring(lastSlashInd + 1); + + Session session = getSession(uri, repositoryName, workspaceName); + + Node parentNode = (Node)session.getItem(parentPath); + + Node newNode = addNode(parentNode, newNodeName, body); + + session.save(); + + String json = jsonFor(newNode, -1).toString(); + return Response.status(Status.CREATED).entity(json).build(); + } + + /** + * Adds the node described by {@code jsonNode} with name {@code nodeName} to the existing node {@code parentNode}. + * + * @param parentNode the parent of the node to be added + * @param nodeName the name of the node to be added + * @param jsonNode the JSON-encoded representation of the node or nodes to be added. + * @return the JSON-encoded representation of the node or nodes that were added. This will differ from {@code requestContent} + * in that auto-created and protected properties (e.g., jcr:uuid) will be populated. + * @throws JSONException if there is an error encoding the node + * @throws RepositoryException if any other error occurs + */ + private Node addNode( Node parentNode, + String nodeName, + JSONObject jsonNode ) throws RepositoryException, JSONException { + Node newNode; + + JSONObject properties = jsonNode.has(PROPERTIES_HOLDER) ? jsonNode.getJSONObject(PROPERTIES_HOLDER) : new JSONObject(); + + if (properties.has(PRIMARY_TYPE_PROPERTY)) { + String primaryType = properties.getString(PRIMARY_TYPE_PROPERTY); + newNode = parentNode.addNode(nodeName, primaryType); + } else { + newNode = parentNode.addNode(nodeName); + } + + if (properties.has(MIXIN_TYPES_PROPERTY)) { + Object rawMixinTypes = properties.get(MIXIN_TYPES_PROPERTY); + + if (rawMixinTypes instanceof JSONArray) { + JSONArray mixinTypes = (JSONArray)rawMixinTypes; + for (int i = 0; i < mixinTypes.length(); i++) { + newNode.addMixin(mixinTypes.getString(i)); + } + + } else { + newNode.addMixin(rawMixinTypes.toString()); + + } + } + + for (Iterator iter = properties.keys(); iter.hasNext();) { + String key = (String)iter.next(); + + if (PRIMARY_TYPE_PROPERTY.equals(key)) continue; + if (MIXIN_TYPES_PROPERTY.equals(key)) continue; + Object value = properties.get(key); + + if (value instanceof JSONArray) { + JSONArray jsonValues = (JSONArray)value; + String[] values = new String[jsonValues.length()]; + + for (int i = 0; i < values.length; i++) { + values[i] = jsonValues.getString(i); + } + newNode.setProperty(key, values); + } else { + newNode.setProperty(key, (String)value); + } + + } + + if (jsonNode.has(CHILD_NODE_HOLDER)) { + JSONObject children = jsonNode.getJSONObject(CHILD_NODE_HOLDER); + + for (Iterator iter = children.keys(); iter.hasNext();) { + String childName = (String)iter.next(); + JSONObject child = children.getJSONObject(childName); + + addNode(newNode, childName, child); + } + } + + return newNode; + } + + /** + * Deletes the item at {@code path}. + * + * @param request the servlet request; may not be null + * @param rawRepositoryName the URL-encoded repository name + * @param rawWorkspaceName the URL-encoded workspace name + * @param path the path to the item + * @throws NotFoundException if no item exists at {@code path} + * @throws UnauthorizedException if the user does not have the access required to delete the item at this path + * @throws RepositoryException if any other error occurs + */ + @DELETE + @Path( "/{repositoryName}/{workspaceName}/items/{path:.*}" ) + @Consumes( "application/json" ) + // @Produces( "application/json" ) + public void deleteItem( @Context HttpServletRequest request, + @PathParam( "repositoryName" ) String rawRepositoryName, + @PathParam( "workspaceName" ) String rawWorkspaceName, + @PathParam( "path" ) String path ) + throws NotFoundException, UnauthorizedException, RepositoryException { + + assert rawRepositoryName != null; + assert rawWorkspaceName != null; + assert path != null; + + String uri = relativeUriFor(request); + String repositoryName = repositoryNameFor(rawRepositoryName); + String workspaceName = workspaceNameFor(rawWorkspaceName); + + Session session = getSession(relativeUriFor(request), repositoryName, workspaceName); + Item item; + + // If we can't find the item, this should bubble out as a NotFoundException (404) + try { + item = session.getItem("/" + path); + } catch (RepositoryException re) { + throw new NotFoundException(uri); + } + + // If the item exists, but cannot be deleted, this is a RepositoryException (406) + item.remove(); + session.save(); + } + + @Provider + public static class NotFoundExceptionMapper implements ExceptionMapper { + + public Response toResponse( NotFoundException exception ) { + try { + URI uri = new URI(URL_ENCODER.encode(exception.getMessage())); + return Response.status(Status.NOT_FOUND).contentLocation(uri).build(); + } catch (URISyntaxException use) { + return Response.status(Status.NOT_FOUND).entity(exception.getMessage()).build(); + } + } + + } + + @Provider + public static class JSONExceptionMapper implements ExceptionMapper { + + public Response toResponse( JSONException exception ) { + return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build(); + } + + } + + @Provider + public static class RepositoryExceptionMapper implements ExceptionMapper { + + public Response toResponse( RepositoryException exception ) { + /* + * This error code is murky - the request must have been syntactically valid to get to + * the JCR operations, but there isn't an HTTP status code for "semantically invalid." + */ + return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build(); + } + + } + } Index: extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/RepositoryFactory.java =================================================================== --- extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/RepositoryFactory.java (revision 0) +++ extensions/dna-web-jcr-rest/src/main/java/org/jboss/dna/web/jcr/rest/RepositoryFactory.java (revision 0) @@ -0,0 +1,49 @@ +package org.jboss.dna.web.jcr.rest; + +import java.util.Collection; +import javax.jcr.Repository; +import javax.jcr.RepositoryException; +import javax.servlet.ServletContext; +import org.jboss.dna.graph.connector.inmemory.InMemoryRepositorySource; +import org.jboss.dna.jcr.JcrConfiguration; +import org.jboss.dna.jcr.JcrEngine; + +public class RepositoryFactory { + + private static JcrEngine jcrEngine; + + private RepositoryFactory() { + + } + + static void initialize(ServletContext context) { + jcrEngine = new JcrConfiguration().withConfigurationRepository() + .usingClass(InMemoryRepositorySource.class.getName()) + .loadedFromClasspath() + .describedAs("Configuration Repository") + .with("name").setTo("configuration") + .with("retryLimit") + .setTo(5) + .and() + .addRepository("Source2") + .usingClass(InMemoryRepositorySource.class.getName()) + .loadedFromClasspath() + .describedAs("description") + .with("name").setTo("JCR Repository") + .and() + .build(); + jcrEngine.start(); + } + + public static Repository getRepository(String repositoryName) throws RepositoryException { + return jcrEngine.getRepository(repositoryName); + } + + public static Collection getJcrRepositoryNames() { + return jcrEngine.getJcrRepositoryNames(); + } + + static void shutdown() { + jcrEngine.shutdown(); + } +} Property changes on: extensions\dna-web-jcr-rest\src\main\java\org\jboss\dna\web\jcr\rest\RepositoryFactory.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/main/webapp/WEB-INF/web.xml =================================================================== --- extensions/dna-web-jcr-rest/src/main/webapp/WEB-INF/web.xml (revision 923) +++ extensions/dna-web-jcr-rest/src/main/webapp/WEB-INF/web.xml (working copy) @@ -1,54 +1,60 @@ - + + JBoss DNA JCR RESTful Interface - JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA - is licensed to you under the terms of the GNU Lesser General Public License as - published by the Free Software Foundation; either version 2.1 of - the License, or (at your option) any later version. + + resteasy.providers + org.jboss.dna.web.jcr.rest.JcrResources$NotFoundExceptionMapper, + org.jboss.dna.web.jcr.rest.JcrResources$JSONExceptionMapper, + org.jboss.dna.web.jcr.rest.JcrResources$RepositoryExceptionMapper + + - JBoss DNA is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - Lesser General Public License for more details. + + javax.ws.rs.core.Application + org.jboss.dna.web.jcr.rest.JcrApplication + - You should have received a copy of the GNU Lesser General Public - License along with this software; if not, write to the Free - Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - 02110-1301 USA, or see the FSF site: http://www.fsf.org. + + + org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap + + - --> - - JBoss DNA JCR RESTful Interface + + org.jboss.dna.web.jcr.rest.DnaJcrDeployer + - - javax.ws.rs.core.Application - org.jboss.dna.web.jcr.rest.JcrApplication - - - - - org.jboss.resteasy.plugins.server.servlet.ResteasyBootstrap - - - - - Resteasy - - org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher + + Resteasy + + org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher - + - - Resteasy - /* - + + Resteasy + /* + \ No newline at end of file Index: extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java =================================================================== --- extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java (revision 923) +++ extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/JcrResourcesTest.java (working copy) @@ -24,52 +24,634 @@ package org.jboss.dna.web.jcr.rest; import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.hamcrest.core.IsNull.notNullValue; import static org.junit.Assert.assertThat; -import org.jboss.resteasy.client.ClientRequest; -import org.jboss.resteasy.client.ClientResponse; -import org.junit.Ignore; +import static org.junit.Assert.assertTrue; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashSet; +import java.util.Set; +import javax.ws.rs.core.MediaType; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONObject; import org.junit.Test; public class JcrResourcesTest { - private static final String SERVER_URL = "http://localhost:8080/resources"; - - private ClientRequest request; - private ClientResponse response; - - private ClientRequest requestFor(String path) { - return new ClientRequest(SERVER_URL + path); + private static final String SERVER_CONTEXT = "/resources"; + private static final String SERVER_URL = "http://admin:admin@localhost:8080" + SERVER_CONTEXT; + private static final String SERVER_URL_NO_CREDS = "http://localhost:8080" + SERVER_CONTEXT; + + private String getResponseFor( HttpURLConnection connection ) throws IOException { + StringBuffer buff = new StringBuffer(); + + InputStream stream = connection.getInputStream(); + int bytesRead; + byte[] bytes = new byte[1024]; + while (-1 != (bytesRead = stream.read(bytes, 0, 1024))) { + buff.append(new String(bytes, 0, bytesRead)); + } + + return buff.toString(); } - - @Ignore + @Test public void shouldServeContentAtRoot() throws Exception { - request = requestFor("/"); + URL postUrl = new URL(SERVER_URL + "/"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + String body = getResponseFor(connection); + + JSONObject objFromResponse = new JSONObject(body); + JSONObject expected = new JSONObject( + "{\"JCR%20Repository\":{\"repository\":{\"name\":\"JCR%20Repository\",\"resources\":{\"workspaces\":\"/resources/JCR%20Repository\"}}}}"); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + assertThat(objFromResponse.toString(), is(expected.toString())); + connection.disconnect(); + } + + @Test + public void shouldServeListOfWorkspacesForValidRepository() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + String body = getResponseFor(connection); + + JSONObject objFromResponse = new JSONObject(body); + JSONObject expected = new JSONObject( + "{\"%3cdefault%3e\":{\"workspace\":{\"name\":\"%3cdefault%3e\",\"resources\":{\"items\":\"/resources/JCR%20Repository/%3cdefault%3e/items\"}}}}"); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + assertThat(objFromResponse.toString(), is(expected.toString())); + connection.disconnect(); + } + + @Test + public void shouldReturnErrorForInvalidWorkspace() throws Exception { + URL postUrl = new URL(SERVER_URL + "/XXX"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + assertThat(connection.getHeaderField("content-location"), is(SERVER_URL_NO_CREDS + "/XXX")); + connection.disconnect(); + } + + @Test + public void shouldRetrieveRootNodeForValidRepository() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(2)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(2)); + assertThat(properties.getString("jcr:primaryType"), is("dna:root")); + assertThat(properties.get("jcr:uuid"), is(notNullValue())); + + JSONArray children = body.getJSONArray("children"); + assertThat(children.length(), is(1)); + assertThat(children.getString(0), is("jcr:system")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + } + + @Test + public void shouldRetrieveRootNodeAndChildrenWhenDepthSet() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items?dna:depth=1"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(2)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(2)); + assertThat(properties.getString("jcr:primaryType"), is("dna:root")); + assertThat(properties.get("jcr:uuid"), is(notNullValue())); + + JSONObject children = body.getJSONObject("children"); + assertThat(children.length(), is(1)); + + JSONObject system = children.getJSONObject("jcr:system"); + assertThat(system.length(), is(2)); + + properties = system.getJSONObject("properties"); + assertThat(properties.length(), is(1)); + assertThat(properties.getString("jcr:primaryType"), is("dna:system")); + + JSONArray namespaces = system.getJSONArray("children"); + assertThat(namespaces.length(), is(1)); + assertThat(namespaces.getString(0), is("dna:namespaces")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + } + + @Test + public void shouldRetrieveNodeAndChildrenWhenDepthSet() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/jcr:system?dna:depth=1"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(2)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(1)); + assertThat(properties.getString("jcr:primaryType"), is("dna:system")); + + JSONObject children = body.getJSONObject("children"); + assertThat(children.length(), is(1)); + + JSONObject namespaces = children.getJSONObject("dna:namespaces"); + assertThat(namespaces.length(), is(2)); + + properties = namespaces.getJSONObject("properties"); + assertThat(properties.length(), is(1)); + assertThat(properties.getString("jcr:primaryType"), is("dna:namespaces")); + + JSONArray namespace = namespaces.getJSONArray("children"); + assertThat(namespace.length(), is(10)); + Set prefixes = new HashSet(namespace.length()); + + for (int i = 0; i < namespace.length(); i++) { + prefixes.add(namespace.getString(i)); + } + + String[] expectedNamespaces = new String[] {"dna", "jcr", "nt", "mix", "sv", "xml", "dnaint", "xmlns", "xsi", "xsd"}; + for (int i = 0; i < expectedNamespaces.length; i++) { + assertTrue(prefixes.contains(expectedNamespaces[i])); + } + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + } + + @Test + public void shouldNotRetrieveNonExistentNode() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/foo"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + } + + @Test + public void shouldNotRetrieveNonExistentProperty() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/jcr:system/foobar"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + assertThat(connection.getHeaderField("content-location"), is(SERVER_URL_NO_CREDS + + "/JCR%20Repository/%3cdefault%3e/items/jcr:system/foobar")); + connection.disconnect(); + } + + @Test + public void shouldRetrieveProperty() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/jcr:system/jcr:primaryType"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String body = getResponseFor(connection); + assertThat(body, is("\"dna:system\"")); + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + } + + @Test + public void shouldPostNodeToValidPathWithPrimaryType() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nodeA"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}}"; + connection.getOutputStream().write(payload.getBytes()); - response = request.get(); + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("testProperty"), is("testValue")); + assertThat(properties.get("multiValuedProperty"), instanceOf(JSONArray.class)); + + JSONArray values = properties.getJSONArray("multiValuedProperty"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(2)); + assertThat(values.getString(0), is("value1")); + assertThat(values.getString(1), is("value2")); - assertThat(response.getStatus(), is(200)); + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); + } + + @Test + public void shouldPostNodeToValidPathWithoutPrimaryType() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/noPrimaryType"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{}"; + connection.getOutputStream().write(payload.getBytes()); + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(1)); + assertThat(properties.getString("jcr:primaryType"), is("nt:base")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); } @Test - public void shouldServeListOfRepositories() throws Exception { - request = requestFor("/repositories"); + public void shouldPostNodeToValidPathWithMixinTypes() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/withMixinType"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:mixinTypes\": \"mix:referenceable\"}}"; + connection.getOutputStream().write(payload.getBytes()); - response = request.get(); + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); - assertThat(response.getStatus(), is(200)); + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:base")); + assertThat(properties.getString("jcr:uuid"), is(notNullValue())); + + JSONArray values = properties.getJSONArray("jcr:mixinTypes"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(1)); + assertThat(values.getString(0), is("mix:referenceable")); + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); + + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/withMixinType"); + connection = (HttpURLConnection)postUrl.openConnection(); + + // Make sure that we can retrieve the node with a GET + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + body = new JSONObject(getResponseFor(connection)); + + assertThat(body.length(), is(1)); + + properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:base")); + assertThat(properties.getString("jcr:uuid"), is(notNullValue())); + + values = properties.getJSONArray("jcr:mixinTypes"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(1)); + assertThat(values.getString(0), is("mix:referenceable")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + } @Test - public void shouldServeListOfWorkspaces() throws Exception { - String validRepositoryName = "foo"; // Stub this for now - request = requestFor("/" + validRepositoryName + "/workspaces"); + public void shouldNotPostNodeAtInvalidParentPath() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/foo/bar"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + + } + + @Test + public void shouldNotPostNodeWithInvalidPrimaryType() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/invalidPrimaryType"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"invalidType\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}}"; + connection.getOutputStream().write(payload.getBytes()); - response = request.get(); + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + connection.disconnect(); - assertThat(response.getStatus(), is(200)); + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/invalidPrimaryType"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + } + @Test + public void shouldPostNodeHierarchy() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nestedPost"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}," + + " \"children\": { \"childNode\" : { \"properties\": {\"nestedProperty\": \"nestedValue\"}}}}"; + connection.getOutputStream().write(payload.getBytes()); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); + + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nestedPost?dna:depth=1"); + connection = (HttpURLConnection)postUrl.openConnection(); + + // Make sure that we can retrieve the node with a GET + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + JSONObject body = new JSONObject(getResponseFor(connection)); + + assertThat(body.length(), is(2)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("testProperty"), is("testValue")); + assertThat(properties.get("multiValuedProperty"), instanceOf(JSONArray.class)); + + JSONArray values = properties.getJSONArray("multiValuedProperty"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(2)); + assertThat(values.getString(0), is("value1")); + assertThat(values.getString(1), is("value2")); + + JSONObject children = body.getJSONObject("children"); + assertThat(children, is(notNullValue())); + assertThat(children.length(), is(1)); + + JSONObject child = children.getJSONObject("childNode"); + assertThat(child, is(notNullValue())); + assertThat(child.length(), is(1)); + + properties = child.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(2)); + // Parent primary type is nt:unstructured, so this should default to nt:unstructured primary type + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("nestedProperty"), is("nestedValue")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + + } + + @Test + public void shouldFailWholeTransactionIfOneNodeIsBad() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/invalidNestedPost"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}," + + " \"children\": { \"childNode\" : { \"properties\": {\"jcr:primaryType\": \"invalidType\"}}}}"; + connection.getOutputStream().write(payload.getBytes()); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_BAD_REQUEST)); + connection.disconnect(); + + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/invalidNestedPost?dna:depth=1"); + connection = (HttpURLConnection)postUrl.openConnection(); + + // Make sure that we can retrieve the node with a GET + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + + } + + @Test + public void shouldNotDeleteNonExistentItem() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/invalidItemForDelete"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("DELETE"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + } + + @Test + public void shouldDeleteExtantNode() throws Exception { + + // Create the node + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nodeForDeletion"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}}"; + connection.getOutputStream().write(payload.getBytes()); + + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("testProperty"), is("testValue")); + assertThat(properties.get("multiValuedProperty"), instanceOf(JSONArray.class)); + + JSONArray values = properties.getJSONArray("multiValuedProperty"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(2)); + assertThat(values.getString(0), is("value1")); + assertThat(values.getString(1), is("value2")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); + + // Confirm that it exists + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nodeForDeletion"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + + // Delete the node + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nodeForDeletion"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("DELETE"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NO_CONTENT)); + connection.disconnect(); + + // Confirm that it no longer exists + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/nodeForDeletion"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NOT_FOUND)); + connection.disconnect(); + } + + @Test + public void shouldDeleteExtantProperty() throws Exception { + URL postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/propertyForDeletion"); + HttpURLConnection connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + String payload = "{ \"properties\": {\"jcr:primaryType\": \"nt:unstructured\", \"testProperty\": \"testValue\", \"multiValuedProperty\": [\"value1\", \"value2\"]}}"; + connection.getOutputStream().write(payload.getBytes()); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_CREATED)); + connection.disconnect(); + + // Confirm that it exists + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/propertyForDeletion"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + JSONObject body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); + + JSONObject properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(3)); + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("testProperty"), is("testValue")); + assertThat(properties.get("multiValuedProperty"), instanceOf(JSONArray.class)); + + JSONArray values = properties.getJSONArray("multiValuedProperty"); + assertThat(values, is(notNullValue())); + assertThat(values.length(), is(2)); + assertThat(values.getString(0), is("value1")); + assertThat(values.getString(1), is("value2")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + + // Delete the property + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/propertyForDeletion/multiValuedProperty"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("DELETE"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_NO_CONTENT)); + connection.disconnect(); + + // Confirm that it no longer exists + postUrl = new URL(SERVER_URL + "/JCR%20Repository/%3cdefault%3e/items/propertyForDeletion"); + connection = (HttpURLConnection)postUrl.openConnection(); + + connection.setDoOutput(true); + connection.setRequestMethod("GET"); + connection.setRequestProperty("Content-Type", MediaType.APPLICATION_JSON); + + body = new JSONObject(getResponseFor(connection)); + assertThat(body.length(), is(1)); + + properties = body.getJSONObject("properties"); + assertThat(properties, is(notNullValue())); + assertThat(properties.length(), is(2)); + assertThat(properties.getString("jcr:primaryType"), is("nt:unstructured")); + assertThat(properties.getString("testProperty"), is("testValue")); + + assertThat(connection.getResponseCode(), is(HttpURLConnection.HTTP_OK)); + connection.disconnect(); + + } + } Index: extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryEntry.java =================================================================== --- extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryEntry.java (revision 0) +++ extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryEntry.java (revision 0) @@ -0,0 +1,32 @@ +package org.jboss.dna.web.jcr.rest.model; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement( name = "repository" ) +public class RepositoryEntry { + + private String name; + private RepositoryResources resources; + + public RepositoryEntry() { + resources = new RepositoryResources(); + } + + public RepositoryEntry( String contextName, + String repositoryName ) { + this.name = repositoryName; + + resources = new RepositoryResources(contextName, repositoryName); + } + + @XmlElement + public String getName() { + return name; + } + + @XmlElement + public RepositoryResources getResources() { + return resources; + } +} Property changes on: extensions\dna-web-jcr-rest\src\test\java\org\jboss\dna\web\jcr\rest\model\RepositoryEntry.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryResources.java =================================================================== --- extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryResources.java (revision 0) +++ extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/RepositoryResources.java (revision 0) @@ -0,0 +1,21 @@ +package org.jboss.dna.web.jcr.rest.model; + +import javax.xml.bind.annotation.XmlElement; + +public class RepositoryResources { + private String baseUri; + + public RepositoryResources() { + } + + public RepositoryResources( String contextName, + String repositoryName ) { + this.baseUri = contextName + "/" + repositoryName; + } + + @XmlElement( name = "workspaces" ) + public String getWorkspaces() { + return baseUri; + } +} + Property changes on: extensions\dna-web-jcr-rest\src\test\java\org\jboss\dna\web\jcr\rest\model\RepositoryResources.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceEntry.java =================================================================== --- extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceEntry.java (revision 0) +++ extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceEntry.java (revision 0) @@ -0,0 +1,34 @@ +package org.jboss.dna.web.jcr.rest.model; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement( name = "workspace" ) +public class WorkspaceEntry { + + private String name; + private WorkspaceResources resources; + + public WorkspaceEntry() { + + } + + public WorkspaceEntry( String contextName, + String repositoryName, + String workspaceName ) { + this.name = workspaceName; + + resources = new WorkspaceResources(contextName, repositoryName, workspaceName); + } + + @XmlElement + public String getName() { + return name; + } + + @XmlElement + public WorkspaceResources getResources() { + return resources; + } +} + Property changes on: extensions\dna-web-jcr-rest\src\test\java\org\jboss\dna\web\jcr\rest\model\WorkspaceEntry.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceResources.java =================================================================== --- extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceResources.java (revision 0) +++ extensions/dna-web-jcr-rest/src/test/java/org/jboss/dna/web/jcr/rest/model/WorkspaceResources.java (revision 0) @@ -0,0 +1,22 @@ +package org.jboss.dna.web.jcr.rest.model; + +import javax.xml.bind.annotation.XmlElement; + +public class WorkspaceResources { + private String baseUri; + + public WorkspaceResources() { + } + + public WorkspaceResources( String contextName, + String repositoryName, + String workspaceName ) { + this.baseUri = contextName + "/" + repositoryName + "/" + workspaceName; + } + + @XmlElement( name = "items" ) + public String getWorkspaces() { + return baseUri + "/items"; + } +} + Property changes on: extensions\dna-web-jcr-rest\src\test\java\org\jboss\dna\web\jcr\rest\model\WorkspaceResources.java ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/test/resources/dna-test-users.props =================================================================== --- extensions/dna-web-jcr-rest/src/test/resources/dna-test-users.props (revision 0) +++ extensions/dna-web-jcr-rest/src/test/resources/dna-test-users.props (revision 0) @@ -0,0 +1 @@ +dnauser=password,readwrite Index: extensions/dna-web-jcr-rest/src/test/resources/jetty-dna.policy =================================================================== --- extensions/dna-web-jcr-rest/src/test/resources/jetty-dna.policy (revision 0) +++ extensions/dna-web-jcr-rest/src/test/resources/jetty-dna.policy (revision 0) @@ -0,0 +1,5 @@ +dna-jcr { + org.mortbay.jetty.plus.jaas.spi.PropertyFileLoginModule optional + debug="true" + file="target/test-classes/dna-test-users.props"; +}; Index: extensions/dna-web-jcr-rest/src/test/resources/jetty-jaas.xml =================================================================== --- extensions/dna-web-jcr-rest/src/test/resources/jetty-jaas.xml (revision 0) +++ extensions/dna-web-jcr-rest/src/test/resources/jetty-jaas.xml (revision 0) @@ -0,0 +1,9 @@ + + + + xyzrealm + dna-jcr + + + + Property changes on: extensions\dna-web-jcr-rest\src\test\resources\jetty-jaas.xml ___________________________________________________________________ Added: svn:keywords + Id Revision Added: svn:eol-style + LF Index: extensions/dna-web-jcr-rest/src/test/resources/log4j.properties =================================================================== --- extensions/dna-web-jcr-rest/src/test/resources/log4j.properties (revision 0) +++ extensions/dna-web-jcr-rest/src/test/resources/log4j.properties (revision 0) @@ -0,0 +1,13 @@ +log4j.rootLogger = DEBUG, stdout + +log4j.category.org.apache=INFO +log4j.category.org.jboss.resteasy=INFO +log4j.category.org.mortbay=DEBUG +log4j.category.org.slf4j.impl.JCLLoggerAdapter=INFO +log4j.category.org.springframework=INFO + +log4j.appender.stdout = org.apache.log4j.ConsoleAppender +log4j.appender.stdout.Threshold = DEBUG +log4j.appender.stdout.Target = System.out +log4j.appender.stdout.layout = org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern = [%-5p] [%C{1}] : %m%n