001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.web.jcr.rest;
025    
026    import java.io.IOException;
027    import java.util.ArrayList;
028    import java.util.Arrays;
029    import java.util.HashMap;
030    import java.util.HashSet;
031    import java.util.Iterator;
032    import java.util.List;
033    import java.util.Map;
034    import java.util.Set;
035    import javax.jcr.Item;
036    import javax.jcr.Node;
037    import javax.jcr.NodeIterator;
038    import javax.jcr.PathNotFoundException;
039    import javax.jcr.Property;
040    import javax.jcr.PropertyIterator;
041    import javax.jcr.RepositoryException;
042    import javax.jcr.Session;
043    import javax.jcr.Value;
044    import javax.jcr.nodetype.NodeType;
045    import javax.jcr.nodetype.PropertyDefinition;
046    import javax.servlet.http.HttpServletRequest;
047    import javax.ws.rs.Consumes;
048    import javax.ws.rs.DELETE;
049    import javax.ws.rs.DefaultValue;
050    import javax.ws.rs.GET;
051    import javax.ws.rs.POST;
052    import javax.ws.rs.PUT;
053    import javax.ws.rs.Path;
054    import javax.ws.rs.PathParam;
055    import javax.ws.rs.Produces;
056    import javax.ws.rs.QueryParam;
057    import javax.ws.rs.core.Context;
058    import javax.ws.rs.core.Response;
059    import javax.ws.rs.core.Response.Status;
060    import javax.ws.rs.ext.ExceptionMapper;
061    import javax.ws.rs.ext.Provider;
062    import net.jcip.annotations.Immutable;
063    import org.codehaus.jettison.json.JSONArray;
064    import org.codehaus.jettison.json.JSONException;
065    import org.codehaus.jettison.json.JSONObject;
066    import org.jboss.dna.common.text.UrlEncoder;
067    import org.jboss.dna.web.jcr.rest.model.RepositoryEntry;
068    import org.jboss.dna.web.jcr.rest.model.WorkspaceEntry;
069    import org.jboss.resteasy.spi.NotFoundException;
070    import org.jboss.resteasy.spi.UnauthorizedException;
071    
072    /**
073     * RESTEasy handler to provide the JCR resources at the URIs below. Please note that these URIs assume a context of {@code
074     * /resources} for the web application.
075     * <table border="1">
076     * <tr>
077     * <th>URI Pattern</th>
078     * <th>Description</th>
079     * <th>Supported Methods</th>
080     * </tr>
081     * <tr>
082     * <td>/resources</td>
083     * <td>returns a list of accessible repositories</td>
084     * <td>GET</td>
085     * </tr>
086     * <tr>
087     * <td>/resources/{repositoryName}</td>
088     * <td>returns a list of accessible workspaces within that repository</td>
089     * <td>GET</td>
090     * </tr>
091     * <tr>
092     * <td>/resources/{repositoryName}/{workspaceName}</td>
093     * <td>returns a list of operations within the workspace</td>
094     * <td>GET</td>
095     * </tr>
096     * <tr>
097     * <td>/resources/{repositoryName}/{workspaceName}/item/{path}</td>
098     * <td>accesses the item (node or property) at the path</td>
099     * <td>ALL</td>
100     * </tr>
101     * </table>
102     */
103    @Immutable
104    @Path( "/" )
105    public class JcrResources {
106    
107        private static final UrlEncoder URL_ENCODER = new UrlEncoder();
108    
109        private static final String PROPERTIES_HOLDER = "properties";
110        private static final String CHILD_NODE_HOLDER = "children";
111    
112        private static final String PRIMARY_TYPE_PROPERTY = "jcr:primaryType";
113        private static final String MIXIN_TYPES_PROPERTY = "jcr:mixinTypes";
114    
115        /** Name to be used when the repository name is empty string as {@code "//"} is not a valid path. */
116        public static final String EMPTY_REPOSITORY_NAME = "<default>";
117        /** Name to be used when the workspace name is empty string as {@code "//"} is not a valid path. */
118        public static final String EMPTY_WORKSPACE_NAME = "<default>";
119    
120        /**
121         * Returns an active session for the given workspace name in the named repository.
122         * 
123         * @param request the servlet request; may not be null or unauthenticated
124         * @param rawRepositoryName the URL-encoded name of the repository in which the session is created
125         * @param rawWorkspaceName the URL-encoded name of the workspace to which the session should be connected
126         * @return an active session with the given workspace in the named repository
127         * @throws RepositoryException if any other error occurs
128         */
129        private Session getSession( HttpServletRequest request,
130                                    String rawRepositoryName,
131                                    String rawWorkspaceName ) throws NotFoundException, RepositoryException {
132            assert request != null;
133            assert request.getUserPrincipal() != null : "Request must be authorized";
134    
135            // Sanity check
136            if (request.getUserPrincipal() == null) {
137                throw new UnauthorizedException("Client is not authorized");
138            }
139    
140            return RepositoryFactory.getSession(request, repositoryNameFor(rawRepositoryName), workspaceNameFor(rawWorkspaceName));
141        }
142    
143        /**
144         * Returns the list of JCR repositories available on this server
145         * 
146         * @param request the servlet request; may not be null
147         * @return the list of JCR repositories available on this server
148         */
149        @GET
150        @Path( "/" )
151        @Produces( "application/json" )
152        public Map<String, RepositoryEntry> getRepositories( @Context HttpServletRequest request ) {
153            assert request != null;
154    
155            Map<String, RepositoryEntry> repositories = new HashMap<String, RepositoryEntry>();
156    
157            for (String name : RepositoryFactory.getJcrRepositoryNames()) {
158                if (name.trim().length() == 0) {
159                    name = EMPTY_REPOSITORY_NAME;
160                }
161                name = URL_ENCODER.encode(name);
162                repositories.put(name, new RepositoryEntry(request.getContextPath(), name));
163            }
164    
165            return repositories;
166        }
167    
168        /**
169         * Returns the list of workspaces available to this user within the named repository.
170         * 
171         * @param rawRepositoryName the name of the repository; may not be null
172         * @param request the servlet request; may not be null
173         * @return the list of workspaces available to this user within the named repository.
174         * @throws IOException if the given repository name does not map to any repositories and there is an error writing the error
175         *         code to the response.
176         * @throws RepositoryException if there is any other error accessing the list of available workspaces for the repository
177         */
178        @GET
179        @Path( "/{repositoryName}" )
180        @Produces( "application/json" )
181        public Map<String, WorkspaceEntry> getWorkspaces( @Context HttpServletRequest request,
182                                                          @PathParam( "repositoryName" ) String rawRepositoryName )
183            throws RepositoryException, IOException {
184    
185            assert request != null;
186            assert rawRepositoryName != null;
187    
188            Map<String, WorkspaceEntry> workspaces = new HashMap<String, WorkspaceEntry>();
189    
190            Session session = getSession(request, rawRepositoryName, null);
191            rawRepositoryName = URL_ENCODER.encode(rawRepositoryName);
192    
193            for (String name : session.getWorkspace().getAccessibleWorkspaceNames()) {
194                if (name.trim().length() == 0) {
195                    name = EMPTY_WORKSPACE_NAME;
196                }
197                name = URL_ENCODER.encode(name);
198                workspaces.put(name, new WorkspaceEntry(request.getContextPath(), rawRepositoryName, name));
199            }
200    
201            return workspaces;
202        }
203    
204        /**
205         * Handles GET requests for an item in a workspace.
206         * 
207         * @param request the servlet request; may not be null or unauthenticated
208         * @param rawRepositoryName the URL-encoded repository name
209         * @param rawWorkspaceName the URL-encoded workspace name
210         * @param path the path to the item
211         * @param depth the depth of the node graph that should be returned if {@code path} refers to a node. @{code 0} means return
212         *        the requested node only. A negative value indicates that the full subgraph under the node should be returned. This
213         *        parameter defaults to {@code 0} and is ignored if {@code path} refers to a property.
214         * @return the JSON-encoded version of the item (and, if the item is a node, its subgraph, depending on the value of {@code
215         *         depth})
216         * @throws NotFoundException if the named repository does not exists, the named workspace does not exist, or the user does not
217         *         have access to the named workspace
218         * @throws JSONException if there is an error encoding the node
219         * @throws UnauthorizedException if the given login information is invalid
220         * @throws RepositoryException if any other error occurs
221         * @see #EMPTY_REPOSITORY_NAME
222         * @see #EMPTY_WORKSPACE_NAME
223         * @see Session#getItem(String)
224         */
225        @GET
226        @Path( "/{repositoryName}/{workspaceName}/items{path:.*}" )
227        @Produces( "application/json" )
228        public String getItem( @Context HttpServletRequest request,
229                               @PathParam( "repositoryName" ) String rawRepositoryName,
230                               @PathParam( "workspaceName" ) String rawWorkspaceName,
231                               @PathParam( "path" ) String path,
232                               @QueryParam( "dna:depth" ) @DefaultValue( "0" ) int depth )
233            throws JSONException, UnauthorizedException, RepositoryException {
234            assert path != null;
235            assert rawRepositoryName != null;
236            assert rawWorkspaceName != null;
237    
238            Session session = getSession(request, rawRepositoryName, rawWorkspaceName);
239            Item item;
240    
241            if ("/".equals(path) || "".equals(path)) {
242                item = session.getRootNode();
243            } else {
244                try {
245                    item = session.getItem(path);
246                } catch (PathNotFoundException pnfe) {
247                    throw new NotFoundException(pnfe.getMessage(), pnfe);
248                }
249            }
250    
251            if (item instanceof Node) {
252                return jsonFor((Node)item, depth).toString();
253            }
254            return jsonFor((Property)item);
255        }
256    
257        /**
258         * Returns the JSON-encoded version of the given property. If the property is single-valued, the returned string is {@code
259         * property.getValue().getString()} encoded as a JSON string. If the property is multi-valued with {@code N} values, this
260         * method returns a JSON array containing {@code property.getValues()[N].getString()} for all values of {@code N}.
261         * 
262         * @param property the property to be encoded
263         * @return the JSON-encoded version of the property
264         * @throws RepositoryException if an error occurs accessing the property, its values, or its definition.
265         * @see Property#getDefinition()
266         * @see PropertyDefinition#isMultiple()
267         */
268        private String jsonFor( Property property ) throws RepositoryException {
269            if (property.getDefinition().isMultiple()) {
270                Value[] values = property.getValues();
271                List<String> list = new ArrayList<String>(values.length);
272                for (int i = 0; i < values.length; i++) {
273                    list.add(values[i].getString());
274                }
275                return new JSONArray(list).toString();
276            }
277            return JSONObject.quote(property.getValue().getString());
278        }
279    
280        /**
281         * Recursively returns the JSON-encoding of a node and its children to depth {@code toDepth}.
282         * 
283         * @param node the node to be encoded
284         * @param toDepth the depth to which the recursion should extend; {@code 0} means no further recursion should occur.
285         * @return the JSON-encoding of a node and its children to depth {@code toDepth}.
286         * @throws JSONException if there is an error encoding the node
287         * @throws RepositoryException if any other error occurs
288         */
289        private JSONObject jsonFor( Node node,
290                                    int toDepth ) throws JSONException, RepositoryException {
291            JSONObject jsonNode = new JSONObject();
292    
293            JSONObject properties = new JSONObject();
294    
295            for (PropertyIterator iter = node.getProperties(); iter.hasNext();) {
296                Property prop = iter.nextProperty();
297                String propName = prop.getName();
298    
299                if (prop.getDefinition().isMultiple()) {
300                    Value[] values = prop.getValues();
301                    JSONArray array = new JSONArray();
302                    for (int i = 0; i < values.length; i++) {
303                        array.put(values[i].getString());
304                    }
305                    properties.put(propName, array);
306    
307                } else {
308                    properties.put(propName, prop.getValue().getString());
309                }
310    
311            }
312            if (properties.length() > 0) {
313                jsonNode.put(PROPERTIES_HOLDER, properties);
314            }
315    
316            if (toDepth == 0) {
317                List<String> children = new ArrayList<String>();
318    
319                for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
320                    Node child = iter.nextNode();
321    
322                    children.add(child.getName());
323                }
324    
325                if (children.size() > 0) {
326                    jsonNode.put(CHILD_NODE_HOLDER, new JSONArray(children));
327                }
328            } else {
329                JSONObject children = new JSONObject();
330    
331                for (NodeIterator iter = node.getNodes(); iter.hasNext();) {
332                    Node child = iter.nextNode();
333    
334                    children.put(child.getName(), jsonFor(child, toDepth - 1));
335                }
336    
337                if (children.length() > 0) {
338                    jsonNode.put(CHILD_NODE_HOLDER, children);
339                }
340            }
341    
342            return jsonNode;
343        }
344    
345        /**
346         * Adds the content of the request as a node (or subtree of nodes) at the location specified by {@code path}.
347         * <p>
348         * The primary type and mixin type(s) may optionally be specified through the {@code jcr:primaryType} and {@code
349         * jcr:mixinTypes} properties.
350         * </p>
351         * 
352         * @param request the servlet request; may not be null or unauthenticated
353         * @param rawRepositoryName the URL-encoded repository name
354         * @param rawWorkspaceName the URL-encoded workspace name
355         * @param path the path to the item
356         * @param requestContent the JSON-encoded representation of the node or nodes to be added
357         * @return the JSON-encoded representation of the node or nodes that were added. This will differ from {@code requestContent}
358         *         in that auto-created and protected properties (e.g., jcr:uuid) will be populated.
359         * @throws NotFoundException if the parent of the item to be added does not exist
360         * @throws UnauthorizedException if the user does not have the access required to create the node at this path
361         * @throws JSONException if there is an error encoding the node
362         * @throws RepositoryException if any other error occurs
363         */
364        @POST
365        @Path( "/{repositoryName}/{workspaceName}/items/{path:.*}" )
366        @Consumes( "application/json" )
367        public Response postItem( @Context HttpServletRequest request,
368                                  @PathParam( "repositoryName" ) String rawRepositoryName,
369                                  @PathParam( "workspaceName" ) String rawWorkspaceName,
370                                  @PathParam( "path" ) String path,
371                                  String requestContent )
372            throws NotFoundException, UnauthorizedException, RepositoryException, JSONException {
373    
374            assert rawRepositoryName != null;
375            assert rawWorkspaceName != null;
376            assert path != null;
377            JSONObject body = new JSONObject(requestContent);
378    
379            int lastSlashInd = path.lastIndexOf('/');
380            String parentPath = lastSlashInd == -1 ? "/" : "/" + path.substring(0, lastSlashInd);
381            String newNodeName = lastSlashInd == -1 ? path : path.substring(lastSlashInd + 1);
382    
383            Session session = getSession(request, rawRepositoryName, rawWorkspaceName);
384    
385            Node parentNode = (Node)session.getItem(parentPath);
386    
387            Node newNode = addNode(parentNode, newNodeName, body);
388    
389            session.save();
390    
391            String json = jsonFor(newNode, -1).toString();
392            return Response.status(Status.CREATED).entity(json).build();
393        }
394    
395        /**
396         * Adds the node described by {@code jsonNode} with name {@code nodeName} to the existing node {@code parentNode}.
397         * 
398         * @param parentNode the parent of the node to be added
399         * @param nodeName the name of the node to be added
400         * @param jsonNode the JSON-encoded representation of the node or nodes to be added.
401         * @return the JSON-encoded representation of the node or nodes that were added. This will differ from {@code requestContent}
402         *         in that auto-created and protected properties (e.g., jcr:uuid) will be populated.
403         * @throws JSONException if there is an error encoding the node
404         * @throws RepositoryException if any other error occurs
405         */
406        private Node addNode( Node parentNode,
407                              String nodeName,
408                              JSONObject jsonNode ) throws RepositoryException, JSONException {
409            Node newNode;
410    
411            JSONObject properties = jsonNode.has(PROPERTIES_HOLDER) ? jsonNode.getJSONObject(PROPERTIES_HOLDER) : new JSONObject();
412    
413            if (properties.has(PRIMARY_TYPE_PROPERTY)) {
414                String primaryType = properties.getString(PRIMARY_TYPE_PROPERTY);
415                newNode = parentNode.addNode(nodeName, primaryType);
416            } else {
417                newNode = parentNode.addNode(nodeName);
418            }
419    
420            if (properties.has(MIXIN_TYPES_PROPERTY)) {
421                Object rawMixinTypes = properties.get(MIXIN_TYPES_PROPERTY);
422    
423                if (rawMixinTypes instanceof JSONArray) {
424                    JSONArray mixinTypes = (JSONArray)rawMixinTypes;
425                    for (int i = 0; i < mixinTypes.length(); i++) {
426                        newNode.addMixin(mixinTypes.getString(i));
427                    }
428    
429                } else {
430                    newNode.addMixin(rawMixinTypes.toString());
431    
432                }
433            }
434    
435            for (Iterator<?> iter = properties.keys(); iter.hasNext();) {
436                String key = (String)iter.next();
437    
438                if (PRIMARY_TYPE_PROPERTY.equals(key)) continue;
439                if (MIXIN_TYPES_PROPERTY.equals(key)) continue;
440                setPropertyOnNode(newNode, key, properties.get(key));
441            }
442    
443            if (jsonNode.has(CHILD_NODE_HOLDER)) {
444                JSONObject children = jsonNode.getJSONObject(CHILD_NODE_HOLDER);
445    
446                for (Iterator<?> iter = children.keys(); iter.hasNext();) {
447                    String childName = (String)iter.next();
448                    JSONObject child = children.getJSONObject(childName);
449    
450                    addNode(newNode, childName, child);
451                }
452            }
453    
454            return newNode;
455        }
456    
457        /**
458         * Sets the named property on the given node. This method expects {@code value} to be either a JSON string or a JSON array of
459         * JSON strings. If {@code value} is a JSON array, {@code Node#setProperty(String, String[]) the multi-valued property setter}
460         * will be used.
461         * 
462         * @param node the node on which the property is to be set
463         * @param propName the name of the property to set
464         * @param value the JSON-encoded values to be set
465         * @throws RepositoryException if there is an error setting the property
466         * @throws JSONException if {@code value} cannot be decoded
467         */
468        private void setPropertyOnNode( Node node,
469                                        String propName,
470                                        Object value ) throws RepositoryException, JSONException {
471            String[] values;
472            if (value instanceof JSONArray) {
473                JSONArray jsonValues = (JSONArray)value;
474                values = new String[jsonValues.length()];
475    
476                for (int i = 0; i < values.length; i++) {
477                    values[i] = jsonValues.getString(i);
478                }
479            } else {
480                values = new String[] { (String)value };
481            }
482    
483            if (propName.equals(JcrResources.MIXIN_TYPES_PROPERTY)) {
484                Set<String> toBeMixins = new HashSet<String>(Arrays.asList(values));
485                Set<String> asIsMixins = new HashSet<String>();
486                
487                for (NodeType nodeType : node.getMixinNodeTypes()) {
488                    asIsMixins.add(nodeType.getName());
489                }
490                
491                Set<String> mixinsToAdd = new HashSet<String>(toBeMixins);
492                mixinsToAdd.removeAll(asIsMixins);
493                asIsMixins.removeAll(toBeMixins);
494                
495                for (String nodeType : mixinsToAdd) {
496                    node.addMixin(nodeType);
497                }
498    
499                for (String nodeType : asIsMixins) {
500                    node.removeMixin(nodeType);
501                }
502            } else {
503                if (values.length == 1) {
504                    node.setProperty(propName, values[0]);
505                    
506                }
507                else {
508                    node.setProperty(propName, values);
509                }
510            }
511        }
512    
513        /**
514         * Deletes the item at {@code path}.
515         * 
516         * @param request the servlet request; may not be null or unauthenticated
517         * @param rawRepositoryName the URL-encoded repository name
518         * @param rawWorkspaceName the URL-encoded workspace name
519         * @param path the path to the item
520         * @throws NotFoundException if no item exists at {@code path}
521         * @throws UnauthorizedException if the user does not have the access required to delete the item at this path
522         * @throws RepositoryException if any other error occurs
523         */
524        @DELETE
525        @Path( "/{repositoryName}/{workspaceName}/items{path:.*}" )
526        @Consumes( "application/json" )
527        public void deleteItem( @Context HttpServletRequest request,
528                                @PathParam( "repositoryName" ) String rawRepositoryName,
529                                @PathParam( "workspaceName" ) String rawWorkspaceName,
530                                @PathParam( "path" ) String path )
531            throws NotFoundException, UnauthorizedException, RepositoryException {
532    
533            assert rawRepositoryName != null;
534            assert rawWorkspaceName != null;
535            assert path != null;
536    
537            Session session = getSession(request, rawRepositoryName, rawWorkspaceName);
538    
539            Item item;
540            try {
541                item = session.getItem(path);
542            } catch (PathNotFoundException pnfe) {
543                throw new NotFoundException(pnfe.getMessage(), pnfe);
544            }
545            item.remove();
546            session.save();
547        }
548    
549        /**
550         * Updates the properties at the path.
551         * <p>
552         * If path points to a property, this method expects the request content to be either a JSON array or a JSON string. The array
553         * or string will become the values or value of the property. If path points to a node, this method expects the request
554         * content to be a JSON object. The keys of the objects correspond to property names that will be set and the values for the
555         * keys correspond to the values that will be set on the properties.
556         * </p>
557         * 
558         * @param request the servlet request; may not be null or unauthenticated
559         * @param rawRepositoryName the URL-encoded repository name
560         * @param rawWorkspaceName the URL-encoded workspace name
561         * @param path the path to the item
562         * @param requestContent the JSON-encoded representation of the values and, possibly, properties to be set
563         * @return the JSON-encoded representation of the node on which the property or properties were set.
564         * @throws NotFoundException if the parent of the item to be added does not exist
565         * @throws UnauthorizedException if the user does not have the access required to create the node at this path
566         * @throws JSONException if there is an error encoding the node
567         * @throws RepositoryException if any other error occurs
568         */
569        @PUT
570        @Path( "/{repositoryName}/{workspaceName}/items{path:.*}" )
571        @Consumes( "application/json" )
572        public String putItem( @Context HttpServletRequest request,
573                               @PathParam( "repositoryName" ) String rawRepositoryName,
574                               @PathParam( "workspaceName" ) String rawWorkspaceName,
575                               @PathParam( "path" ) String path,
576                               String requestContent ) throws UnauthorizedException, JSONException, RepositoryException {
577    
578            assert path != null;
579            assert rawRepositoryName != null;
580            assert rawWorkspaceName != null;
581    
582            Session session = getSession(request, rawRepositoryName, rawWorkspaceName);
583            Node node;
584            Item item;
585            if ("".equals(path) || "/".equals(path)) {
586                item = session.getRootNode();
587            } else {
588                try {
589                    item = session.getItem(path);
590                } catch (PathNotFoundException pnfe) {
591                    throw new NotFoundException(pnfe.getMessage(), pnfe);
592                }
593            }
594    
595            if (item instanceof Node) {
596                JSONObject properties = new JSONObject(requestContent);
597                node = (Node)item;
598    
599                for (Iterator<?> iter = properties.keys(); iter.hasNext();) {
600                    String key = (String)iter.next();
601    
602                    setPropertyOnNode(node, key, properties.get(key));
603                }
604    
605            } else {
606                /*
607                 * The incoming content should be a JSON string or a JSON array. Wrap it into an object so it can be parsed more easily
608                 */
609    
610                JSONObject properties = new JSONObject("{ \"value\": " + requestContent + "}");
611                Property property = (Property)item;
612                node = property.getParent();
613    
614                setPropertyOnNode(node, property.getName(), properties.get("value"));
615            }
616            node.save();
617            return jsonFor(node, 0).toString();
618        }
619    
620        private String workspaceNameFor( String rawWorkspaceName ) {
621            String workspaceName = URL_ENCODER.decode(rawWorkspaceName);
622    
623            if (EMPTY_WORKSPACE_NAME.equals(workspaceName)) {
624                workspaceName = "";
625            }
626    
627            return workspaceName;
628        }
629    
630        private String repositoryNameFor( String rawRepositoryName ) {
631            String repositoryName = URL_ENCODER.decode(rawRepositoryName);
632    
633            if (EMPTY_REPOSITORY_NAME.equals(repositoryName)) {
634                repositoryName = "";
635            }
636    
637            return repositoryName;
638        }
639    
640        @Provider
641        public static class NotFoundExceptionMapper implements ExceptionMapper<NotFoundException> {
642    
643            public Response toResponse( NotFoundException exception ) {
644                return Response.status(Status.NOT_FOUND).entity(exception.getMessage()).build();
645            }
646    
647        }
648    
649        @Provider
650        public static class JSONExceptionMapper implements ExceptionMapper<JSONException> {
651    
652            public Response toResponse( JSONException exception ) {
653                return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
654            }
655    
656        }
657    
658        @Provider
659        public static class RepositoryExceptionMapper implements ExceptionMapper<RepositoryException> {
660    
661            public Response toResponse( RepositoryException exception ) {
662                /*
663                 * This error code is murky - the request must have been syntactically valid to get to
664                 * the JCR operations, but there isn't an HTTP status code for "semantically invalid." 
665                 */
666                return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
667            }
668    
669        }
670    
671    }