Add support for RESTCONF standard errors

Change-Id: I74c0997bc8e06bc10c97cd610ff70c7a6aa68c8b
diff --git a/protocols/restconf/server/rpp/BUCK b/protocols/restconf/server/rpp/BUCK
index 8f7bb52..faeeb6f 100644
--- a/protocols/restconf/server/rpp/BUCK
+++ b/protocols/restconf/server/rpp/BUCK
@@ -9,7 +9,14 @@
     '//apps/restconf/api:onos-apps-restconf-api',
 ]
 
+TEST_DEPS = [
+    '//lib:TEST_REST',
+    '//utils/osgi:onlab-osgi-tests',
+    '//web/api:onos-rest-tests',
+]
+
 osgi_jar_with_tests (
     deps = COMPILE_DEPS,
+    test_deps = TEST_DEPS,
     web_context = '/onos/restconf',
 )
diff --git a/protocols/restconf/server/rpp/src/main/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResource.java b/protocols/restconf/server/rpp/src/main/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResource.java
index ba11421..36eb3a2 100644
--- a/protocols/restconf/server/rpp/src/main/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResource.java
+++ b/protocols/restconf/server/rpp/src/main/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResource.java
@@ -20,6 +20,7 @@
 import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.glassfish.jersey.server.ChunkedOutput;
 import org.onosproject.rest.AbstractWebResource;
+import org.onosproject.restconf.api.RestconfError;
 import org.onosproject.restconf.api.RestconfException;
 import org.onosproject.restconf.api.RestconfRpcOutput;
 import org.onosproject.restconf.api.RestconfService;
@@ -42,6 +43,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.util.Arrays;
 import java.util.concurrent.CompletableFuture;
 
 import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
@@ -68,22 +70,19 @@
     @Context
     UriInfo uriInfo;
 
-    private static final String NOT_EXIST = "Requested data resource does not exist";
-
     private final RestconfService service = get(RestconfService.class);
     private final Logger log = getLogger(getClass());
 
     /**
      * Handles a RESTCONF GET operation against a target data resource. If the
      * operation is successful, the JSON presentation of the resource plus HTTP
-     * status code "200 OK" is returned. Otherwise, HTTP error status code
-     * "400 Bad Request" is returned.
+     * status code "200 OK" is returned. If it is not found then "404 Not Found"
+     * is returned. On internal error "500 Internal Server Error" is returned.
      *
      * @param uriString URI of the data resource.
-     * @return HTTP response
+     * @return HTTP response - 200, 404 or 500
      */
     @GET
-    @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("data/{identifier : .+}")
     public Response handleGetRequest(@PathParam("identifier") String uriString) {
@@ -94,13 +93,27 @@
         try {
             ObjectNode node = service.runGetOperationOnDataResource(uri);
             if (node == null) {
-                return Response.status(NOT_FOUND).entity(NOT_EXIST).build();
+                RestconfError error =
+                        RestconfError.builder(RestconfError.ErrorType.PROTOCOL,
+                                RestconfError.ErrorTag.INVALID_VALUE)
+                        .errorMessage("Resource not found")
+                        .errorPath(uriString)
+                        .errorAppTag("handleGetRequest")
+                        .build();
+                return Response.status(NOT_FOUND)
+                        .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
             }
             return ok(node).build();
         } catch (RestconfException e) {
             log.error("ERROR: handleGetRequest: {}", e.getMessage());
             log.debug("Exception in handleGetRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus()).entity(e.toRestconfErrorJson()).build();
+        } catch (Exception e) {
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED)
+                    .errorMessage(e.getMessage()).errorAppTag("handlePostRequest").build();
+            return Response.status(INTERNAL_SERVER_ERROR)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         }
     }
 
@@ -186,14 +199,23 @@
             return Response.created(uriInfo.getRequestUri()).build();
         } catch (JsonProcessingException e) {
             log.error("ERROR: handlePostRequest ", e);
-            return Response.status(BAD_REQUEST).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.MALFORMED_MESSAGE)
+                    .errorMessage(e.getMessage()).errorAppTag("handlePostRequest").build();
+            return Response.status(BAD_REQUEST)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         } catch (RestconfException e) {
             log.error("ERROR: handlePostRequest: {}", e.getMessage());
             log.debug("Exception in handlePostRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus())
+                    .entity(e.toRestconfErrorJson()).build();
         } catch (IOException ex) {
             log.error("ERROR: handlePostRequest ", ex);
-            return Response.status(INTERNAL_SERVER_ERROR).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED)
+                    .errorMessage(ex.getMessage()).errorAppTag("handlePostRequest").build();
+            return Response.status(INTERNAL_SERVER_ERROR)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         }
     }
 
@@ -230,14 +252,23 @@
             return ok(node).build();
         } catch (JsonProcessingException e) {
             log.error("ERROR:  handleRpcRequest", e);
-            return Response.status(BAD_REQUEST).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.MALFORMED_MESSAGE)
+                    .errorMessage(e.getMessage()).errorAppTag("handleRpcRequest").build();
+            return Response.status(BAD_REQUEST)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         } catch (RestconfException e) {
             log.error("ERROR: handleRpcRequest: {}", e.getMessage());
             log.debug("Exception in handleRpcRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus())
+                    .entity(e.toRestconfErrorJson()).build();
         } catch (Exception e) {
             log.error("ERROR: handleRpcRequest ", e);
-            return Response.status(INTERNAL_SERVER_ERROR).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED)
+                    .errorMessage(e.getMessage()).errorAppTag("handleRpcRequest").build();
+            return Response.status(INTERNAL_SERVER_ERROR)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         }
     }
 
@@ -272,14 +303,23 @@
             return Response.created(uriInfo.getRequestUri()).build();
         } catch (JsonProcessingException e) {
             log.error("ERROR: handlePutRequest ", e);
-            return Response.status(BAD_REQUEST).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.MALFORMED_MESSAGE)
+                    .errorMessage(e.getMessage()).errorAppTag("handlePutRequest").build();
+            return Response.status(BAD_REQUEST)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         } catch (RestconfException e) {
             log.error("ERROR: handlePutRequest: {}", e.getMessage());
             log.debug("Exception in handlePutRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus())
+                    .entity(e.toRestconfErrorJson()).build();
         } catch (IOException ex) {
             log.error("ERROR: handlePutRequest ", ex);
-            return Response.status(INTERNAL_SERVER_ERROR).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED)
+                    .errorMessage(ex.getMessage()).errorAppTag("handlePutRequest").build();
+            return Response.status(INTERNAL_SERVER_ERROR)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         }
     }
 
@@ -294,7 +334,6 @@
      * @return HTTP response
      */
     @DELETE
-    @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     @Path("data/{identifier : .+}")
     public Response handleDeleteRequest(@PathParam("identifier") String uriString) {
@@ -308,7 +347,8 @@
         } catch (RestconfException e) {
             log.error("ERROR: handleDeleteRequest: {}", e.getMessage());
             log.debug("Exception in handleDeleteRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus())
+                    .entity(e.toRestconfErrorJson()).build();
         }
     }
 
@@ -340,14 +380,23 @@
             return Response.ok().build();
         } catch (JsonProcessingException e) {
             log.error("ERROR: handlePatchRequest ", e);
-            return Response.status(BAD_REQUEST).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.MALFORMED_MESSAGE)
+                    .errorMessage(e.getMessage()).errorAppTag("handlePatchRequest").build();
+            return Response.status(BAD_REQUEST)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         } catch (RestconfException e) {
             log.error("ERROR: handlePatchRequest: {}", e.getMessage());
             log.debug("Exception in handlePatchRequest:", e);
-            return e.getResponse();
+            return Response.status(e.getResponse().getStatus())
+                    .entity(e.toRestconfErrorJson()).build();
         } catch (IOException ex) {
             log.error("ERROR: handlePatchRequest ", ex);
-            return Response.status(INTERNAL_SERVER_ERROR).build();
+            RestconfError error = RestconfError
+                    .builder(RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED)
+                    .errorMessage(ex.getMessage()).errorAppTag("handlePatchRequest").build();
+            return Response.status(INTERNAL_SERVER_ERROR)
+                    .entity(RestconfError.wrapErrorAsJson(Arrays.asList(error))).build();
         }
     }
 
diff --git a/protocols/restconf/server/rpp/src/test/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResourceTest.java b/protocols/restconf/server/rpp/src/test/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResourceTest.java
new file mode 100644
index 0000000..603bbf6
--- /dev/null
+++ b/protocols/restconf/server/rpp/src/test/java/org/onosproject/protocol/restconf/server/rpp/RestconfWebResourceTest.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright 2017-present Open Networking Foundation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.onosproject.protocol.restconf.server.rpp;
+
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.easymock.EasyMock;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.junit.Before;
+import org.junit.Test;
+import org.onlab.osgi.ServiceDirectory;
+import org.onlab.osgi.TestServiceDirectory;
+import org.onosproject.rest.resources.ResourceTest;
+import org.onosproject.restconf.api.RestconfError;
+import org.onosproject.restconf.api.RestconfException;
+import org.onosproject.restconf.api.RestconfService;
+
+import javax.ws.rs.InternalServerErrorException;
+import javax.ws.rs.NotFoundException;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Response;
+
+import java.io.ByteArrayInputStream;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static javax.ws.rs.core.Response.Status.CONFLICT;
+import static javax.ws.rs.core.Response.Status.INTERNAL_SERVER_ERROR;
+import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.fail;
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * Test the RestconfWebResource.
+ */
+public class RestconfWebResourceTest extends ResourceTest {
+
+    public static final String DATA_IETF_SYSTEM_SYSTEM = "data/ietf-system:system";
+
+    private static final Pattern RESTCONF_ERROR_REGEXP =
+            Pattern.compile("(\\{\"ietf-restconf:errors\":\\[)\\R?"
+                    + "((\\{\"error\":)\\R?"
+                    + "(\\{\"error-type\":\")((protocol)|(transport)|(rpc)|(application))(\",)\\R?"
+                    + "(\"error-tag\":\")[a-z\\-]*(\",)\\R?"
+                    + "((\"error-app-tag\":\").*(\",))?\\R?"
+                    + "((\"error-path\":\").*(\",))?\\R?"
+                    + "((\"error-message\":\").*(\"))?(\\}\\},?))*(\\]\\})", Pattern.DOTALL);
+
+    private RestconfService restconfService = createMock(RestconfService.class);
+
+    public RestconfWebResourceTest() {
+        super(ResourceConfig.forApplicationClass(RestconfProtocolProxy.class));
+    }
+
+    @Before
+    public void setup() {
+        ServiceDirectory testDirectory = new TestServiceDirectory()
+                .add(RestconfService.class, restconfService);
+        setServiceDirectory(testDirectory);
+    }
+
+    /**
+     * Test handleGetRequest when an Json object is returned.
+     */
+    @Test
+    public void testHandleGetRequest() {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode node = mapper.createObjectNode();
+        expect(restconfService
+                .runGetOperationOnDataResource(URI.create(getBaseUri() + DATA_IETF_SYSTEM_SYSTEM)))
+                .andReturn(node).anyTimes();
+        replay(restconfService);
+
+        WebTarget wt = target();
+        String response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM).request().get(String.class);
+        assertNotNull(response);
+    }
+
+    /**
+     * Test handleGetRequest when nothing is returned.
+     */
+    @Test
+    public void testHandleGetRequestNotFound() {
+        expect(restconfService
+                .runGetOperationOnDataResource(URI.create(getBaseUri() + DATA_IETF_SYSTEM_SYSTEM)))
+                .andReturn(null).anyTimes();
+        replay(restconfService);
+
+        WebTarget wt = target();
+        try {
+            String response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM).request().get(String.class);
+            fail("Expecting fail as response is none");
+        } catch (NotFoundException e) {
+            assertNotNull(e.getResponse());
+            assertRestconfErrorJson(e.getResponse());
+        }
+    }
+
+    /**
+     * Test handleGetRequest when an RestconfException is thrown.
+     */
+    @Test
+    public void testHandleGetRequestRestconfException() {
+        expect(restconfService
+                .runGetOperationOnDataResource(URI.create(getBaseUri() + DATA_IETF_SYSTEM_SYSTEM)))
+                .andThrow(new RestconfException("Suitable error message",
+                        RestconfError.ErrorTag.OPERATION_FAILED, INTERNAL_SERVER_ERROR,
+                        Optional.of("/" + DATA_IETF_SYSTEM_SYSTEM),
+                        Optional.of("More info about the error")))
+                .anyTimes();
+        replay(restconfService);
+
+        WebTarget wt = target();
+        try {
+            String response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM).request().get(String.class);
+            fail("Expecting fail as response is RestconfException");
+        } catch (InternalServerErrorException e) {
+            assertNotNull(e.getResponse());
+            assertRestconfErrorJson(e.getResponse());
+        }
+    }
+
+    /**
+     * Test handleGetRequest when an Exception is thrown.
+     */
+    @Test
+    public void testHandleGetRequestIoException() {
+        expect(restconfService
+                .runGetOperationOnDataResource(URI.create(getBaseUri() + DATA_IETF_SYSTEM_SYSTEM)))
+                .andThrow(new IllegalArgumentException("A test exception"))
+                .anyTimes();
+        replay(restconfService);
+
+        WebTarget wt = target();
+        try {
+            String response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM).request().get(String.class);
+            fail("Expecting fail as response is IllegalArgumentException");
+        } catch (InternalServerErrorException e) {
+            assertNotNull(e.getResponse());
+            assertRestconfErrorJson(e.getResponse());
+        }
+    }
+
+    /**
+     * Test handlePostRequest with no exception.
+     */
+    @Test
+    public void testHandlePostRequest() {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode ietfSystemSubNode = mapper.createObjectNode();
+        ietfSystemSubNode.put("contact", "Open Networking Foundation");
+        ietfSystemSubNode.put("hostname", "host1");
+        ietfSystemSubNode.put("location", "The moon");
+
+        ObjectNode ietfSystemNode = mapper.createObjectNode();
+        ietfSystemNode.put("ietf-system:system", ietfSystemSubNode);
+
+        WebTarget wt = target();
+        Response response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM)
+                .request()
+                .post(Entity.json(ietfSystemNode.toString()));
+        assertEquals(201, response.getStatus());
+    }
+
+    /**
+     * Test handlePostRequest with 'already exists' exception.
+     */
+    @Test
+    public void testHandlePostRequestAlreadyExists() {
+        ObjectMapper mapper = new ObjectMapper();
+        ObjectNode ietfSystemSubNode = mapper.createObjectNode();
+        ietfSystemSubNode.put("contact", "Open Networking Foundation");
+        ietfSystemSubNode.put("hostname", "host1");
+        ietfSystemSubNode.put("location", "The moon");
+
+        ObjectNode ietfSystemNode = mapper.createObjectNode();
+        ietfSystemNode.put("ietf-system:system", ietfSystemSubNode);
+
+        restconfService.runPostOperationOnDataResource(
+                EasyMock.<URI>anyObject(), EasyMock.<ObjectNode>anyObject());
+        expectLastCall().andThrow(new RestconfException("Requested node already present", null,
+                RestconfError.ErrorTag.DATA_EXISTS, CONFLICT,
+                Optional.of("/" + DATA_IETF_SYSTEM_SYSTEM)));
+        replay(restconfService);
+
+        WebTarget wt = target();
+        Response response = wt.path("/" + DATA_IETF_SYSTEM_SYSTEM)
+                .request()
+                .post(Entity.json(ietfSystemNode.toString()));
+        assertEquals(409, response.getStatus());
+    }
+
+    private static void assertRestconfErrorJson(Response errorResponse) {
+        ByteArrayInputStream in = (ByteArrayInputStream) errorResponse.getEntity();
+        int n = in.available();
+        byte[] bytes = new byte[n];
+        in.read(bytes, 0, n);
+
+        Matcher m = RESTCONF_ERROR_REGEXP.matcher(new String(bytes, StandardCharsets.UTF_8));
+        assertTrue(m.matches());
+    }
+}