Bug 1010: Implement restconf error responses 25/7125/11
authortpantelis <tpanteli@brocade.com>
Thu, 8 May 2014 18:25:02 +0000 (14:25 -0400)
committerTony Tkacik <ttkacik@cisco.com>
Tue, 27 May 2014 14:57:24 +0000 (16:57 +0200)
- RestconfError (new): encapsulates error information as defined in the
  RESTCON RFC.

- RestconfDocumentedException (new): exception that wraps RestconError info.

- RestconfDocumentedExceptionMapper (new): JAX-RS ExceptionMapper that
  translates a RestconfDocumentedException appropriately to XML or
  JSON depending on the user's mime type.

- JsonMapper: modified to handle null child schema as restconf error-info
  is defined as 'anyxml' and thus no schema may be present.

- ControllerContext: added getRestconfModuleErrorsSchemaNode method to
  obtain the 'errors' container schema from the ietf-restconf module.

- Removed ResponseException and changed occurrences in various classes that
  threw ResponseException to throw RestconfDocumentedException instead.

- Added unit tests for new classes and modified existing ones
  accordingly.

Change-Id: Idbe6f6cae9b40ba14701ee05dfadfdd51e961c6b
Signed-off-by: tpantelis <tpanteli@brocade.com>
30 files changed:
opendaylight/md-sal/sal-rest-connector/pom.xml
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/api/Draft02.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonMapper.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/JsonToCompositeNodeProvider.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfApplication.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToJsonProvider.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/StructuredDataToXmlProvider.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/XmlToCompositeNodeProvider.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/BrokerFacade.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ControllerContext.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java [deleted file]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfImpl.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/AbstractRpcExecutor.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/BrokerRpcExecutor.java
opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/rpc/impl/MountPointRpcExecutor.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/json/to/cnsn/test/JsonToCnSnTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/BrokerFacadeTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/CodecsExceptionsCatchingTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/InvokeRpcMethodTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/NormalizeNodeTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetAugmentedElementWhenEqualNamesTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestGetOperationTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestPostOperationTest.java
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java [new file with mode: 0644]
opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/URITest.java
opendaylight/md-sal/samples/toaster-provider/src/main/java/org/opendaylight/controller/sample/toaster/provider/OpendaylightToaster.java

index e4c7c0c..c2d245b 100644 (file)
       <artifactId>mockito-all</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.opendaylight.controller</groupId>
+      <artifactId>sal-common-util</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
 
   <build>
index d0eaa36..af763cc 100644 (file)
@@ -7,18 +7,67 @@
  */
 package org.opendaylight.controller.sal.rest.api;
 
+import org.opendaylight.yangtools.yang.common.QName;
+
 public class Draft02 {
-    public static class MediaTypes {
-        public static final String API = "application/yang.api";
-        public static final String DATASTORE = "application/yang.datastore";
-        public static final String DATA = "application/yang.data";
-        public static final String OPERATION = "application/yang.operation";
-        public static final String PATCH = "application/yang.patch";
-        public static final String PATCH_STATUS = "application/yang.patch-status";
-        public static final String STREAM = "application/yang.stream";
+    public static interface MediaTypes {
+        String API = "application/yang.api";
+        String DATASTORE = "application/yang.datastore";
+        String DATA = "application/yang.data";
+        String OPERATION = "application/yang.operation";
+        String PATCH = "application/yang.patch";
+        String PATCH_STATUS = "application/yang.patch-status";
+        String STREAM = "application/yang.stream";
+    }
+
+    public static interface RestConfModule {
+        String REVISION = "2013-10-19";
+
+        String NAME = "ietf-restconf";
+
+        String NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-restconf";
+
+        String RESTCONF_GROUPING_SCHEMA_NODE = "restconf";
+
+        String RESTCONF_CONTAINER_SCHEMA_NODE = "restconf";
+
+        String MODULES_CONTAINER_SCHEMA_NODE = "modules";
+
+        String MODULE_LIST_SCHEMA_NODE = "module";
+
+        String STREAMS_CONTAINER_SCHEMA_NODE = "streams";
+
+        String STREAM_LIST_SCHEMA_NODE = "stream";
+
+        String OPERATIONS_CONTAINER_SCHEMA_NODE = "operations";
+
+        String ERRORS_GROUPING_SCHEMA_NODE = "errors";
+
+        String ERRORS_CONTAINER_SCHEMA_NODE = "errors";
+
+        String ERROR_LIST_SCHEMA_NODE = "error";
+
+        QName IETF_RESTCONF_QNAME = QName.create( Draft02.RestConfModule.NAMESPACE,
+                                                  Draft02.RestConfModule.REVISION,
+                                                  Draft02.RestConfModule.NAME );
+
+        QName ERRORS_CONTAINER_QNAME = QName.create( IETF_RESTCONF_QNAME, ERRORS_CONTAINER_SCHEMA_NODE );
+
+        QName ERROR_LIST_QNAME = QName.create( IETF_RESTCONF_QNAME, ERROR_LIST_SCHEMA_NODE );
+
+        QName ERROR_TYPE_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-type" );
+
+        QName ERROR_TAG_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-tag" );
+
+        QName ERROR_APP_TAG_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-app-tag" );
+
+        QName ERROR_MESSAGE_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-message" );
+
+        QName ERROR_INFO_QNAME = QName.create( IETF_RESTCONF_QNAME, "error-info" );
     }
-    
-    public static class Paths {
-        
+
+
+    public static interface Paths {
+
     }
 }
index ea0f149..1e5bfbd 100644 (file)
@@ -11,6 +11,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
 
 import java.io.IOException;
 import java.net.URI;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -84,17 +85,22 @@ class JsonMapper {
     private void writeChildrenOfParent(final JsonWriter writer, final CompositeNode parent, final DataNodeContainer parentSchema)
             throws IOException {
         checkNotNull(parent);
-        checkNotNull(parentSchema);
+
+        Set<DataSchemaNode> parentSchemaChildNodes = parentSchema == null ?
+                                   Collections.<DataSchemaNode>emptySet() : parentSchema.getChildNodes();
+
 
         for (Node<?> child : parent.getValue()) {
-            DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchema.getChildNodes());
+            DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchemaChildNodes);
 
             if (childSchema == null) {
-                throw new UnsupportedDataTypeException("Probably the data node \"" + child.getNodeType().getLocalName()
-                        + "\" is not conform to schema");
-            }
+                // Node may not conform to schema or allows "anyxml" - we'll process it.
 
-            if (childSchema instanceof ContainerSchemaNode) {
+                logger.debug( "No schema found for data node \"" + child.getNodeType() );
+
+                handleNoSchemaFound( writer, child, parent );
+            }
+            else if (childSchema instanceof ContainerSchemaNode) {
                 Preconditions.checkState(child instanceof CompositeNode,
                         "Data representation of Container should be CompositeNode - " + child.getNodeType());
                 writeContainer(writer, (CompositeNode) child, (ContainerSchemaNode) childSchema);
@@ -123,7 +129,7 @@ class JsonMapper {
         }
 
         for (Node<?> child : parent.getValue()) {
-            DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchema.getChildNodes());
+            DataSchemaNode childSchema = findFirstSchemaForNode(child, parentSchemaChildNodes);
             if (childSchema instanceof LeafListSchemaNode) {
                 foundLeafLists.remove(childSchema);
             } else if (childSchema instanceof ListSchemaNode) {
@@ -132,6 +138,22 @@ class JsonMapper {
         }
     }
 
+    private void handleNoSchemaFound( final JsonWriter writer, final Node<?> node,
+                                      final CompositeNode parent ) throws IOException {
+        if( node instanceof SimpleNode<?> ) {
+            writeName( node, null, writer );
+            Object value = node.getValue();
+            if( value != null ) {
+                writer.value( String.valueOf( value ) );
+            }
+        } else { // CompositeNode
+            Preconditions.checkState( node instanceof CompositeNode,
+                    "Data representation of Container should be CompositeNode - " + node.getNodeType() );
+
+            writeContainer( writer, (CompositeNode) node, null );
+        }
+    }
+
     private DataSchemaNode findFirstSchemaForNode(final Node<?> node, final Set<DataSchemaNode> dataSchemaNode) {
         for (DataSchemaNode dsn : dataSchemaNode) {
             if (node.getNodeType().equals(dsn.getQName())) {
@@ -301,7 +323,7 @@ class JsonMapper {
 
     private void writeName(final Node<?> node, final DataSchemaNode schema, final JsonWriter writer) throws IOException {
         String nameForOutput = node.getNodeType().getLocalName();
-        if (schema.isAugmenting()) {
+        if ( schema != null && schema.isAugmenting()) {
             ControllerContext contContext = ControllerContext.getInstance();
             CharSequence moduleName = null;
             if (mountPoint == null) {
index 0d73485..856e09f 100644 (file)
@@ -16,13 +16,17 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.MessageBodyReader;
 import javax.ws.rs.ext.Provider;
 
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Provider
 @Consumes({ Draft02.MediaTypes.DATA + RestconfService.JSON, Draft02.MediaTypes.OPERATION + RestconfService.JSON,
@@ -30,6 +34,8 @@ import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 public enum JsonToCompositeNodeProvider implements MessageBodyReader<CompositeNode> {
     INSTANCE;
 
+    private final static Logger LOG = LoggerFactory.getLogger( JsonToCompositeNodeProvider.class );
+
     @Override
     public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
         return true;
@@ -42,9 +48,11 @@ public enum JsonToCompositeNodeProvider implements MessageBodyReader<CompositeNo
         JsonReader jsonReader = new JsonReader();
         try {
             return jsonReader.read(entityStream);
-        } catch (UnsupportedFormatException e) {
-            throw new WebApplicationException(e, Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage())
-                    .build());
+        } catch (Exception e) {
+            LOG.debug( "Error parsing json input", e );
+            throw new RestconfDocumentedException(
+                            "Error parsing input: " + e.getMessage(),
+                            ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE );
         }
     }
 
index 6166a08..a5fd7bd 100644 (file)
@@ -16,8 +16,15 @@ import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
 import org.opendaylight.controller.sal.restconf.impl.RestconfImpl;
 
+import com.google.common.collect.ImmutableSet;
+
 public class RestconfApplication extends Application {
 
+    @Override
+    public Set<Class<?>> getClasses() {
+        return ImmutableSet.<Class<?>>of( RestconfDocumentedExceptionMapper.class );
+    }
+
     @Override
     public Set<Object> getSingletons() {
         Set<Object> singletons = new HashSet<>();
@@ -36,4 +43,5 @@ public class RestconfApplication extends Application {
         return singletons;
     }
 
+
 }
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/rest/impl/RestconfDocumentedExceptionMapper.java
new file mode 100644 (file)
index 0000000..456354b
--- /dev/null
@@ -0,0 +1,273 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.sal.rest.impl;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.StringReader;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+import java.util.Map.Entry;
+
+import javax.activation.UnsupportedDataTypeException;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+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 javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+
+import static org.opendaylight.controller.sal.rest.api.Draft02.RestConfModule.*;
+
+import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.CompositeNode;
+import org.opendaylight.yangtools.yang.data.api.Node;
+import org.opendaylight.yangtools.yang.data.impl.ImmutableCompositeNode;
+import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlDocumentUtils;
+import org.opendaylight.yangtools.yang.data.impl.util.CompositeNodeBuilder;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.gson.stream.JsonWriter;
+
+/**
+ * This class defines an ExceptionMapper that handles RestconfDocumentedExceptions thrown by
+ * resource implementations and translates appropriately to restconf error response as defined in
+ * the RESTCONF RFC draft.
+ *
+ * @author Thomas Pantelis
+ */
+@Provider
+public class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
+
+    private final static Logger LOG = LoggerFactory.getLogger( RestconfDocumentedExceptionMapper.class );
+
+    @Context
+    private HttpHeaders headers;
+
+    @Override
+    public Response toResponse( RestconfDocumentedException exception ) {
+
+        LOG.debug( "In toResponse: {}", exception.getMessage() );
+
+        // Default to the content type if there's no Accept header
+
+        MediaType mediaType = headers.getMediaType();
+
+        List<MediaType> accepts = headers.getAcceptableMediaTypes();
+
+        LOG.debug( "Accept headers: {}", accepts );
+
+        if( accepts != null && accepts.size() > 0 ) {
+            mediaType = accepts.get( 0 ); // just pick the first one
+        }
+
+        LOG.debug( "Using MediaType: {}",  mediaType );
+
+        List<RestconfError> errors = exception.getErrors();
+        if( errors.isEmpty() ) {
+            // We don't actually want to send any content but, if we don't set any content here,
+            // the tomcat front-end will send back an html error report. To prevent that, set a
+            // single space char in the entity.
+
+            return Response.status( exception.getStatus() )
+                                    .type( MediaType.TEXT_PLAIN_TYPE )
+                                    .entity( " " ).build();
+        }
+
+        Status status = errors.iterator().next().getErrorTag().getStatusCode();
+
+        ControllerContext context = ControllerContext.getInstance();
+        DataNodeContainer errorsSchemaNode = (DataNodeContainer)context.getRestconfModuleErrorsSchemaNode();
+
+        if( errorsSchemaNode == null ) {
+            return Response.status( status )
+                           .type( MediaType.TEXT_PLAIN_TYPE )
+                           .entity( exception.getMessage() ).build();
+        }
+
+        ImmutableList.Builder<Node<?>> errorNodes = ImmutableList.<Node<?>> builder();
+        for( RestconfError error: errors ) {
+            errorNodes.add( toDomNode( error ) );
+        }
+
+        ImmutableCompositeNode errorsNode =
+                         ImmutableCompositeNode.create( ERRORS_CONTAINER_QNAME, errorNodes.build() );
+
+        Object responseBody;
+        if( mediaType.getSubtype().endsWith( "json" ) ) {
+            responseBody = toJsonResponseBody( errorsNode, errorsSchemaNode );
+        }
+        else {
+            responseBody = toXMLResponseBody( errorsNode, errorsSchemaNode );
+        }
+
+        return Response.status( status ).type( mediaType ).entity( responseBody ).build();
+    }
+
+    private Object toJsonResponseBody( ImmutableCompositeNode errorsNode,
+                                       DataNodeContainer errorsSchemaNode ) {
+
+        JsonMapper jsonMapper = new JsonMapper();
+
+        Object responseBody = null;
+        try {
+            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+            JsonWriter writer = new JsonWriter( new OutputStreamWriter( outStream, "UTF-8" ) );
+            writer.setIndent( "    " );
+
+            jsonMapper.write( writer, errorsNode, errorsSchemaNode, null );
+            writer.flush();
+
+            responseBody = outStream.toString( "UTF-8" );
+        }
+        catch( IOException e ) {
+            LOG.error( "Error writing error response body", e );
+        }
+
+        return responseBody;
+    }
+
+    private Object toXMLResponseBody( ImmutableCompositeNode errorsNode,
+                                      DataNodeContainer errorsSchemaNode ) {
+
+        XmlMapper xmlMapper = new XmlMapper();
+
+        Object responseBody = null;
+        try {
+            Document xmlDoc = xmlMapper.write( errorsNode, errorsSchemaNode );
+
+            responseBody = documentToString( xmlDoc );
+        }
+        catch( TransformerException | UnsupportedDataTypeException | UnsupportedEncodingException e ) {
+            LOG.error( "Error writing error response body", e );
+        }
+
+        return responseBody;
+    }
+
+    private String documentToString( Document doc ) throws TransformerException, UnsupportedEncodingException {
+        Transformer transformer = createTransformer();
+        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+
+        transformer.transform( new DOMSource( doc ), new StreamResult( outStream ) );
+
+        return outStream.toString( "UTF-8" );
+    }
+
+    private Transformer createTransformer() throws TransformerFactoryConfigurationError,
+        TransformerConfigurationException {
+        TransformerFactory tf = TransformerFactory.newInstance();
+        Transformer transformer = tf.newTransformer();
+        transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "no" );
+        transformer.setOutputProperty( OutputKeys.METHOD, "xml" );
+        transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
+        transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" );
+        transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "4" );
+        return transformer;
+    }
+
+    private Node<?> toDomNode( RestconfError error ) {
+
+        CompositeNodeBuilder<ImmutableCompositeNode> builder = ImmutableCompositeNode.builder();
+        builder.setQName( ERROR_LIST_QNAME );
+
+        addLeaf( builder, ERROR_TYPE_QNAME, error.getErrorType().getErrorTypeTag() );
+        addLeaf( builder, ERROR_TAG_QNAME, error.getErrorTag().getTagValue() );
+        addLeaf( builder, ERROR_MESSAGE_QNAME, error.getErrorMessage() );
+        addLeaf( builder, ERROR_APP_TAG_QNAME, error.getErrorAppTag() );
+
+        Node<?> errorInfoNode = parseErrorInfo( error.getErrorInfo() );
+        if( errorInfoNode != null ) {
+            builder.add( errorInfoNode );
+        }
+
+        return builder.toInstance();
+    }
+
+    private Node<?> parseErrorInfo( String errorInfo ) {
+        if( Strings.isNullOrEmpty( errorInfo ) ) {
+            return null;
+        }
+
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware( true );
+        factory.setCoalescing( true );
+        factory.setIgnoringElementContentWhitespace( true );
+        factory.setIgnoringComments( true );
+
+        // Wrap the error info content in a root <error-info> element so it can be parsed
+        // as XML. The error info content may or may not be XML. If not then it will be
+        // parsed as text content of the <error-info> element.
+
+        String errorInfoWithRoot =
+                new StringBuilder( "<error-info xmlns=\"" ).append( NAMESPACE ).append( "\">" )
+                        .append( errorInfo ).append( "</error-info>" ).toString();
+
+        Document doc = null;
+        try {
+            doc = factory.newDocumentBuilder().parse(
+                                 new InputSource( new StringReader( errorInfoWithRoot ) ) );
+        }
+        catch( Exception e ) {
+            // TODO: what if the content is text that happens to contain invalid markup? Could
+            // wrap in CDATA and try again.
+
+            LOG.warn( "Error parsing restconf error-info, \"" + errorInfo + "\", as XML: " +
+                      e.toString() );
+            return null;
+        }
+
+        Node<?> errorInfoNode = XmlDocumentUtils.toDomNode( doc );
+
+        if( errorInfoNode instanceof CompositeNode ) {
+            CompositeNode compositeNode = (CompositeNode)XmlDocumentUtils.toDomNode( doc );
+
+            // At this point the QName for the "error-info" CompositeNode doesn't contain the revision
+            // as it isn't present in the XML. So we'll copy all the child nodes and create a new
+            // CompositeNode with the full QName. This is done so the XML/JSON mapping code can
+            // locate the schema.
+
+            ImmutableList.Builder<Node<?>> childNodes = ImmutableList.builder();
+            for( Entry<QName, List<Node<?>>> entry: compositeNode.entrySet() ) {
+                childNodes.addAll( entry.getValue() );
+            }
+
+            errorInfoNode = ImmutableCompositeNode.create( ERROR_INFO_QNAME, childNodes.build() );
+        }
+
+        return errorInfoNode;
+    }
+
+    private void addLeaf( CompositeNodeBuilder<ImmutableCompositeNode> builder, QName qname,
+                          String value ) {
+        if( !Strings.isNullOrEmpty( value ) ) {
+            builder.addLeaf( qname, value );
+        }
+    }
+}
index 5dba747..422cf04 100644 (file)
@@ -23,7 +23,7 @@ import javax.ws.rs.ext.Provider;
 
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.controller.sal.restconf.impl.StructuredData;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
@@ -38,7 +38,7 @@ public enum StructuredDataToJsonProvider implements MessageBodyWriter<Structured
 
     @Override
     public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
-        return true;
+        return type.equals( StructuredData.class );
     }
 
     @Override
@@ -52,7 +52,7 @@ public enum StructuredDataToJsonProvider implements MessageBodyWriter<Structured
             throws IOException, WebApplicationException {
         CompositeNode data = t.getData();
         if (data == null) {
-            throw new ResponseException(Response.Status.NOT_FOUND, "No data exists.");
+            throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
         }
 
         JsonWriter writer = new JsonWriter(new OutputStreamWriter(entityStream, "UTF-8"));
index 7d6b329..bcb3c42 100644 (file)
@@ -28,7 +28,9 @@ import javax.xml.transform.stream.StreamResult;
 
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.controller.sal.restconf.impl.StructuredData;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
@@ -46,7 +48,7 @@ public enum StructuredDataToXmlProvider implements MessageBodyWriter<StructuredD
 
     @Override
     public boolean isWriteable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
-        return true;
+        return type.equals( StructuredData.class );
     }
 
     @Override
@@ -60,9 +62,9 @@ public enum StructuredDataToXmlProvider implements MessageBodyWriter<StructuredD
             throws IOException, WebApplicationException {
         CompositeNode data = t.getData();
         if (data == null) {
-            throw new ResponseException(Response.Status.NOT_FOUND, "No data exists.");
+            throw new RestconfDocumentedException(Response.Status.NOT_FOUND);
         }
-        
+
         XmlMapper xmlMapper = new XmlMapper();
         Document domTree = xmlMapper.write(data, (DataNodeContainer) t.getSchema());
         try {
@@ -76,7 +78,8 @@ public enum StructuredDataToXmlProvider implements MessageBodyWriter<StructuredD
             transformer.transform(new DOMSource(domTree), new StreamResult(entityStream));
         } catch (TransformerException e) {
             logger.error("Error during translation of Document to OutputStream", e);
-            throw new ResponseException(Response.Status.INTERNAL_SERVER_ERROR, e.getMessage());
+            throw new RestconfDocumentedException( e.getMessage(), ErrorType.TRANSPORT,
+                                                   ErrorTag.OPERATION_FAILED );
         }
     }
 
index 13d6170..bc74738 100644 (file)
@@ -16,15 +16,18 @@ import javax.ws.rs.Consumes;
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.ws.rs.core.Response;
 import javax.ws.rs.ext.MessageBodyReader;
 import javax.ws.rs.ext.Provider;
 import javax.xml.stream.XMLStreamException;
 
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 @Provider
 @Consumes({ Draft02.MediaTypes.DATA + RestconfService.XML, Draft02.MediaTypes.OPERATION + RestconfService.XML,
@@ -32,6 +35,8 @@ import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 public enum XmlToCompositeNodeProvider implements MessageBodyReader<CompositeNode> {
     INSTANCE;
 
+    private final static Logger LOG = LoggerFactory.getLogger( XmlToCompositeNodeProvider.class );
+
     @Override
     public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
         return true;
@@ -45,7 +50,10 @@ public enum XmlToCompositeNodeProvider implements MessageBodyReader<CompositeNod
         try {
             return xmlReader.read(entityStream);
         } catch (XMLStreamException | UnsupportedFormatException e) {
-            throw new ResponseException(Response.Status.BAD_REQUEST, e.getMessage());
+            LOG.debug( "Error parsing json input", e );
+            throw new RestconfDocumentedException(
+                            "Error parsing input: " + e.getMessage(),
+                            ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE );
         }
     }
 
index 1cc1f78..062d03a 100644 (file)
@@ -18,7 +18,8 @@ import org.opendaylight.controller.sal.core.api.data.DataBrokerService;
 import org.opendaylight.controller.sal.core.api.data.DataChangeListener;
 import org.opendaylight.controller.sal.core.api.data.DataModificationTransaction;
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
-import org.opendaylight.controller.sal.rest.impl.RestconfProvider;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
 import org.opendaylight.yangtools.yang.common.QName;
@@ -53,9 +54,7 @@ public class BrokerFacade implements DataReader<InstanceIdentifier, CompositeNod
 
     private void checkPreconditions() {
         if( context == null || dataService == null ) {
-            ResponseException _responseException = new ResponseException( Status.SERVICE_UNAVAILABLE,
-                    RestconfProvider.NOT_INITALIZED_MSG );
-            throw _responseException;
+            throw new RestconfDocumentedException( Status.SERVICE_UNAVAILABLE );
         }
     }
 
@@ -95,17 +94,10 @@ public class BrokerFacade implements DataReader<InstanceIdentifier, CompositeNod
         return mountPoint.readOperationalData( path );
     }
 
-    public RpcResult<CompositeNode> invokeRpc( final QName type, final CompositeNode payload ) {
+    public Future<RpcResult<CompositeNode>> invokeRpc( final QName type, final CompositeNode payload ) {
         this.checkPreconditions();
 
-        final Future<RpcResult<CompositeNode>> future = context.rpc( type, payload );
-
-        try {
-            return future.get();
-        }
-        catch( Exception e ) {
-            throw new ResponseException( e, "Error invoking RPC " + type );
-        }
+        return context.rpc( type, payload );
     }
 
     public Future<RpcResult<TransactionStatus>> commitConfigurationDataPut( final InstanceIdentifier path,
@@ -138,9 +130,9 @@ public class BrokerFacade implements DataReader<InstanceIdentifier, CompositeNod
         if (availableNode != null) {
             String errMsg = "Post Configuration via Restconf was not executed because data already exists";
             BrokerFacade.LOG.warn((new StringBuilder(errMsg)).append(" : ").append(path).toString());
-            // FIXME: return correct ietf-restconf:errors -> follow specification
-            // (http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48)
-            throw new ResponseException(Status.CONFLICT, errMsg);
+
+            throw new RestconfDocumentedException(
+                    "Data already exists for path: " + path, ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS );
         }
         BrokerFacade.LOG.trace( "Post Configuration via Restconf: {}", path );
         transaction.putConfigurationData( path, payload );
@@ -157,9 +149,9 @@ public class BrokerFacade implements DataReader<InstanceIdentifier, CompositeNod
         if (availableNode != null) {
             String errMsg = "Post Configuration via Restconf was not executed because data already exists";
             BrokerFacade.LOG.warn((new StringBuilder(errMsg)).append(" : ").append(path).toString());
-            // FIXME: return correct ietf-restconf:errors -> follow specification
-            // (http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48)
-            throw new ResponseException(Status.CONFLICT, errMsg);
+
+            throw new RestconfDocumentedException(
+                    "Data already exists for path: " + path, ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS );
         }
         BrokerFacade.LOG.trace( "Post Configuration via Restconf: {}", path );
         transaction.putConfigurationData( path, payload );
index 1c076d1..86ed13a 100644 (file)
@@ -38,11 +38,12 @@ import javax.ws.rs.core.Response.Status;
 
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
 import org.opendaylight.controller.sal.core.api.mount.MountService;
+import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.impl.RestUtil;
-import org.opendaylight.controller.sal.rest.impl.RestconfProvider;
 import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
 import org.opendaylight.controller.sal.restconf.impl.RestCodec;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.yangtools.concepts.Codec;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
@@ -55,6 +56,7 @@ import org.opendaylight.yangtools.yang.model.api.ChoiceNode;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.GroupingDefinition;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
@@ -108,7 +110,7 @@ public class ControllerContext implements SchemaContextListener {
 
     private void checkPreconditions() {
         if( globalSchema == null ) {
-            throw new ResponseException( Status.SERVICE_UNAVAILABLE, RestconfProvider.NOT_INITALIZED_MSG );
+            throw new RestconfDocumentedException( Status.SERVICE_UNAVAILABLE );
         }
     }
 
@@ -139,8 +141,9 @@ public class ControllerContext implements SchemaContextListener {
         String first = pathArgs.iterator().next();
         final String startModule = ControllerContext.toModuleName( first );
         if( startModule == null ) {
-            throw new ResponseException( Status.BAD_REQUEST,
-                    "First node in URI has to be in format \"moduleName:nodeName\"" );
+            throw new RestconfDocumentedException(
+                    "First node in URI has to be in format \"moduleName:nodeName\"",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         InstanceIdentifierBuilder builder = InstanceIdentifier.builder();
@@ -149,7 +152,8 @@ public class ControllerContext implements SchemaContextListener {
                                                            latestModule, null, toMountPointIdentifier );
 
         if( iiWithSchemaNode == null ) {
-            throw new ResponseException( Status.BAD_REQUEST, "URI has bad format" );
+            throw new RestconfDocumentedException(
+                    "URI has bad format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         return iiWithSchemaNode;
@@ -386,6 +390,112 @@ public class ControllerContext implements SchemaContextListener {
         return builder.toString();
     }
 
+    public Module getRestconfModule() {
+        return findModuleByNameAndRevision( Draft02.RestConfModule.IETF_RESTCONF_QNAME );
+    }
+
+    public DataSchemaNode getRestconfModuleErrorsSchemaNode() {
+        Module restconfModule = getRestconfModule();
+        if( restconfModule == null ) {
+            return null;
+        }
+
+        Set<GroupingDefinition> groupings = restconfModule.getGroupings();
+
+        final Predicate<GroupingDefinition> filter = new Predicate<GroupingDefinition>() {
+            @Override
+            public boolean apply(final GroupingDefinition g) {
+                return Objects.equal(g.getQName().getLocalName(),
+                                     Draft02.RestConfModule.ERRORS_GROUPING_SCHEMA_NODE);
+            }
+        };
+
+        Iterable<GroupingDefinition> filteredGroups = Iterables.filter(groupings, filter);
+
+        final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null);
+
+        List<DataSchemaNode> instanceDataChildrenByName =
+                this.findInstanceDataChildrenByName(restconfGrouping,
+                                                    Draft02.RestConfModule.ERRORS_CONTAINER_SCHEMA_NODE);
+        return Iterables.getFirst(instanceDataChildrenByName, null);
+    }
+
+    public DataSchemaNode getRestconfModuleRestConfSchemaNode( Module inRestconfModule,
+                                                               String schemaNodeName ) {
+        Module restconfModule = inRestconfModule;
+        if( restconfModule == null ) {
+            restconfModule = getRestconfModule();
+        }
+
+        if( restconfModule == null ) {
+            return null;
+        }
+
+        Set<GroupingDefinition> groupings = restconfModule.getGroupings();
+
+        final Predicate<GroupingDefinition> filter = new Predicate<GroupingDefinition>() {
+            @Override
+            public boolean apply(final GroupingDefinition g) {
+                return Objects.equal(g.getQName().getLocalName(),
+                                     Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE);
+            }
+        };
+
+        Iterable<GroupingDefinition> filteredGroups = Iterables.filter(groupings, filter);
+
+        final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null);
+
+        List<DataSchemaNode> instanceDataChildrenByName =
+                this.findInstanceDataChildrenByName(restconfGrouping,
+                                                            Draft02.RestConfModule.RESTCONF_CONTAINER_SCHEMA_NODE);
+        final DataSchemaNode restconfContainer = Iterables.getFirst(instanceDataChildrenByName, null);
+
+        if (Objects.equal(schemaNodeName, Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                    Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+        else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                   Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+        else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                   Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
+            final DataSchemaNode modules = Iterables.getFirst(instances, null);
+            instances = this.findInstanceDataChildrenByName(((DataNodeContainer) modules),
+                                                               Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+        else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                         Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+        else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                         Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
+            final DataSchemaNode modules = Iterables.getFirst(instances, null);
+            instances = this.findInstanceDataChildrenByName(((DataNodeContainer) modules),
+                                                                 Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+        else if(Objects.equal(schemaNodeName, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE)) {
+            List<DataSchemaNode> instances =
+                    this.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
+                                                   Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
+            return Iterables.getFirst(instances, null);
+        }
+
+        return null;
+    }
+
     private static DataSchemaNode childByQName( final ChoiceNode container, final QName name ) {
         for( final ChoiceCaseNode caze : container.getCases() ) {
             final DataSchemaNode ret = ControllerContext.childByQName( caze, name );
@@ -461,27 +571,30 @@ public class ControllerContext implements SchemaContextListener {
             if( Objects.equal( moduleName, ControllerContext.MOUNT_MODULE ) &&
                 Objects.equal( nodeName, ControllerContext.MOUNT_NODE ) ) {
                 if( mountPoint != null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                                         "Restconf supports just one mount point in URI." );
+                    throw new RestconfDocumentedException(
+                            "Restconf supports just one mount point in URI.",
+                            ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED );
                 }
 
                 if( mountService == null ) {
-                    throw new ResponseException( Status.SERVICE_UNAVAILABLE,
-                                "MountService was not found. Finding behind mount points does not work." );
+                    throw new RestconfDocumentedException(
+                            "MountService was not found. Finding behind mount points does not work.",
+                            ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED );
                 }
 
                 final InstanceIdentifier partialPath = builder.toInstance();
                 final MountInstance mount = mountService.getMountPoint( partialPath );
                 if( mount == null ) {
                     LOG.debug( "Instance identifier to missing mount point: {}", partialPath );
-                    throw new ResponseException( Status.BAD_REQUEST,
-                                                 "Mount point does not exist." );
+                    throw new RestconfDocumentedException(
+                         "Mount point does not exist.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
                 }
 
                 final SchemaContext mountPointSchema = mount.getSchemaContext();
                 if( mountPointSchema == null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                                       "Mount point does not contain any schema with modules." );
+                    throw new RestconfDocumentedException(
+                            "Mount point does not contain any schema with modules.",
+                            ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT );
                 }
 
                 if( returnJustMountPoint ) {
@@ -496,16 +609,17 @@ public class ControllerContext implements SchemaContextListener {
 
                 final String moduleNameBehindMountPoint = toModuleName(  strings.get( 1 ) );
                 if( moduleNameBehindMountPoint == null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                            "First node after mount point in URI has to be in format \"moduleName:nodeName\"" );
+                    throw new RestconfDocumentedException(
+                        "First node after mount point in URI has to be in format \"moduleName:nodeName\"",
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
                 }
 
                 final Module moduleBehindMountPoint = this.getLatestModule( mountPointSchema,
                                                                             moduleNameBehindMountPoint );
                 if( moduleBehindMountPoint == null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                                                 "URI has bad format. \"" + moduleName +
-                                                 "\" module does not exist in mount point." );
+                    throw new RestconfDocumentedException(
+                            "\"" +moduleName + "\" module does not exist in mount point.",
+                            ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
                 }
 
                 List<String> subList = strings.subList( 1, strings.size() );
@@ -517,8 +631,9 @@ public class ControllerContext implements SchemaContextListener {
             if( mountPoint == null ) {
                 module = this.getLatestModule( globalSchema, moduleName );
                 if( module == null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                            "URI has bad format. \"" + moduleName + "\" module does not exist." );
+                    throw new RestconfDocumentedException(
+                            "\"" + moduleName + "\" module does not exist.",
+                            ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
                 }
             }
             else {
@@ -526,20 +641,20 @@ public class ControllerContext implements SchemaContextListener {
                 module = schemaContext == null ? null :
                                           this.getLatestModule( schemaContext, moduleName );
                 if( module == null ) {
-                    throw new ResponseException( Status.BAD_REQUEST,
-                                        "URI has bad format. \"" + moduleName +
-                                        "\" module does not exist in mount point." );
+                    throw new RestconfDocumentedException(
+                            "\"" + moduleName + "\" module does not exist in mount point.",
+                            ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
                 }
             }
 
             targetNode = this.findInstanceDataChildByNameAndNamespace(
                                           parentNode, nodeName, module.getNamespace() );;
             if( targetNode == null ) {
-                throw new ResponseException( Status.BAD_REQUEST,
-                            "URI has bad format. Possible reasons:\n" +
-                            "1. \"" + head + "\" was not found in parent data node.\n" +
-                            "2. \"" + head + "\" is behind mount point. Then it should be in format \"/" +
-                            MOUNT + "/" + head + "\"." );
+                throw new RestconfDocumentedException(
+                        "URI has bad format. Possible reasons:\n" +
+                        1. \"" + head + "\" was not found in parent data node.\n" +
+                        2. \"" + head + "\" is behind mount point. Then it should be in format \"/" +
+                        MOUNT + "/" + head + "\".", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
         } else {
             final List<DataSchemaNode> potentialSchemaNodes =
@@ -552,26 +667,27 @@ public class ControllerContext implements SchemaContextListener {
                               .append( "\n" );
                 }
 
-                throw new ResponseException( Status.BAD_REQUEST,
+                throw new RestconfDocumentedException(
                         "URI has bad format. Node \"" + nodeName +
                         "\" is added as augment from more than one module. " +
                         "Therefore the node must have module name and it has to be in format \"moduleName:nodeName\"." +
                         "\nThe node is added as augment from modules with namespaces:\n" +
-                        strBuilder.toString() );
+                        strBuilder.toString(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
 
             if( potentialSchemaNodes.isEmpty() ) {
-                throw new ResponseException( Status.BAD_REQUEST, "URI has bad format. \"" + nodeName +
-                                             "\" was not found in parent data node.\n" );
+                throw new RestconfDocumentedException(
+                        "\"" + nodeName + "\" in URI was not found in parent data node",
+                        ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
             }
 
             targetNode = potentialSchemaNodes.iterator().next();
         }
 
         if( !this.isListOrContainer( targetNode ) ) {
-            throw new ResponseException( Status.BAD_REQUEST,
-                                         "URI has bad format. Node \"" + head +
-                                         "\" must be Container or List yang type." );
+            throw new RestconfDocumentedException(
+                    "URI has bad format. Node \"" + head + "\" must be Container or List yang type.",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         int consumed = 1;
@@ -579,8 +695,9 @@ public class ControllerContext implements SchemaContextListener {
             final ListSchemaNode listNode = ((ListSchemaNode) targetNode);
             final int keysSize = listNode.getKeyDefinition().size();
             if( (strings.size() - consumed) < keysSize ) {
-                throw new ResponseException( Status.BAD_REQUEST, "Missing key for list \"" +
-                                             listNode.getQName().getLocalName() + "\"." );
+                throw new RestconfDocumentedException(
+                        "Missing key for list \"" + listNode.getQName().getLocalName() + "\".",
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
 
             final List<String> uriKeyValues = strings.subList( consumed, consumed + keysSize );
@@ -590,9 +707,10 @@ public class ControllerContext implements SchemaContextListener {
                 {
                     final String uriKeyValue = uriKeyValues.get( i );
                     if( uriKeyValue.equals( NULL_VALUE ) ) {
-                        throw new ResponseException( Status.BAD_REQUEST,
-                                    "URI has bad format. List \"" + listNode.getQName().getLocalName() +
-                                    "\" cannot contain \"null\" value as a key." );
+                        throw new RestconfDocumentedException(
+                                "URI has bad format. List \"" + listNode.getQName().getLocalName() +
+                                "\" cannot contain \"null\" value as a key.",
+                                ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
                     }
 
                     this.addKeyValue( keyValues, listNode.getDataChildByName( key ),
@@ -711,8 +829,9 @@ public class ControllerContext implements SchemaContextListener {
         }
 
         if( decoded == null ) {
-            throw new ResponseException( Status.BAD_REQUEST, uriValue + " from URI can\'t be resolved. " +
-                                         additionalInfo );
+            throw new RestconfDocumentedException(
+                            uriValue + " from URI can't be resolved. " + additionalInfo,
+                            ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         map.put( node.getQName(), decoded );
@@ -807,8 +926,9 @@ public class ControllerContext implements SchemaContextListener {
             return decodedPathArgs;
         }
         catch( UnsupportedEncodingException e ) {
-            throw new ResponseException( Status.BAD_REQUEST,
-                    "Invalid URL path '" + strings + "': " + e.getMessage() );
+            throw new RestconfDocumentedException(
+                    "Invalid URL path '" + strings + "': " + e.getMessage(),
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
     }
 
@@ -818,8 +938,9 @@ public class ControllerContext implements SchemaContextListener {
                 return URLDecoder.decode( pathArg, URI_ENCODING_CHAR_SET );
             }
             catch( UnsupportedEncodingException e ) {
-                throw new ResponseException( Status.BAD_REQUEST,
-                        "Invalid URL path arg '" + pathArg + "': " + e.getMessage() );
+                throw new RestconfDocumentedException(
+                        "Invalid URL path arg '" + pathArg + "': " + e.getMessage(),
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
         }
 
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/ResponseException.java
deleted file mode 100644 (file)
index 007fb8e..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) 2014 Cisco Systems, Inc. and others.  All rights reserved.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License v1.0 which accompanies this distribution,
- * and is available at http://www.eclipse.org/legal/epl-v10.html
- */
-package org.opendaylight.controller.sal.restconf.impl;
-
-import javax.ws.rs.WebApplicationException;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
-
-public class ResponseException extends WebApplicationException {
-
-    private static final long serialVersionUID = -5320114450593021655L;
-
-    public ResponseException(Status status, String msg) {
-        super(Response.status(status).type(MediaType.TEXT_PLAIN_TYPE).entity(msg).build());
-    }
-
-    public ResponseException(Throwable cause, String msg) {
-        super(cause, Response.status(Status.INTERNAL_SERVER_ERROR).
-                                         type(MediaType.TEXT_PLAIN_TYPE).entity(msg).build());
-    }
-}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfDocumentedException.java
new file mode 100644 (file)
index 0000000..0548e95
--- /dev/null
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.sal.restconf.impl;
+
+import java.util.List;
+
+import javax.ws.rs.WebApplicationException;
+import javax.ws.rs.core.Response.Status;
+
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+
+/**
+ * Unchecked exception to communicate error information, as defined in the ietf restcong draft,
+ * to be sent to the client.
+ *
+ * @author Devin Avery
+ * @author Thomas Pantelis
+ * @see {@link https://tools.ietf.org/html/draft-bierman-netconf-restconf-02}
+ */
+public class RestconfDocumentedException extends WebApplicationException {
+
+    private static final long serialVersionUID = 1L;
+
+    private final List<RestconfError> errors;
+    private final Status status;
+
+    /**
+     * Constructs an instance with an error message. The error type defaults to APPLICATION and
+     * the error tag defaults to OPERATION_FAILED.
+     *
+     * @param message A string which provides a plain text string describing the error.
+     */
+    public RestconfDocumentedException( String message ) {
+        this( message, RestconfError.ErrorType.APPLICATION, RestconfError.ErrorTag.OPERATION_FAILED );
+    }
+
+    /**
+     * Constructs an instance with an error message, error type, and error tag.
+     *
+     * @param message A string which provides a plain text string describing the error.
+     * @param errorType The enumerated type indicating the layer where the error occurred.
+     * @param errorTag The enumerated tag representing a more specific error cause.
+     */
+    public RestconfDocumentedException( String message, ErrorType errorType, ErrorTag errorTag ) {
+        this( null, new RestconfError( errorType, errorTag, message ) );
+    }
+
+    /**
+     * Constructs an instance with an error message and exception cause. The stack trace of the
+     * exception is included in the error info.
+     *
+     * @param message A string which provides a plain text string describing the error.
+     * @param cause The underlying exception cause.
+     */
+    public RestconfDocumentedException( String message, Throwable cause ) {
+        this( cause, new RestconfError( RestconfError.ErrorType.APPLICATION,
+                                        RestconfError.ErrorTag.OPERATION_FAILED, message,
+                                        null, RestconfError.toErrorInfo( cause ) ) );
+    }
+
+    /**
+     * Constructs an instance with the given error.
+     */
+    public RestconfDocumentedException( RestconfError error ) {
+        this( null, error );
+    }
+
+    /**
+     * Constructs an instance with the given errors.
+     */
+    public RestconfDocumentedException( List<RestconfError> errors ) {
+        this.errors = ImmutableList.copyOf( errors );
+        Preconditions.checkArgument( !this.errors.isEmpty(), "RestconfError list can't be empty" );
+        status = null;
+    }
+
+    /**
+     * Constructs an instance with an HTTP status and no error information.
+     *
+     * @param status the HTTP status.
+     */
+    public RestconfDocumentedException( Status status ) {
+        Preconditions.checkNotNull( status, "Status can't be null" );
+        errors = ImmutableList.of();
+        this.status = status;
+    }
+
+    private RestconfDocumentedException( Throwable cause, RestconfError error ) {
+        super( cause );
+        Preconditions.checkNotNull( error, "RestconfError can't be null" );
+        errors = ImmutableList.of( error );
+        status = null;
+    }
+
+    public List<RestconfError> getErrors() {
+        return errors;
+    }
+
+    public Status getStatus() {
+        return status;
+    }
+
+
+    @Override
+    public String getMessage() {
+        return "errors: " + errors + (status != null ? ", status: " + status : "");
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java b/opendaylight/md-sal/sal-rest-connector/src/main/java/org/opendaylight/controller/sal/restconf/impl/RestconfError.java
new file mode 100644 (file)
index 0000000..9220f8b
--- /dev/null
@@ -0,0 +1,221 @@
+/*
+* Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
+*
+* This program and the accompanying materials are made available under the
+* terms of the Eclipse Public License v1.0 which accompanies this distribution,
+* and is available at http://www.eclipse.org/legal/epl-v10.html
+*/
+package org.opendaylight.controller.sal.restconf.impl;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.opendaylight.yangtools.yang.common.RpcError;
+
+import com.google.common.base.Preconditions;
+
+/**
+ * Encapsulates a restconf error as defined in the ietf restconf draft.
+ *
+ * <br><br><b>Note:</b> Enumerations defined within are provided by the ietf restconf draft.
+ *
+ * @author Devin Avery
+ * @see {@link https://tools.ietf.org/html/draft-bierman-netconf-restconf-02}
+ */
+public class RestconfError {
+
+    public static enum ErrorType {
+        /** Errors relating to the transport layer */
+        TRANSPORT,
+        /** Errors relating to the RPC or notification layer */
+        RPC,
+        /** Errors relating to the protocol operation layer. */
+        PROTOCOL,
+        /** Errors relating to the server application layer. */
+        APPLICATION;
+
+        public String getErrorTypeTag() {
+            return name().toLowerCase();
+        }
+
+        public static ErrorType valueOfCaseInsensitive( String value )
+        {
+            try {
+                return ErrorType.valueOf( ErrorType.class, value.toUpperCase() );
+            }
+            catch( IllegalArgumentException e ) {
+                return APPLICATION;
+            }
+        }
+    }
+
+    public static enum ErrorTag {
+        IN_USE( "in-use", Status.fromStatusCode(409)),
+        INVALID_VALUE( "invalid-value", Status.fromStatusCode(400)),
+        TOO_BIG( "too-big", Status.fromStatusCode(413)),
+        MISSING_ATTRIBUTE( "missing-attribute", Status.fromStatusCode(400)),
+        BAD_ATTRIBUTE( "bad-attribute", Status.fromStatusCode(400)),
+        UNKNOWN_ATTRIBUTE( "unknown-attribute", Status.fromStatusCode(400)),
+        BAD_ELEMENT( "bad-element", Status.fromStatusCode(400)),
+        UNKNOWN_ELEMENT( "unknown-element", Status.fromStatusCode(400)),
+        UNKNOWN_NAMESPACE( "unknown-namespace", Status.fromStatusCode(400)),
+        ACCESS_DENIED( "access-denied", Status.fromStatusCode(403)),
+        LOCK_DENIED( "lock-denied", Status.fromStatusCode(409)),
+        RESOURCE_DENIED( "resource-denied", Status.fromStatusCode(409)),
+        ROLLBACK_FAILED( "rollback-failed", Status.fromStatusCode(500)),
+        DATA_EXISTS( "data-exists", Status.fromStatusCode(409)),
+        DATA_MISSING( "data-missing", Status.fromStatusCode(409)),
+        OPERATION_NOT_SUPPORTED( "operation-not-supported", Status.fromStatusCode(501)),
+        OPERATION_FAILED( "operation-failed", Status.fromStatusCode(500)),
+        PARTIAL_OPERATION( "partial-operation", Status.fromStatusCode(500)),
+        MALFORMED_MESSAGE( "malformed-message", Status.fromStatusCode(400));
+
+        private final String tagValue;
+        private final Status statusCode;
+
+        ErrorTag(final String tagValue, final Status statusCode) {
+            this.tagValue = tagValue;
+            this.statusCode = statusCode;
+        }
+
+        public String getTagValue() {
+            return this.tagValue.toLowerCase();
+        }
+
+        public static ErrorTag valueOfCaseInsensitive( String value )
+        {
+            try {
+                return ErrorTag.valueOf( ErrorTag.class, value.toUpperCase().replaceAll( "-","_" ) );
+            }
+            catch( IllegalArgumentException e ) {
+                return OPERATION_FAILED;
+            }
+        }
+
+        public Status getStatusCode() {
+            return statusCode;
+        }
+    }
+
+    private final ErrorType errorType;
+    private final ErrorTag errorTag;
+    private final String errorInfo;
+    private final String errorAppTag;
+    private final String errorMessage;
+    //TODO: Add in the error-path concept as defined in the ietf draft.
+
+    static String toErrorInfo( Throwable cause ) {
+        StringWriter writer = new StringWriter();
+        cause.printStackTrace( new PrintWriter( writer ) );
+        return writer.toString();
+    }
+
+    /**
+     * Constructs a RestConfError
+     *
+     * @param errorType The enumerated type indicating the layer where the error occurred.
+     * @param errorTag The enumerated tag representing a more specific error cause.
+     * @param errorMessage A string which provides a plain text string describing the error.
+     */
+    public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage) {
+        this( errorType, errorTag, errorMessage, null );
+    }
+
+    /**
+     * Constructs a RestConfError object.
+     *
+     * @param errorType The enumerated type indicating the layer where the error occurred.
+     * @param errorTag The enumerated tag representing a more specific error cause.
+     * @param errorMessage A string which provides a plain text string describing the error.
+     * @param errorAppTag A string which represents an application-specific error tag that further
+     *                    specifies the error cause.
+     */
+    public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage,
+                         String errorAppTag) {
+        this( errorType, errorTag, errorMessage, errorAppTag, null );
+    }
+
+    /**
+     * Constructs a RestConfError object.
+     *
+     * @param errorType The enumerated type indicating the layer where the error occurred.
+     * @param errorTag The enumerated tag representing a more specific error cause.
+     * @param errorMessage A string which provides a plain text string describing the error.
+     * @param errorAppTag A string which represents an application-specific error tag that further
+     *                    specifies the error cause.
+     * @param errorInfo A string, <b>formatted as XML</b>, which contains additional error information.
+     */
+    public RestconfError(ErrorType errorType, ErrorTag errorTag, String errorMessage,
+                         String errorAppTag, String errorInfo) {
+        Preconditions.checkNotNull( errorType, "Error type is required for RestConfError" );
+        Preconditions.checkNotNull( errorTag, "Error tag is required for RestConfError");
+        this.errorType = errorType;
+        this.errorTag = errorTag;
+        this.errorMessage = errorMessage;
+        this.errorAppTag = errorAppTag;
+        this.errorInfo = errorInfo;
+    }
+
+    /**
+     * Constructs a RestConfError object from an RpcError.
+     */
+    public RestconfError( RpcError rpcError ) {
+
+        this.errorType = rpcError.getErrorType() == null ? ErrorType.APPLICATION :
+                               ErrorType.valueOfCaseInsensitive( rpcError.getErrorType().name() );
+
+        this.errorTag = rpcError.getTag() == null ? ErrorTag.OPERATION_FAILED :
+                                    ErrorTag.valueOfCaseInsensitive( rpcError.getTag().toString() );
+
+        this.errorMessage = rpcError.getMessage();
+        this.errorAppTag = rpcError.getApplicationTag();
+
+        String errorInfo = null;
+        if( rpcError.getInfo() == null ) {
+            if( rpcError.getCause() != null ) {
+                errorInfo = toErrorInfo( rpcError.getCause() );
+            }
+            else if( rpcError.getSeverity() != null ) {
+                errorInfo = "<severity>" + rpcError.getSeverity().toString().toLowerCase() +
+                            "</severity>";
+            }
+        }
+        else {
+            errorInfo = rpcError.getInfo();
+        }
+
+        this.errorInfo = errorInfo;
+    }
+
+    public ErrorType getErrorType() {
+        return errorType;
+    }
+
+    public ErrorTag getErrorTag() {
+        return errorTag;
+    }
+
+    public String getErrorInfo() {
+        return errorInfo;
+    }
+
+    public String getErrorAppTag() {
+        return errorAppTag;
+    }
+
+    public String getErrorMessage() {
+        return errorMessage;
+    }
+
+    @Override
+    public String toString() {
+        return "error-type: " + errorType.getErrorTypeTag()
+                + ", error-tag: " + errorTag.getTagValue() + ", "
+                + (errorAppTag != null ? "error-app-tag: " + errorAppTag + ", " : "")
+                + (errorMessage != null ? "error-message: " + errorMessage : "")
+                + (errorInfo != null ? "error-info: " + errorInfo + ", " : "") + "]";
+    }
+
+}
\ No newline at end of file
index e9d489d..ad682bc 100644 (file)
@@ -8,11 +8,19 @@
  */
 package org.opendaylight.controller.sal.restconf.impl;
 
+import com.google.common.base.Objects;
+import com.google.common.base.Preconditions;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+
 import java.net.URI;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -28,15 +36,29 @@ import javax.ws.rs.core.UriInfo;
 import org.apache.commons.lang3.StringUtils;
 import org.opendaylight.controller.md.sal.common.api.TransactionStatus;
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
+import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.api.RestconfService;
 import org.opendaylight.controller.sal.restconf.rpc.impl.BrokerRpcExecutor;
 import org.opendaylight.controller.sal.restconf.rpc.impl.MountPointRpcExecutor;
 import org.opendaylight.controller.sal.restconf.rpc.impl.RpcExecutor;
+import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
+import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper;
+import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
+import org.opendaylight.controller.sal.restconf.impl.EmptyNodeWrapper;
+import org.opendaylight.controller.sal.restconf.impl.IdentityValuesDTO;
+import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
+import org.opendaylight.controller.sal.restconf.impl.NodeWrapper;
+import org.opendaylight.controller.sal.restconf.impl.RestCodec;
+import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper;
+import org.opendaylight.controller.sal.restconf.impl.StructuredData;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter;
 import org.opendaylight.controller.sal.streams.listeners.Notificator;
 import org.opendaylight.controller.sal.streams.websockets.WebSocketServer;
 import org.opendaylight.yangtools.concepts.Codec;
 import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.RpcError;
 import org.opendaylight.yangtools.yang.common.RpcResult;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
@@ -49,7 +71,6 @@ import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.FeatureDefinition;
-import org.opendaylight.yangtools.yang.model.api.GroupingDefinition;
 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
@@ -63,15 +84,6 @@ import org.opendaylight.yangtools.yang.model.util.EmptyType;
 import org.opendaylight.yangtools.yang.parser.builder.impl.ContainerSchemaNodeBuilder;
 import org.opendaylight.yangtools.yang.parser.builder.impl.LeafSchemaNodeBuilder;
 
-import com.google.common.base.Objects;
-import com.google.common.base.Preconditions;
-import com.google.common.base.Predicate;
-import com.google.common.base.Splitter;
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-
-@SuppressWarnings("all")
 public class RestconfImpl implements RestconfService {
     private final static RestconfImpl INSTANCE = new RestconfImpl();
 
@@ -81,26 +93,6 @@ public class RestconfImpl implements RestconfService {
 
     private final static SimpleDateFormat REVISION_FORMAT =  new SimpleDateFormat("yyyy-MM-dd");
 
-    private final static String RESTCONF_MODULE_DRAFT02_REVISION = "2013-10-19";
-
-    private final static String RESTCONF_MODULE_DRAFT02_NAME = "ietf-restconf";
-
-    private final static String RESTCONF_MODULE_DRAFT02_NAMESPACE = "urn:ietf:params:xml:ns:yang:ietf-restconf";
-
-    private final static String RESTCONF_MODULE_DRAFT02_RESTCONF_GROUPING_SCHEMA_NODE = "restconf";
-
-    private final static String RESTCONF_MODULE_DRAFT02_RESTCONF_CONTAINER_SCHEMA_NODE = "restconf";
-
-    private final static String RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE = "modules";
-
-    private final static String RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE = "module";
-
-    private final static String RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE = "streams";
-
-    private final static String RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE = "stream";
-
-    private final static String RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE = "operations";
-
     private final static String SAL_REMOTE_NAMESPACE = "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote";
 
     private final static String SAL_REMOTE_RPC_SUBSRCIBE = "create-data-change-event-subscription";
@@ -129,8 +121,8 @@ public class RestconfImpl implements RestconfService {
         final Module restconfModule = this.getRestconfModule();
 
         final List<Node<?>> modulesAsData = new ArrayList<Node<?>>();
-        final DataSchemaNode moduleSchemaNode =
-                this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE);
+        final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                        restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE);
 
         Set<Module> allModules = this.controllerContext.getAllModules();
         for (final Module module : allModules) {
@@ -138,8 +130,8 @@ public class RestconfImpl implements RestconfService {
             modulesAsData.add(moduleCompositeNode);
         }
 
-        final DataSchemaNode modulesSchemaNode =
-                this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE);
+        final DataSchemaNode modulesSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                   restconfModule, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
         QName qName = modulesSchemaNode.getQName();
         final CompositeNode modulesNode = NodeFactory.createImmutableCompositeNode(qName, null, modulesAsData);
         return new StructuredData(modulesNode, modulesSchemaNode, null);
@@ -151,14 +143,14 @@ public class RestconfImpl implements RestconfService {
 
         final List<Node<?>> streamsAsData = new ArrayList<Node<?>>();
         Module restconfModule = this.getRestconfModule();
-        final DataSchemaNode streamSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE);
+        final DataSchemaNode streamSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                             restconfModule, Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE);
         for (final String streamName : availableStreams) {
             streamsAsData.add(this.toStreamCompositeNode(streamName, streamSchemaNode));
         }
 
-        final DataSchemaNode streamsSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE);
+        final DataSchemaNode streamsSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                     restconfModule, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE);
         QName qName = streamsSchemaNode.getQName();
         final CompositeNode streamsNode = NodeFactory.createImmutableCompositeNode(qName, null, streamsAsData);
         return new StructuredData(streamsNode, streamsSchemaNode, null);
@@ -175,22 +167,22 @@ public class RestconfImpl implements RestconfService {
             modules = this.controllerContext.getAllModules(mountPoint);
         }
         else {
-            throw new ResponseException(Status.BAD_REQUEST,
-                "URI has bad format. If modules behind mount point should be showed, URI has to end with " +
-                ControllerContext.MOUNT);
+            throw new RestconfDocumentedException(
+                    "URI has bad format. If modules behind mount point should be showed, URI has to end with " +
+                    ControllerContext.MOUNT, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         final List<Node<?>> modulesAsData = new ArrayList<Node<?>>();
         Module restconfModule = this.getRestconfModule();
-        final DataSchemaNode moduleSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE);
+        final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                         restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE);
 
         for (final Module module : modules) {
             modulesAsData.add(this.toModuleCompositeNode(module, moduleSchemaNode));
         }
 
-        final DataSchemaNode modulesSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE);
+        final DataSchemaNode modulesSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                  restconfModule, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE);
         QName qName = modulesSchemaNode.getQName();
         final CompositeNode modulesNode = NodeFactory.createImmutableCompositeNode(qName, null, modulesAsData);
         return new StructuredData(modulesNode, modulesSchemaNode, mountPoint);
@@ -212,14 +204,15 @@ public class RestconfImpl implements RestconfService {
         }
 
         if (module == null) {
-            throw new ResponseException(Status.BAD_REQUEST,
+            throw new RestconfDocumentedException(
                     "Module with name '" + moduleNameAndRevision.getLocalName() + "' and revision '" +
-                    moduleNameAndRevision.getRevision() + "' was not found.");
+                    moduleNameAndRevision.getRevision() + "' was not found.",
+                    ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
         }
 
         Module restconfModule = this.getRestconfModule();
-        final DataSchemaNode moduleSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE);
+        final DataSchemaNode moduleSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                                          restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE);
         final CompositeNode moduleNode = this.toModuleCompositeNode(module, moduleSchemaNode);
         return new StructuredData(moduleNode, moduleSchemaNode, mountPoint);
     }
@@ -241,9 +234,9 @@ public class RestconfImpl implements RestconfService {
             modules = this.controllerContext.getAllModules(mountPoint);
         }
         else {
-            throw new ResponseException(Status.BAD_REQUEST,
-                "URI has bad format. If operations behind mount point should be showed, URI has to end with " +
-            ControllerContext.MOUNT);
+            throw new RestconfDocumentedException(
+                    "URI has bad format. If operations behind mount point should be showed, URI has to end with " +
+                    ControllerContext.MOUNT, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         return this.operationsFromModulesToStructuredData(modules, mountPoint);
@@ -253,12 +246,12 @@ public class RestconfImpl implements RestconfService {
                                                                  final MountInstance mountPoint) {
         final List<Node<?>> operationsAsData = new ArrayList<Node<?>>();
         Module restconfModule = this.getRestconfModule();
-        final DataSchemaNode operationsSchemaNode =
-            this.getSchemaNode(restconfModule, RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE);
+        final DataSchemaNode operationsSchemaNode = controllerContext.getRestconfModuleRestConfSchemaNode(
+                              restconfModule, Draft02.RestConfModule.OPERATIONS_CONTAINER_SCHEMA_NODE);
         QName qName = operationsSchemaNode.getQName();
         SchemaPath path = operationsSchemaNode.getPath();
         ContainerSchemaNodeBuilder containerSchemaNodeBuilder =
-                             new ContainerSchemaNodeBuilder(RESTCONF_MODULE_DRAFT02_NAME, 0, qName, path);
+                             new ContainerSchemaNodeBuilder(Draft02.RestConfModule.NAME, 0, qName, path);
         final ContainerSchemaNodeBuilder fakeOperationsSchemaNode = containerSchemaNodeBuilder;
         for (final Module module : modules) {
             Set<RpcDefinition> rpcs = module.getRpcs();
@@ -286,11 +279,11 @@ public class RestconfImpl implements RestconfService {
     }
 
     private Module getRestconfModule() {
-        QName qName = QName.create(RESTCONF_MODULE_DRAFT02_NAMESPACE, RESTCONF_MODULE_DRAFT02_REVISION,
-                                   RESTCONF_MODULE_DRAFT02_NAME);
-        final Module restconfModule = this.controllerContext.findModuleByNameAndRevision(qName);
+        Module restconfModule = controllerContext.getRestconfModule();
         if (restconfModule == null) {
-            throw new ResponseException(Status.INTERNAL_SERVER_ERROR, "Restconf module was not found.");
+            throw new RestconfDocumentedException(
+                    "ietf-restconf module was not found.", ErrorType.APPLICATION,
+                    ErrorTag.OPERATION_NOT_SUPPORTED );
         }
 
         return restconfModule;
@@ -310,8 +303,9 @@ public class RestconfImpl implements RestconfService {
         Iterable<String> split = splitter.split(moduleNameAndRevision);
         final List<String> pathArgs = Lists.<String>newArrayList(split);
         if (pathArgs.size() < 2) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                    "URI has bad format. End of URI should be in format \'moduleName/yyyy-MM-dd\'");
+            throw new RestconfDocumentedException(
+                    "URI has bad format. End of URI should be in format \'moduleName/yyyy-MM-dd\'",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         try {
@@ -321,7 +315,9 @@ public class RestconfImpl implements RestconfService {
             return QName.create(null, moduleRevision, moduleName);
         }
         catch (ParseException e) {
-            throw new ResponseException(Status.BAD_REQUEST, "URI has bad format. It should be \'moduleName/yyyy-MM-dd\'");
+            throw new RestconfDocumentedException(
+                    "URI has bad format. It should be \'moduleName/yyyy-MM-dd\'",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
     }
 
@@ -393,66 +389,6 @@ public class RestconfImpl implements RestconfService {
         return NodeFactory.createImmutableCompositeNode(moduleSchemaNode.getQName(), null, moduleNodeValues);
     }
 
-    private DataSchemaNode getSchemaNode(final Module restconfModule, final String schemaNodeName) {
-        Set<GroupingDefinition> groupings = restconfModule.getGroupings();
-
-        final Predicate<GroupingDefinition> filter = new Predicate<GroupingDefinition>() {
-            @Override
-            public boolean apply(final GroupingDefinition g) {
-                return Objects.equal(g.getQName().getLocalName(),
-                                     RESTCONF_MODULE_DRAFT02_RESTCONF_GROUPING_SCHEMA_NODE);
-            }
-        };
-
-        Iterable<GroupingDefinition> filteredGroups = Iterables.filter(groupings, filter);
-
-        final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null);
-
-        List<DataSchemaNode> instanceDataChildrenByName =
-                this.controllerContext.findInstanceDataChildrenByName(restconfGrouping,
-                                                            RESTCONF_MODULE_DRAFT02_RESTCONF_CONTAINER_SCHEMA_NODE);
-        final DataSchemaNode restconfContainer = Iterables.getFirst(instanceDataChildrenByName, null);
-
-        if (Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE)) {
-            List<DataSchemaNode> instances =
-                    this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
-                                                    RESTCONF_MODULE_DRAFT02_OPERATIONS_CONTAINER_SCHEMA_NODE);
-            return Iterables.getFirst(instances, null);
-        }
-        else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE)) {
-            List<DataSchemaNode> instances =
-                    this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
-                                                   RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE);
-            return Iterables.getFirst(instances, null);
-        }
-        else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE)) {
-            List<DataSchemaNode> instances =
-                    this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
-                                                   RESTCONF_MODULE_DRAFT02_STREAMS_CONTAINER_SCHEMA_NODE);
-            final DataSchemaNode modules = Iterables.getFirst(instances, null);
-            instances = this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) modules),
-                                                               RESTCONF_MODULE_DRAFT02_STREAM_LIST_SCHEMA_NODE);
-            return Iterables.getFirst(instances, null);
-        }
-        else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE)) {
-            List<DataSchemaNode> instances =
-                    this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
-                                                         RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE);
-            return Iterables.getFirst(instances, null);
-        }
-        else if(Objects.equal(schemaNodeName, RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE)) {
-            List<DataSchemaNode> instances =
-                    this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) restconfContainer),
-                                                         RESTCONF_MODULE_DRAFT02_MODULES_CONTAINER_SCHEMA_NODE);
-            final DataSchemaNode modules = Iterables.getFirst(instances, null);
-            instances = this.controllerContext.findInstanceDataChildrenByName(((DataNodeContainer) modules),
-                                                                 RESTCONF_MODULE_DRAFT02_MODULE_LIST_SCHEMA_NODE);
-            return Iterables.getFirst(instances, null);
-        }
-
-        return null;
-    }
-
     @Override
     public Object getRoot() {
         return null;
@@ -480,8 +416,9 @@ public class RestconfImpl implements RestconfService {
         final Object pathValue = pathNode == null ? null : pathNode.getValue();
 
         if (!(pathValue instanceof InstanceIdentifier)) {
-            throw new ResponseException(Status.INTERNAL_SERVER_ERROR,
-                                         "Instance identifier was not normalized correctly.");
+            throw new RestconfDocumentedException(
+                    "Instance identifier was not normalized correctly.",
+                    ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED );
         }
 
         final InstanceIdentifier pathIdentifier = ((InstanceIdentifier) pathValue);
@@ -492,8 +429,9 @@ public class RestconfImpl implements RestconfService {
         }
 
         if (Strings.isNullOrEmpty(streamName)) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                     "Path is empty or contains data node which is not Container or List build-in type.");
+            throw new RestconfDocumentedException(
+                    "Path is empty or contains data node which is not Container or List build-in type.",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         final SimpleNode<String> streamNameNode = NodeFactory.<String>createImmutableSimpleNode(
@@ -514,8 +452,8 @@ public class RestconfImpl implements RestconfService {
     @Override
     public StructuredData invokeRpc(final String identifier, final String noPayload) {
         if (StringUtils.isNotBlank(noPayload)) {
-            throw new ResponseException(
-                    Status.UNSUPPORTED_MEDIA_TYPE, "Content-Type contains unsupported Media Type.");
+            throw new RestconfDocumentedException(
+                    "Content must be empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
         final RpcExecutor rpc = resolveIdentifierInInvokeRpc(identifier);
         return callRpc(rpc, null);
@@ -540,7 +478,8 @@ public class RestconfImpl implements RestconfService {
                     .format("Identifier %n%s%ncan\'t contain slash "
                             + "character (/).%nIf slash is part of identifier name then use %%2F placeholder.",
                             identifier);
-            throw new ResponseException(Status.NOT_FOUND, slashErrorMsg);
+            throw new RestconfDocumentedException(
+                    slashErrorMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         } else {
             identifierEncoded = identifier;
         }
@@ -549,7 +488,8 @@ public class RestconfImpl implements RestconfService {
         RpcDefinition rpc = controllerContext.getRpcDefinition(identifierDecoded);
 
         if (rpc == null) {
-            throw new ResponseException(Status.NOT_FOUND, "RPC does not exist.");
+            throw new RestconfDocumentedException(
+                    "RPC does not exist.", ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT );
         }
 
         if (mountPoint == null) {
@@ -562,7 +502,8 @@ public class RestconfImpl implements RestconfService {
 
     private StructuredData callRpc(final RpcExecutor rpcExecutor, final CompositeNode payload) {
         if (rpcExecutor == null) {
-            throw new ResponseException(Status.NOT_FOUND, "RPC does not exist.");
+            throw new RestconfDocumentedException(
+                    "RPC does not exist.", ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT );
         }
 
         CompositeNode rpcRequest = null;
@@ -595,9 +536,20 @@ public class RestconfImpl implements RestconfService {
 
     private void checkRpcSuccessAndThrowException(RpcResult<CompositeNode> rpcResult) {
         if (rpcResult.isSuccessful() == false) {
-            //TODO: Get smart about what error code we are return (Future Bug coming)
-            throw new ResponseException(Status.INTERNAL_SERVER_ERROR,
-                        "The operation was not successful and there were no RPC errors returned");
+
+            Collection<RpcError> rpcErrors = rpcResult.getErrors();
+            if( rpcErrors == null || rpcErrors.isEmpty() ) {
+                throw new RestconfDocumentedException(
+                    "The operation was not successful and there were no RPC errors returned",
+                    ErrorType.RPC, ErrorTag.OPERATION_FAILED );
+            }
+
+            List<RestconfError> errorList = Lists.newArrayList();
+            for( RpcError rpcError: rpcErrors ) {
+                errorList.add( new RestconfError( rpcError ) );
+            }
+
+            throw new RestconfDocumentedException( errorList );
         }
     }
 
@@ -647,7 +599,7 @@ public class RestconfImpl implements RestconfService {
             }
         }
         catch( Exception e ) {
-            throw new ResponseException( e, "Error updating data" );
+            throw new RestconfDocumentedException( "Error updating data", e );
         }
 
         if( status.getResult() == TransactionStatus.COMMITED )
@@ -660,8 +612,9 @@ public class RestconfImpl implements RestconfService {
     public Response createConfigurationData(final String identifier, final CompositeNode payload) {
         URI payloadNS = this.namespace(payload);
         if (payloadNS == null) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                    "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)");
+            throw new RestconfDocumentedException(
+                 "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)",
+                 ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE );
         }
 
         InstanceIdWithSchemaNode iiWithData = null;
@@ -670,9 +623,10 @@ public class RestconfImpl implements RestconfService {
              // payload represents mount point data and URI represents path to the mount point
 
             if (this.endsWithMountPoint(identifier)) {
-                throw new ResponseException(Status.BAD_REQUEST,
-                            "URI has bad format. URI should be without \"" + ControllerContext.MOUNT +
-                            "\" for POST operation.");
+                throw new RestconfDocumentedException(
+                        "URI has bad format. URI should be without \"" + ControllerContext.MOUNT +
+                        "\" for POST operation.",
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
 
             final String completeIdentifier = this.addMountPointIdentifier(identifier);
@@ -687,8 +641,9 @@ public class RestconfImpl implements RestconfService {
             MountInstance mountPoint = incompleteInstIdWithData.getMountPoint();
             final Module module = this.findModule(mountPoint, payload);
             if (module == null) {
-                throw new ResponseException(Status.BAD_REQUEST,
-                                            "Module was not found for \"" + payloadNS + "\"");
+                throw new RestconfDocumentedException(
+                        "Module was not found for \"" + payloadNS + "\"",
+                        ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
             }
 
             String payloadName = this.getName(payload);
@@ -714,9 +669,8 @@ public class RestconfImpl implements RestconfService {
                 status = future == null ? null : future.get();
             }
         }
-        catch( ResponseException e ){ throw e; }
         catch( Exception e ) {
-            throw new ResponseException( e, "Error creating data" );
+            throw new RestconfDocumentedException( "Error creating data", e );
         }
 
         if (status == null) {
@@ -733,14 +687,16 @@ public class RestconfImpl implements RestconfService {
     public Response createConfigurationData(final CompositeNode payload) {
         URI payloadNS = this.namespace(payload);
         if (payloadNS == null) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                    "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)");
+            throw new RestconfDocumentedException(
+                "Data has bad format. Root element node must have namespace (XML format) or module name(JSON format)",
+                ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE );
         }
 
         final Module module = this.findModule(null, payload);
         if (module == null) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                    "Data has bad format. Root element node has incorrect namespace (XML format) or module name(JSON format)");
+            throw new RestconfDocumentedException(
+                    "Data has bad format. Root element node has incorrect namespace (XML format) or module name(JSON format)",
+                    ErrorType.PROTOCOL, ErrorTag.UNKNOWN_NAMESPACE );
         }
 
         String payloadName = this.getName(payload);
@@ -764,9 +720,8 @@ public class RestconfImpl implements RestconfService {
                 status = future == null ? null : future.get();
             }
         }
-       catch( ResponseException e ){ throw e; }
         catch( Exception e ) {
-            throw new ResponseException( e, "Error creating data" );
+            throw new RestconfDocumentedException( "Error creating data", e );
         }
 
         if (status == null) {
@@ -795,7 +750,7 @@ public class RestconfImpl implements RestconfService {
             }
         }
         catch( Exception e ) {
-            throw new ResponseException( e, "Error creating data" );
+            throw new RestconfDocumentedException( "Error creating data", e );
         }
 
         if( status.getResult() == TransactionStatus.COMMITED )
@@ -808,12 +763,14 @@ public class RestconfImpl implements RestconfService {
     public Response subscribeToStream(final String identifier, final UriInfo uriInfo) {
         final String streamName = Notificator.createStreamNameFromUri(identifier);
         if (Strings.isNullOrEmpty(streamName)) {
-            throw new ResponseException(Status.BAD_REQUEST, "Stream name is empty.");
+            throw new RestconfDocumentedException(
+                    "Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         final ListenerAdapter listener = Notificator.getListenerFor(streamName);
         if (listener == null) {
-            throw new ResponseException(Status.BAD_REQUEST, "Stream was not found.");
+            throw new RestconfDocumentedException(
+                    "Stream was not found.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT );
         }
 
         broker.registerToListenDataChanges(listener);
@@ -917,9 +874,10 @@ public class RestconfImpl implements RestconfService {
             }
 
             if (dataNodeKeyValueObject == null) {
-                throw new ResponseException(Status.BAD_REQUEST,
-                            "Data contains list \"" + dataNode.getNodeType().getLocalName() +
-                            "\" which does not contain key: \"" + key.getLocalName() + "\"");
+                throw new RestconfDocumentedException(
+                        "Data contains list \"" + dataNode.getNodeType().getLocalName() +
+                        "\" which does not contain key: \"" + key.getLocalName() + "\"",
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
 
             keyValues.put(key, dataNodeKeyValueObject);
@@ -954,14 +912,16 @@ public class RestconfImpl implements RestconfService {
         if (schema == null) {
             QName nodeType = node == null ? null : node.getNodeType();
             String localName = nodeType == null ? null : nodeType.getLocalName();
-            String _plus = ("Data schema node was not found for " + localName);
-            throw new ResponseException(Status.INTERNAL_SERVER_ERROR,
-                                        "Data schema node was not found for " + localName );
+
+            throw new RestconfDocumentedException(
+                    "Data schema node was not found for " + localName,
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         if (!(schema instanceof DataNodeContainer)) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                                        "Root element has to be container or list yang datatype.");
+            throw new RestconfDocumentedException(
+                    "Root element has to be container or list yang datatype.",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         if ((node instanceof CompositeNodeWrapper)) {
@@ -970,8 +930,9 @@ public class RestconfImpl implements RestconfService {
                 try {
                     this.normalizeNode(((CompositeNodeWrapper) node), schema, null, mountPoint);
                 }
-                catch (NumberFormatException e) {
-                    throw new ResponseException(Status.BAD_REQUEST, e.getMessage());
+                catch (IllegalArgumentException e) {
+                    throw new RestconfDocumentedException(
+                                    e.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
                 }
             }
 
@@ -985,9 +946,10 @@ public class RestconfImpl implements RestconfService {
                                final DataSchemaNode schema, final QName previousAugment,
                                final MountInstance mountPoint) {
         if (schema == null) {
-            throw new ResponseException(Status.BAD_REQUEST,
-                                        "Data has bad format.\n\"" + nodeBuilder.getLocalName() +
-                                        "\" does not exist in yang schema.");
+            throw new RestconfDocumentedException(
+                    "Data has bad format.\n\"" + nodeBuilder.getLocalName() +
+                    "\" does not exist in yang schema.",
+                    ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
         }
 
         QName currentAugment = null;
@@ -997,13 +959,14 @@ public class RestconfImpl implements RestconfService {
         else {
             currentAugment = this.normalizeNodeName(nodeBuilder, schema, previousAugment, mountPoint);
             if (nodeBuilder.getQname() == null) {
-                throw new ResponseException(Status.BAD_REQUEST,
+                throw new RestconfDocumentedException(
                         "Data has bad format.\nIf data is in XML format then namespace for \"" +
                         nodeBuilder.getLocalName() +
                         "\" should be \"" + schema.getQName().getNamespace() + "\".\n" +
                         "If data is in JSON format then module name for \"" + nodeBuilder.getLocalName() +
                          "\" should be corresponding to namespace \"" +
-                        schema.getQName().getNamespace() + "\".");
+                        schema.getQName().getNamespace() + "\".",
+                        ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
             }
         }
 
@@ -1021,11 +984,12 @@ public class RestconfImpl implements RestconfService {
                                .append("\n");
                     }
 
-                    throw new ResponseException(Status.BAD_REQUEST,
+                    throw new RestconfDocumentedException(
                                  "Node \"" + child.getLocalName() +
                                  "\" is added as augment from more than one module. " +
                                  "Therefore node must have namespace (XML format) or module name (JSON format)." +
-                                 "\nThe node is added as augment from modules with namespaces:\n" + builder);
+                                 "\nThe node is added as augment from modules with namespaces:\n" + builder,
+                                 ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
                 }
 
                 boolean rightNodeSchemaFound = false;
@@ -1041,8 +1005,9 @@ public class RestconfImpl implements RestconfService {
                 }
 
                 if (!rightNodeSchemaFound) {
-                    throw new ResponseException(Status.BAD_REQUEST,
-                                      "Schema node \"" + child.getLocalName() + "\" was not found in module.");
+                    throw new RestconfDocumentedException(
+                               "Schema node \"" + child.getLocalName() + "\" was not found in module.",
+                               ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT );
                 }
             }
 
@@ -1057,9 +1022,10 @@ public class RestconfImpl implements RestconfService {
                     }
 
                     if (!foundKey) {
-                        throw new ResponseException(Status.BAD_REQUEST,
+                        throw new RestconfDocumentedException(
                                        "Missing key in URI \"" + listKey.getLocalName() +
-                                       "\" of list \"" + schema.getQName().getLocalName() + "\"");
+                                       "\" of list \"" + schema.getQName().getLocalName() + "\"",
+                                       ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
                     }
                 }
             }
index 446d07d..0bc8428 100644 (file)
@@ -7,6 +7,15 @@
 */
 package org.opendaylight.controller.sal.restconf.rpc.impl;
 
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 
 public abstract class AbstractRpcExecutor implements RpcExecutor {
@@ -20,4 +29,41 @@ public abstract class AbstractRpcExecutor implements RpcExecutor {
     public RpcDefinition getRpcDefinition() {
         return rpcDef;
     }
+
+    protected RpcResult<CompositeNode> getRpcResult(
+                                            Future<RpcResult<CompositeNode>> fromFuture ) {
+        try {
+            return fromFuture.get();
+        }
+        catch( InterruptedException e ) {
+            throw new RestconfDocumentedException(
+                        "The operation was interrupted while executing and did not complete.",
+                        ErrorType.RPC, ErrorTag.PARTIAL_OPERATION );
+        }
+        catch( ExecutionException e ) {
+            Throwable cause = e.getCause();
+            if( cause instanceof CancellationException ) {
+                throw new RestconfDocumentedException(
+                        "The operation was cancelled while executing.",
+                        ErrorType.RPC, ErrorTag.PARTIAL_OPERATION );
+            }
+            else if( cause != null ){
+                while( cause.getCause() != null ) {
+                    cause = cause.getCause();
+                }
+
+                if( cause instanceof IllegalArgumentException ) {
+                    throw new RestconfDocumentedException(
+                            cause.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE );
+                }
+
+                throw new RestconfDocumentedException(
+                       "The operation encountered an unexpected error while executing.", cause );
+            }
+            else {
+                throw new RestconfDocumentedException(
+                        "The operation encountered an unexpected error while executing.", e );
+            }
+        }
+    }
 }
\ No newline at end of file
index 0748832..249b657 100644 (file)
@@ -23,6 +23,6 @@ public class BrokerRpcExecutor extends AbstractRpcExecutor {
 
     @Override
     public RpcResult<CompositeNode> invokeRpc(CompositeNode rpcRequest) {
-        return broker.invokeRpc( getRpcDefinition().getQName(), rpcRequest );
+        return getRpcResult( broker.invokeRpc( getRpcDefinition().getQName(), rpcRequest ) );
     }
 }
\ No newline at end of file
index b56db21..da19a00 100644 (file)
@@ -7,18 +7,13 @@
 */
 package org.opendaylight.controller.sal.restconf.rpc.impl;
 
-import java.util.concurrent.ExecutionException;
-
-import javax.ws.rs.core.Response.Status;
-
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.yangtools.yang.common.RpcResult;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 
 import com.google.common.base.Preconditions;
-import com.google.common.util.concurrent.ListenableFuture;
 
 /**
  * Provides an implementation which invokes rpc methods via a mounted yang data model.
@@ -35,14 +30,8 @@ public class MountPointRpcExecutor extends AbstractRpcExecutor {
     }
 
     @Override
-    public RpcResult<CompositeNode> invokeRpc( CompositeNode rpcRequest ) throws ResponseException {
-        ListenableFuture<RpcResult<CompositeNode>> rpcFuture =
-                mountPoint.rpc( getRpcDefinition().getQName(), rpcRequest);
-        try {
-            return rpcFuture.get();
-        } catch (InterruptedException | ExecutionException e) {
-            throw new ResponseException(Status.INTERNAL_SERVER_ERROR,
-                                        e.getCause().getMessage() );
-        }
+    public RpcResult<CompositeNode> invokeRpc( CompositeNode rpcRequest )
+                                                   throws RestconfDocumentedException {
+        return getRpcResult( mountPoint.rpc( getRpcDefinition().getQName(), rpcRequest ) );
     }
 }
\ No newline at end of file
index 415d58e..3c70cca 100644 (file)
@@ -8,7 +8,6 @@
 package org.opendaylight.controller.sal.restconf.impl.json.to.cnsn.test;
 
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
@@ -16,13 +15,11 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-import javax.ws.rs.WebApplicationException;
-
 import org.junit.Ignore;
 import org.junit.Test;
 import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider;
 import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.controller.sal.restconf.impl.test.TestUtils;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
@@ -32,8 +29,6 @@ import org.opendaylight.yangtools.yang.model.api.Module;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.gson.JsonSyntaxException;
-
 public class JsonToCnSnTest {
 
     private static final Logger LOG = LoggerFactory.getLogger(JsonToCnSnTest.class);
@@ -116,44 +111,39 @@ public class JsonToCnSnTest {
 
     @Test
     public void incorrectTopLevelElementsTest() {
-        Throwable cause1 = null;
+        RestconfDocumentedException cause1 = null;
         try {
-            TestUtils
-            .readInputToCnSn("/json-to-cnsn/wrong-top-level1.json", true, JsonToCompositeNodeProvider.INSTANCE);
-        } catch (WebApplicationException e) {
+            TestUtils.readInputToCnSn("/json-to-cnsn/wrong-top-level1.json", true, JsonToCompositeNodeProvider.INSTANCE);
+        } catch (RestconfDocumentedException e) {
             cause1 = e;
         }
 
         assertNotNull(cause1);
-        assertTrue(cause1
-                .getCause()
-                .getMessage()
-                .contains(
-                        "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet."));
+        assertTrue(cause1.getErrors().get( 0 ).getErrorMessage().contains(
+            "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet."));
 
-        Throwable cause2 = null;
+        RestconfDocumentedException cause2 = null;
         try {
             TestUtils
-            .readInputToCnSn("/json-to-cnsn/wrong-top-level2.json", true, JsonToCompositeNodeProvider.INSTANCE);
-        } catch (WebApplicationException e) {
+                    .readInputToCnSn("/json-to-cnsn/wrong-top-level2.json", true, JsonToCompositeNodeProvider.INSTANCE);
+        } catch (RestconfDocumentedException e) {
             cause2 = e;
         }
         assertNotNull(cause2);
-        assertTrue(cause2.getCause().getMessage().contains("Json Object should contain one element"));
+        assertTrue(cause2.getErrors().get( 0 ).getErrorMessage().contains(
+                                                     "Json Object should contain one element"));
 
-        Throwable cause3 = null;
+        RestconfDocumentedException cause3 = null;
         try {
             TestUtils
-            .readInputToCnSn("/json-to-cnsn/wrong-top-level3.json", true, JsonToCompositeNodeProvider.INSTANCE);
-        } catch (WebApplicationException e) {
+
+                    .readInputToCnSn("/json-to-cnsn/wrong-top-level3.json", true, JsonToCompositeNodeProvider.INSTANCE);
+        } catch (RestconfDocumentedException e) {
             cause3 = e;
         }
         assertNotNull(cause3);
-        assertTrue(cause3
-                .getCause()
-                .getMessage()
-                .contains(
-                        "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet."));
+        assertTrue(cause3.getErrors().get( 0 ).getErrorMessage().contains(
+            "First element in Json Object has to be \"Object\" or \"Array with one Object element\". Other scenarios are not supported yet."));
 
     }
 
@@ -178,8 +168,8 @@ public class JsonToCnSnTest {
         String reason = null;
         try {
             TestUtils.readInputToCnSn("/json-to-cnsn/empty-data1.json", true, JsonToCompositeNodeProvider.INSTANCE);
-        } catch (JsonSyntaxException e) {
-            reason = e.getMessage();
+        } catch (RestconfDocumentedException e) {
+            reason = e.getErrors().get( 0 ).getErrorMessage();
         }
 
         assertTrue(reason.contains("Expected value at line"));
@@ -278,15 +268,9 @@ public class JsonToCnSnTest {
     @Ignore
     @Test
     public void loadDataAugmentedSchemaMoreEqualNamesTest() {
-        boolean exceptionCaught = false;
-        try {
-            loadAndNormalizeData("/common/augment/json/dataa.json", "/common/augment/yang", "cont", "main");
-            loadAndNormalizeData("/common/augment/json/datab.json", "/common/augment/yang", "cont", "main");
-        } catch (ResponseException e) {
-            exceptionCaught = true;
-        }
+        loadAndNormalizeData("/common/augment/json/dataa.json", "/common/augment/yang", "cont", "main");
+        loadAndNormalizeData("/common/augment/json/datab.json", "/common/augment/yang", "cont", "main");
 
-        assertFalse(exceptionCaught);
     }
 
     private void simpleTest(final String jsonPath, final String yangPath, final String topLevelElementName, final String namespace,
@@ -399,8 +383,8 @@ public class JsonToCnSnTest {
         try {
             TestUtils.readInputToCnSn("/json-to-cnsn/unsupported-json-format.json", true,
                     JsonToCompositeNodeProvider.INSTANCE);
-        } catch (WebApplicationException e) {
-            exceptionMessage = e.getCause().getMessage();
+        } catch (RestconfDocumentedException e) {
+            exceptionMessage = e.getErrors().get( 0 ).getErrorMessage();
         }
         assertTrue(exceptionMessage.contains("Root element of Json has to be Object"));
     }
index 18199de..ddab700 100644 (file)
@@ -9,6 +9,7 @@
 package org.opendaylight.controller.sal.restconf.impl.test;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertSame;
 import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
@@ -19,8 +20,6 @@ import static org.mockito.Mockito.when;
 import java.util.Map;
 import java.util.concurrent.Future;
 
-import javax.ws.rs.core.Response.Status;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.mockito.InOrder;
@@ -34,7 +33,8 @@ import org.opendaylight.controller.sal.core.api.data.DataModificationTransaction
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
 import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider;
 import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError;
 import org.opendaylight.controller.sal.streams.listeners.ListenerAdapter;
 import org.opendaylight.controller.sal.streams.listeners.Notificator;
 import org.opendaylight.yangtools.concepts.ListenerRegistration;
@@ -120,7 +120,7 @@ public class BrokerFacadeTest {
         assertSame( "readOperationalDataBehindMountPoint", dataNode, actualNode );
     }
 
-    @Test(expected=ResponseException.class)
+    @Test(expected=RestconfDocumentedException.class)
     public void testReadOperationalDataWithNoDataBroker() {
         brokerFacade.setDataService( null );
 
@@ -129,26 +129,19 @@ public class BrokerFacadeTest {
 
     @SuppressWarnings("unchecked")
     @Test
-    public void testInvokeRpc() {
+    public void testInvokeRpc() throws Exception {
         RpcResult<CompositeNode> expResult = mock( RpcResult.class );
         Future<RpcResult<CompositeNode>> future = Futures.immediateFuture( expResult );
         when( mockConsumerSession.rpc( qname, dataNode ) ).thenReturn( future );
 
-        RpcResult<CompositeNode> actualResult = brokerFacade.invokeRpc( qname, dataNode );
+        Future<RpcResult<CompositeNode>> actualFuture = brokerFacade.invokeRpc( qname, dataNode );
+        assertNotNull( "Future is null", actualFuture );
+        RpcResult<CompositeNode> actualResult = actualFuture.get();
 
         assertSame( "invokeRpc", expResult, actualResult );
     }
 
-    @Test(expected=ResponseException.class)
-    public void testInvokeRpcWithException() {
-        Exception mockEx = new Exception( "mock" );
-        Future<RpcResult<CompositeNode>> future = Futures.immediateFailedFuture( mockEx );
-        when( mockConsumerSession.rpc( qname, dataNode ) ).thenReturn( future );
-
-        brokerFacade.invokeRpc( qname, dataNode );
-    }
-
-    @Test(expected=ResponseException.class)
+    @Test(expected=RestconfDocumentedException.class)
     public void testInvokeRpcWithNoConsumerSession() {
         brokerFacade.setContext( null );
 
@@ -218,7 +211,7 @@ public class BrokerFacadeTest {
         inOrder.verify( mockTransaction ).commit();
     }
 
-    @Test(expected=ResponseException.class)
+    @Test(expected=RestconfDocumentedException.class)
     public void testCommitConfigurationDataPostAlreadyExists() {
         when( dataBroker.beginTransaction() ).thenReturn( mockTransaction );
         mockTransaction.putConfigurationData( instanceID, dataNode );
@@ -226,10 +219,10 @@ public class BrokerFacadeTest {
             .thenReturn( dataNode );
         try {
             brokerFacade.commitConfigurationDataPost( instanceID, dataNode );
-        } catch (ResponseException e) {
-            assertEquals("Unexpect Exception Status -> "
-                    + "http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48",
-                    (e.getResponse().getStatus()), Status.CONFLICT.getStatusCode());
+        }
+        catch (RestconfDocumentedException e) {
+            assertEquals("getErrorTag",
+                    RestconfError.ErrorTag.DATA_EXISTS, e.getErrors().get( 0 ).getErrorTag());
             throw e;
         }
     }
@@ -259,7 +252,7 @@ public class BrokerFacadeTest {
         inOrder.verify( mockTransaction ).commit();
     }
 
-    @Test(expected=ResponseException.class)
+    @Test(expected=RestconfDocumentedException.class)
     public void testCommitConfigurationDataPostBehindMountPointAlreadyExists() {
 
         when( mockMountInstance.beginTransaction() ).thenReturn( mockTransaction );
@@ -269,10 +262,10 @@ public class BrokerFacadeTest {
         try {
             brokerFacade.commitConfigurationDataPostBehindMountPoint( mockMountInstance,
                     instanceID, dataNode );
-        } catch (ResponseException e) {
-            assertEquals("Unexpect Exception Status -> "
-                    + "http://tools.ietf.org/html/draft-bierman-netconf-restconf-03#page-48",
-                    e.getResponse().getStatus(), Status.CONFLICT.getStatusCode());
+        }
+        catch (RestconfDocumentedException e) {
+            assertEquals("getErrorTag",
+                    RestconfError.ErrorTag.DATA_EXISTS, e.getErrors().get( 0 ).getErrorTag());
             throw e;
         }
     }
index 767aaf3..51687e2 100644 (file)
@@ -14,6 +14,7 @@ import org.glassfish.jersey.test.JerseyTest;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider;
+import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider;
 import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider;
@@ -46,6 +47,7 @@ public class CodecsExceptionsCatchingTest extends JerseyTest {
         resourceConfig = resourceConfig.registerInstances(restConf, StructuredDataToXmlProvider.INSTANCE,
                 StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE,
                 JsonToCompositeNodeProvider.INSTANCE);
+        resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class );
         return resourceConfig;
     }
 
index 018a235..c0c86c3 100644 (file)
@@ -22,28 +22,30 @@ import static org.mockito.Mockito.when;
 import java.io.FileNotFoundException;
 import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
-import javax.ws.rs.core.Response.Status;
-
 import org.junit.Before;
 import org.junit.BeforeClass;
 import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
+import org.opendaylight.controller.sal.common.util.RpcErrors;
+import org.opendaylight.controller.sal.common.util.Rpcs;
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
 import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
 import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
 import org.opendaylight.controller.sal.restconf.impl.RestconfImpl;
 import org.opendaylight.controller.sal.restconf.impl.StructuredData;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorSeverity;
 import org.opendaylight.yangtools.yang.common.RpcResult;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 import org.opendaylight.yangtools.yang.data.api.ModifyAction;
@@ -54,6 +56,8 @@ import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
+import com.google.common.base.Optional;
+import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 
 public class InvokeRpcMethodTest {
@@ -61,13 +65,6 @@ public class InvokeRpcMethodTest {
     private RestconfImpl restconfImpl = null;
     private static ControllerContext controllerContext = null;
 
-    private class AnswerImpl implements Answer<RpcResult<CompositeNode>> {
-        @Override
-        public RpcResult<CompositeNode> answer(final InvocationOnMock invocation) throws Throwable {
-            CompositeNode compNode = (CompositeNode) invocation.getArguments()[1];
-            return new DummyRpcResult.Builder<CompositeNode>().result(compNode).isSuccessful(true).build();
-        }
-    }
 
     @BeforeClass
     public static void init() throws FileNotFoundException {
@@ -111,9 +108,13 @@ public class InvokeRpcMethodTest {
         restconf.setBroker(mockedBrokerFacade);
         restconf.setControllerContext(contContext);
 
-        when(mockedBrokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))).thenAnswer(new AnswerImpl());
+        CompositeNode payload = preparePayload();
+
+        when(mockedBrokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class)))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture(
+                                               Rpcs.<CompositeNode>getRpcResult( true ) ) );
 
-        StructuredData structData = restconf.invokeRpc("invoke-rpc-module:rpc-test", preparePayload());
+        StructuredData structData = restconf.invokeRpc("invoke-rpc-module:rpc-test", payload);
         assertTrue(structData == null);
 
     }
@@ -131,74 +132,90 @@ public class InvokeRpcMethodTest {
 
     @Test
     public void testInvokeRpcWithNoPayloadRpc_FailNoErrors() {
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(false);
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( false );
 
-        ArgumentCaptor<CompositeNode> payload = ArgumentCaptor
-                .forClass(CompositeNode.class);
         BrokerFacade brokerFacade = mock(BrokerFacade.class);
-        when(
-                brokerFacade.invokeRpc(
-                        eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
-                        payload.capture())).thenReturn(rpcResult);
+        when( brokerFacade.invokeRpc(
+                 eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
+                 any(CompositeNode.class)))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ) );
 
         restconfImpl.setBroker(brokerFacade);
 
         try {
             restconfImpl.invokeRpc("toaster:cancel-toast", "");
             fail("Expected an exception to be thrown.");
-        } catch (ResponseException e) {
-            assertEquals(e.getMessage(),
-                    Status.INTERNAL_SERVER_ERROR.getStatusCode(), e
-                    .getResponse().getStatus());
+        }
+        catch (RestconfDocumentedException e) {
+            verifyRestconfDocumentedException( e, 0, ErrorType.RPC, ErrorTag.OPERATION_FAILED,
+                                               Optional.<String>absent(), Optional.<String>absent() );
         }
     }
 
-    @Test
-    public void testInvokeRpcWithNoPayloadRpc_FailWithRpcError() {
-        List<RpcError> rpcErrors = new LinkedList<RpcError>();
+    void verifyRestconfDocumentedException( final RestconfDocumentedException e, final int index,
+                                            final ErrorType expErrorType, final ErrorTag expErrorTag,
+                                            final Optional<String> expErrorMsg,
+                                            final Optional<String> expAppTag ) {
+        RestconfError actual = null;
+        try {
+            actual = e.getErrors().get( index );
+        }
+        catch( ArrayIndexOutOfBoundsException ex ) {
+            fail( "RestconfError not found at index " + index );
+        }
+
+        assertEquals( "getErrorType", expErrorType, actual.getErrorType() );
+        assertEquals( "getErrorTag", expErrorTag, actual.getErrorTag() );
+        assertNotNull( "getErrorMessage is null", actual.getErrorMessage() );
+
+        if( expErrorMsg.isPresent() ) {
+            assertEquals( "getErrorMessage", expErrorMsg.get(), actual.getErrorMessage() );
+        }
 
-        RpcError unknownError = mock(RpcError.class);
-        when( unknownError.getTag() ).thenReturn( "bogusTag" );
-        rpcErrors.add( unknownError );
+        if( expAppTag.isPresent() ) {
+            assertEquals( "getErrorAppTag", expAppTag.get(), actual.getErrorAppTag() );
+        }
+    }
 
-        RpcError knownError = mock( RpcError.class );
-        when( knownError.getTag() ).thenReturn( "in-use" );
-        rpcErrors.add( knownError );
+    @Test
+    public void testInvokeRpcWithNoPayloadRpc_FailWithRpcError() {
+        List<RpcError> rpcErrors = Arrays.asList(
+            RpcErrors.getRpcError( null, "bogusTag", null, ErrorSeverity.ERROR, "foo",
+                                   RpcError.ErrorType.TRANSPORT, null ),
+            RpcErrors.getRpcError( "app-tag", "in-use", null, ErrorSeverity.WARNING, "bar",
+                                   RpcError.ErrorType.RPC, null ));
 
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(false);
-        when(rpcResult.getErrors()).thenReturn( rpcErrors  );
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( false, rpcErrors );
 
-        ArgumentCaptor<CompositeNode> payload = ArgumentCaptor
-                .forClass(CompositeNode.class);
         BrokerFacade brokerFacade = mock(BrokerFacade.class);
-        when(
-                brokerFacade.invokeRpc(
-                        eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
-                        payload.capture())).thenReturn(rpcResult);
+        when( brokerFacade.invokeRpc(
+                 eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
+                 any(CompositeNode.class)))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ) );
 
         restconfImpl.setBroker(brokerFacade);
 
         try {
             restconfImpl.invokeRpc("toaster:cancel-toast", "");
             fail("Expected an exception to be thrown.");
-        } catch (ResponseException e) {
-            //TODO: Change to a 409 in the future - waiting on additional BUG to enhance this.
-            assertEquals(e.getMessage(), 500, e.getResponse().getStatus());
+        }
+        catch (RestconfDocumentedException e) {
+            verifyRestconfDocumentedException( e, 0, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED,
+                                               Optional.of( "foo" ), Optional.<String>absent() );
+            verifyRestconfDocumentedException( e, 1, ErrorType.RPC, ErrorTag.IN_USE,
+                                               Optional.of( "bar" ), Optional.of( "app-tag" ) );
         }
     }
 
     @Test
     public void testInvokeRpcWithNoPayload_Success() {
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(true);
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( true );
 
         BrokerFacade brokerFacade = mock(BrokerFacade.class);
-        when(
-                brokerFacade.invokeRpc(
-                        eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
-                        any( CompositeNode.class ))).thenReturn(rpcResult);
+        when( brokerFacade.invokeRpc(
+                 eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)cancel-toast")),
+                 any( CompositeNode.class )))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ) );
 
         restconfImpl.setBroker(brokerFacade);
 
@@ -213,10 +230,9 @@ public class InvokeRpcMethodTest {
         try {
             restconfImpl.invokeRpc("toaster:cancel-toast", " a payload ");
             fail("Expected an exception");
-        } catch (ResponseException e) {
-            assertEquals(e.getMessage(),
-                    Status.UNSUPPORTED_MEDIA_TYPE.getStatusCode(), e
-                    .getResponse().getStatus());
+        } catch (RestconfDocumentedException e) {
+            verifyRestconfDocumentedException( e, 0, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
+                                               Optional.<String>absent(), Optional.<String>absent() );
         }
     }
 
@@ -225,24 +241,24 @@ public class InvokeRpcMethodTest {
         try {
             restconfImpl.invokeRpc("toaster:bad-method", "");
             fail("Expected an exception");
-        } catch (ResponseException e) {
-            assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e
-                    .getResponse().getStatus());
+        }
+        catch (RestconfDocumentedException e) {
+            verifyRestconfDocumentedException( e, 0, ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT,
+                                               Optional.<String>absent(), Optional.<String>absent() );
         }
     }
 
     @Test
     public void testInvokeRpcMethodWithInput() {
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(true);
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( true );
 
         CompositeNode payload = mock(CompositeNode.class);
 
         BrokerFacade brokerFacade = mock(BrokerFacade.class);
-        when(
-                brokerFacade.invokeRpc(
-                        eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)make-toast")),
-                        any(CompositeNode.class))).thenReturn(rpcResult);
+        when( brokerFacade.invokeRpc(
+                 eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)make-toast")),
+                 any(CompositeNode.class)))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ) );
 
         restconfImpl.setBroker(brokerFacade);
 
@@ -257,29 +273,28 @@ public class InvokeRpcMethodTest {
         try {
             restconfImpl.invokeRpc("toaster/slash", "");
             fail("Expected an exception.");
-        } catch (ResponseException e) {
-            assertEquals(e.getMessage(), Status.NOT_FOUND.getStatusCode(), e
-                    .getResponse().getStatus());
+        }
+        catch (RestconfDocumentedException e) {
+            verifyRestconfDocumentedException( e, 0, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE,
+                                               Optional.<String>absent(), Optional.<String>absent() );
         }
     }
 
     @Test
     public void testInvokeRpcWithNoPayloadWithOutput_Success() {
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(true);
-
         CompositeNode compositeNode = mock( CompositeNode.class );
-        when( rpcResult.getResult() ).thenReturn( compositeNode );
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( true, compositeNode,
+                                                            Collections.<RpcError>emptyList() );
 
         BrokerFacade brokerFacade = mock(BrokerFacade.class);
         when( brokerFacade.invokeRpc(
-                eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)testOutput")),
-                any( CompositeNode.class ))).thenReturn(rpcResult);
+                        eq(QName.create("(http://netconfcentral.org/ns/toaster?revision=2009-11-20)testOutput")),
+                        any( CompositeNode.class )))
+            .thenReturn( Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ) );
 
         restconfImpl.setBroker(brokerFacade);
 
-        StructuredData output = restconfImpl.invokeRpc("toaster:testOutput",
-                "");
+        StructuredData output = restconfImpl.invokeRpc("toaster:testOutput", "");
         assertNotNull( output );
         assertSame( compositeNode, output.getData() );
         assertNotNull( output.getSchema() );
@@ -288,8 +303,7 @@ public class InvokeRpcMethodTest {
     @Test
     public void testMountedRpcCallNoPayload_Success() throws Exception
     {
-        RpcResult<CompositeNode> rpcResult = mock(RpcResult.class);
-        when(rpcResult.isSuccessful()).thenReturn(true);
+        RpcResult<CompositeNode> rpcResult = Rpcs.<CompositeNode>getRpcResult( true );
 
         ListenableFuture<RpcResult<CompositeNode>> mockListener = mock( ListenableFuture.class );
         when( mockListener.get() ).thenReturn( rpcResult );
@@ -321,6 +335,4 @@ public class InvokeRpcMethodTest {
 
         //additional validation in the fact that the restconfImpl does not throw an exception.
     }
-
-
 }
index 6d2723c..f5aa453 100644 (file)
@@ -15,7 +15,7 @@ import java.net.URISyntaxException;
 import org.junit.BeforeClass;
 import org.junit.Test;
 import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper;
 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
 
@@ -26,48 +26,28 @@ public class NormalizeNodeTest extends YangAndXmlAndDataSchemaLoader {
         dataLoad("/normalize-node/yang/");
     }
 
-    @Test
+    @Test(expected=RestconfDocumentedException.class)
     public void namespaceNotNullAndInvalidNamespaceAndNoModuleNameTest() {
-        boolean exceptionReised = false;
-        try {
-            TestUtils.normalizeCompositeNode(prepareCnSn("wrongnamespace"), modules, schemaNodePath);
-        } catch (ResponseException e) {
-            exceptionReised = true;
-        }
-        assertTrue(exceptionReised);
+
+        TestUtils.normalizeCompositeNode(prepareCnSn("wrongnamespace"), modules, schemaNodePath);
     }
 
     @Test
     public void namespaceNullTest() {
-        String exceptionMessage = null;
-        try {
-            TestUtils.normalizeCompositeNode(prepareCnSn(null), modules, schemaNodePath);
-        } catch (ResponseException e) {
-            exceptionMessage = String.valueOf(e.getResponse().getEntity());
-        }
-        assertNull(exceptionMessage);
+
+        TestUtils.normalizeCompositeNode(prepareCnSn(null), modules, schemaNodePath);
     }
 
     @Test
     public void namespaceValidNamespaceTest() {
-        String exceptionMessage = null;
-        try {
-            TestUtils.normalizeCompositeNode(prepareCnSn("normalize:node:module"), modules, schemaNodePath);
-        } catch (ResponseException e) {
-            exceptionMessage = String.valueOf(e.getResponse().getEntity());
-        }
-        assertNull(exceptionMessage);
+
+        TestUtils.normalizeCompositeNode(prepareCnSn("normalize:node:module"), modules, schemaNodePath);
     }
 
     @Test
     public void namespaceValidModuleNameTest() {
-        String exceptionMessage = null;
-        try {
-            TestUtils.normalizeCompositeNode(prepareCnSn("normalize-node-module"), modules, schemaNodePath);
-        } catch (ResponseException e) {
-            exceptionMessage = String.valueOf(e.getResponse().getEntity());
-        }
-        assertNull(exceptionMessage);
+
+        TestUtils.normalizeCompositeNode(prepareCnSn("normalize-node-module"), modules, schemaNodePath);
     }
 
     private CompositeNode prepareCnSn(String namespace) {
index a639189..53183c6 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.controller.sal.restconf.impl.test;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
 import java.io.FileNotFoundException;
 
@@ -18,16 +19,16 @@ import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
 import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
 public class RestGetAugmentedElementWhenEqualNamesTest {
-    
+
     private static ControllerContext controllerContext = ControllerContext.getInstance();
-    
+
     @Rule
     public ExpectedException exception = ExpectedException.none();
-    
+
     @BeforeClass
     public static void init() throws FileNotFoundException {
         SchemaContext schemaContextTestModule = TestUtils.loadSchemaContext("/common/augment/yang");
@@ -41,17 +42,15 @@ public class RestGetAugmentedElementWhenEqualNamesTest {
         iiWithData = controllerContext.toInstanceIdentifier("main:cont/augment-main-b:cont1");
         assertEquals("ns:augment:main:b", iiWithData.getSchemaNode().getQName().getNamespace().toString());
     }
-    
+
     @Test
     public void nodeWithoutNamespaceHasMoreAugments() {
-        boolean exceptionCaught = false;
         try {
             controllerContext.toInstanceIdentifier("main:cont/cont1");
-        } catch (ResponseException e) {
-            assertTrue(((String) e.getResponse().getEntity()).contains("is added as augment from more than one module"));
-            exceptionCaught = true;
+            fail( "Expected exception" );
+        } catch (RestconfDocumentedException e) {
+            assertTrue(e.getErrors().get( 0 ).getErrorMessage().contains(
+                                    "is added as augment from more than one module"));
         }
-        assertTrue(exceptionCaught);
     }
-
 }
index 4198e20..893622f 100644 (file)
@@ -34,11 +34,11 @@ import javax.ws.rs.core.Response;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 import org.junit.BeforeClass;
-import org.junit.Ignore;
 import org.junit.Test;
 import org.opendaylight.controller.sal.core.api.mount.MountInstance;
 import org.opendaylight.controller.sal.core.api.mount.MountService;
 import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider;
+import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider;
 import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider;
@@ -92,6 +92,7 @@ public class RestGetOperationTest extends JerseyTest {
         resourceConfig = resourceConfig.registerInstances(restconfImpl, StructuredDataToXmlProvider.INSTANCE,
                 StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE,
                 JsonToCompositeNodeProvider.INSTANCE);
+        resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class );
         return resourceConfig;
     }
 
@@ -145,15 +146,15 @@ public class RestGetOperationTest extends JerseyTest {
 
     /**
      * MountPoint test. URI represents mount point.
-     * 
+     *
      * Slashes in URI behind mount point. lst1 element with key
      * GigabitEthernet0%2F0%2F0%2F0 (GigabitEthernet0/0/0/0) is requested via
      * GET HTTP operation. It is tested whether %2F character is replaced with
      * simple / in InstanceIdentifier parameter in method
      * {@link BrokerFacade#readConfigurationDataBehindMountPoint(MountInstance, InstanceIdentifier)}
      * which is called in method {@link RestconfImpl#readConfigurationData}
-     * 
-     * 
+     *
+     *
      * @throws ParseException
      */
     @Test
index c6e2f14..ce460fe 100644 (file)
@@ -38,6 +38,7 @@ import org.opendaylight.controller.sal.core.api.mount.MountInstance;
 import org.opendaylight.controller.sal.core.api.mount.MountService;
 import org.opendaylight.controller.sal.rest.api.Draft02;
 import org.opendaylight.controller.sal.rest.impl.JsonToCompositeNodeProvider;
+import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider;
 import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider;
 import org.opendaylight.controller.sal.rest.impl.XmlToCompositeNodeProvider;
@@ -52,6 +53,8 @@ import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 
+import com.google.common.util.concurrent.Futures;
+
 public class RestPostOperationTest extends JerseyTest {
 
     private static String xmlDataAbsolutePath;
@@ -99,6 +102,7 @@ public class RestPostOperationTest extends JerseyTest {
         resourceConfig = resourceConfig.registerInstances(restconfImpl, StructuredDataToXmlProvider.INSTANCE,
                 StructuredDataToJsonProvider.INSTANCE, XmlToCompositeNodeProvider.INSTANCE,
                 JsonToCompositeNodeProvider.INSTANCE);
+        resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class );
         return resourceConfig;
     }
 
@@ -116,7 +120,7 @@ public class RestPostOperationTest extends JerseyTest {
         assertEquals(500, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput));
 
         uri = "/operations/test-module:rpc-wrongtest";
-        assertEquals(404, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput));
+        assertEquals(400, post(uri, MediaType.APPLICATION_XML, xmlDataRpcInput));
     }
 
     @Test
@@ -173,7 +177,8 @@ public class RestPostOperationTest extends JerseyTest {
     private void mockInvokeRpc(CompositeNode result, boolean sucessful) {
         RpcResult<CompositeNode> rpcResult = new DummyRpcResult.Builder<CompositeNode>().result(result)
                 .isSuccessful(sucessful).build();
-        when(brokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class))).thenReturn(rpcResult);
+        when(brokerFacade.invokeRpc(any(QName.class), any(CompositeNode.class)))
+            .thenReturn(Futures.<RpcResult<CompositeNode>>immediateFuture( rpcResult ));
     }
 
     private void mockCommitConfigurationDataPostMethod(TransactionStatus statusName) {
diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfDocumentedExceptionMapperTest.java
new file mode 100644 (file)
index 0000000..fc5d7be
--- /dev/null
@@ -0,0 +1,966 @@
+/*
+ * Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.controller.sal.restconf.impl.test;
+
+import static org.junit.Assert.*;
+import static org.mockito.Matchers.*;
+import static org.mockito.Mockito.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Map.Entry;
+
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.Response.Status;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.xpath.XPath;
+import javax.xml.xpath.XPathConstants;
+import javax.xml.xpath.XPathExpression;
+import javax.xml.xpath.XPathFactory;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.controller.sal.rest.api.Draft02;
+import org.opendaylight.controller.sal.rest.api.RestconfService;
+import org.opendaylight.controller.sal.rest.impl.RestconfDocumentedExceptionMapper;
+import org.opendaylight.controller.sal.rest.impl.StructuredDataToJsonProvider;
+import org.opendaylight.controller.sal.rest.impl.StructuredDataToXmlProvider;
+import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError;
+import org.opendaylight.controller.sal.restconf.impl.StructuredData;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Maps;
+import com.google.common.io.ByteStreams;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParser;
+
+/**
+ * Unit tests for RestconfDocumentedExceptionMapper.
+ *
+ * @author Thomas Pantelis
+ */
+public class RestconfDocumentedExceptionMapperTest extends JerseyTest {
+
+    interface ErrorInfoVerifier {
+        void verifyXML( Node errorInfoNode );
+        void verifyJson( JsonElement errorInfoElement );
+    }
+
+    static class ComplexErrorInfoVerifier implements ErrorInfoVerifier {
+
+        Map<String, String> expErrorInfo;
+
+        public ComplexErrorInfoVerifier( Map<String, String> expErrorInfo ) {
+            this.expErrorInfo = expErrorInfo;
+        }
+
+        @Override
+        public void verifyXML( Node errorInfoNode ) {
+
+            Map<String, String> mutableExpMap = Maps.newHashMap( expErrorInfo );
+            NodeList childNodes = errorInfoNode.getChildNodes();
+            for( int i = 0; i < childNodes.getLength(); i++ ) {
+                Node child = childNodes.item( i );
+                if( child  instanceof Element ) {
+                    String expValue = mutableExpMap.remove( child.getNodeName() );
+                    assertNotNull( "Found unexpected \"error-info\" child node: " +
+                                   child.getNodeName(), expValue );
+                    assertEquals( "Text content for \"error-info\" child node " +
+                                   child.getNodeName(), expValue, child.getTextContent() );
+                }
+            }
+
+            if( !mutableExpMap.isEmpty() ) {
+                fail( "Missing \"error-info\" child nodes: " + mutableExpMap );
+            }
+        }
+
+        @Override
+        public void verifyJson( JsonElement errorInfoElement ) {
+
+            assertTrue( "\"error-info\" Json element is not an Object",
+                    errorInfoElement.isJsonObject() );
+
+            Map<String, String> actualErrorInfo = Maps.newHashMap();
+            for( Entry<String, JsonElement> entry: errorInfoElement.getAsJsonObject().entrySet() ) {
+                String leafName = entry.getKey();
+                JsonElement leafElement = entry.getValue();
+                actualErrorInfo.put( leafName, leafElement.getAsString() );
+            }
+
+            Map<String, String> mutableExpMap = Maps.newHashMap( expErrorInfo );
+            for( Entry<String,String> actual: actualErrorInfo.entrySet() ) {
+                String expValue = mutableExpMap.remove( actual.getKey() );
+                assertNotNull( "Found unexpected \"error-info\" child node: " +
+                                actual.getKey(), expValue );
+                assertEquals( "Text content for \"error-info\" child node " +
+                              actual.getKey(), expValue, actual.getValue() );
+            }
+
+            if( !mutableExpMap.isEmpty() ) {
+                fail( "Missing \"error-info\" child nodes: " + mutableExpMap );
+            }
+        }
+    }
+
+    static class SimpleErrorInfoVerifier implements ErrorInfoVerifier {
+
+        String expTextContent;
+
+        public SimpleErrorInfoVerifier( String expErrorInfo ) {
+            this.expTextContent = expErrorInfo;
+        }
+
+        void verifyContent( String actualContent ) {
+            assertNotNull( "Actual \"error-info\" text content is null", actualContent );
+            assertTrue( "", actualContent.contains( expTextContent ) );
+        }
+
+        @Override
+        public void verifyXML( Node errorInfoNode ) {
+            verifyContent( errorInfoNode.getTextContent() );
+        }
+
+        @Override
+        public void verifyJson( JsonElement errorInfoElement ) {
+            verifyContent( errorInfoElement.getAsString() );
+        }
+    }
+
+    static RestconfService mockRestConf = mock( RestconfService.class );
+
+    static XPath XPATH = XPathFactory.newInstance().newXPath();
+    static XPathExpression ERROR_LIST;
+    static XPathExpression ERROR_TYPE;
+    static XPathExpression ERROR_TAG;
+    static XPathExpression ERROR_MESSAGE;
+    static XPathExpression ERROR_APP_TAG;
+    static XPathExpression ERROR_INFO;
+
+    @BeforeClass
+    public static void init() throws Exception {
+        ControllerContext.getInstance().setGlobalSchema( TestUtils.loadSchemaContext("/modules") );
+
+        NamespaceContext nsContext = new NamespaceContext() {
+            @Override
+            public Iterator getPrefixes( String namespaceURI ) {
+                return null;
+            }
+
+            @Override
+            public String getPrefix( String namespaceURI ) {
+                return null;
+            }
+
+            @Override
+            public String getNamespaceURI( String prefix ) {
+                return "ietf-restconf".equals( prefix ) ? Draft02.RestConfModule.NAMESPACE : null;
+            }
+        };
+
+        XPATH.setNamespaceContext( nsContext );
+        ERROR_LIST = XPATH.compile( "ietf-restconf:errors/ietf-restconf:error" );
+        ERROR_TYPE = XPATH.compile( "ietf-restconf:error-type" );
+        ERROR_TAG = XPATH.compile( "ietf-restconf:error-tag" );
+        ERROR_MESSAGE = XPATH.compile( "ietf-restconf:error-message" );
+        ERROR_APP_TAG = XPATH.compile( "ietf-restconf:error-app-tag" );
+        ERROR_INFO = XPATH.compile( "ietf-restconf:error-info" );
+    }
+
+    @Override
+    @Before
+    public void setUp() throws Exception {
+        reset( mockRestConf );
+        super.setUp();
+    }
+
+    @Override
+    protected Application configure() {
+        ResourceConfig resourceConfig = new ResourceConfig();
+        resourceConfig = resourceConfig.registerInstances( mockRestConf, StructuredDataToXmlProvider.INSTANCE,
+                                                           StructuredDataToJsonProvider.INSTANCE );
+        resourceConfig.registerClasses( RestconfDocumentedExceptionMapper.class );
+        return resourceConfig;
+    }
+
+    void stageMockEx( RestconfDocumentedException ex ) {
+        reset( mockRestConf );
+        when( mockRestConf.readOperationalData( any( String.class ) ) ).thenThrow( ex );
+    }
+
+    void testJsonResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType,
+                           ErrorTag expErrorTag, String expErrorMessage, String expErrorAppTag,
+                           ErrorInfoVerifier errorInfoVerifier ) throws Exception {
+
+        stageMockEx( ex );
+
+        Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get();
+
+        InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, expStatus );
+
+        verifyJsonResponseBody( stream, expErrorType, expErrorTag, expErrorMessage,
+                                expErrorAppTag, errorInfoVerifier );
+    }
+
+    @Test
+    public void testToJsonResponseWithMessageOnly() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR,
+                          ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null );
+
+        // To test verification code
+//        String json =
+//            "{ errors: {" +
+//            "    error: [{" +
+//            "      error-tag : \"operation-failed\"" +
+//            "      ,error-type : \"application\"" +
+//            "      ,error-message : \"An error occurred\"" +
+//            "      ,error-info : {" +
+//            "        session-id: \"123\"" +
+//            "        ,address: \"1.2.3.4\"" +
+//            "      }" +
+//            "    }]" +
+//            "  }" +
+//            "}";
+//
+//        verifyJsonResponseBody( new java.io.StringBufferInputStream(json ), ErrorType.APPLICATION,
+//            ErrorTag.OPERATION_FAILED, "An error occurred", null,
+//            com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) );
+    }
+
+    @Test
+    public void testToJsonResponseWithInUseErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.IN_USE ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.IN_USE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithInvalidValueErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC,
+                                                           ErrorTag.INVALID_VALUE ),
+                          Status.BAD_REQUEST, ErrorType.RPC,
+                          ErrorTag.INVALID_VALUE, "mock error", null, null );
+
+    }
+
+    @Test
+    public void testToJsonResponseWithTooBigErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT,
+                                                           ErrorTag.TOO_BIG ),
+                          Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT,
+                          ErrorTag.TOO_BIG, "mock error", null, null );
+
+    }
+
+    @Test
+    public void testToJsonResponseWithMissingAttributeErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.MISSING_ATTRIBUTE ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithBadAttributeErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.BAD_ATTRIBUTE ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.BAD_ATTRIBUTE, "mock error", null, null );
+    }
+    @Test
+    public void testToJsonResponseWithUnknownAttributeErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_ATTRIBUTE ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithBadElementErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.BAD_ELEMENT ),
+                          Status.BAD_REQUEST,
+                          ErrorType.PROTOCOL, ErrorTag.BAD_ELEMENT, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithUnknownElementErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_ELEMENT ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithUnknownNamespaceErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_NAMESPACE ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithMalformedMessageErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.MALFORMED_MESSAGE ),
+                          Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                          ErrorTag.MALFORMED_MESSAGE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithAccessDeniedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.ACCESS_DENIED ),
+                          Status.FORBIDDEN, ErrorType.PROTOCOL,
+                          ErrorTag.ACCESS_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithLockDeniedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.LOCK_DENIED ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.LOCK_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithResourceDeniedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.RESOURCE_DENIED ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.RESOURCE_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithRollbackFailedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.ROLLBACK_FAILED ),
+                          Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                          ErrorTag.ROLLBACK_FAILED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithDataExistsErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.DATA_EXISTS ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.DATA_EXISTS, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithDataMissingErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.DATA_MISSING ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.DATA_MISSING, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithOperationNotSupportedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.OPERATION_NOT_SUPPORTED ),
+                          Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL,
+                          ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithOperationFailedErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.OPERATION_FAILED ),
+                          Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                          ErrorTag.OPERATION_FAILED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithPartialOperationErrorTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.PARTIAL_OPERATION ),
+                          Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                          ErrorTag.PARTIAL_OPERATION, "mock error", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithErrorAppTag() throws Exception {
+
+        testJsonResponse( new RestconfDocumentedException( new RestconfError(
+                                   ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                                   "mock error", "mock-app-tag" ) ),
+                          Status.BAD_REQUEST, ErrorType.APPLICATION,
+                          ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null );
+    }
+
+    @Test
+    public void testToJsonResponseWithMultipleErrors() throws Exception {
+
+        List<RestconfError> errorList = Arrays.asList(
+                new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ),
+                new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) );
+        stageMockEx( new RestconfDocumentedException( errorList ) );
+
+        Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get();
+
+        InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON, Status.CONFLICT );
+
+        JsonArray arrayElement = parseJsonErrorArrayElement( stream );
+
+        assertEquals( "\"error\" Json array element length", 2, arrayElement.size() );
+
+        verifyJsonErrorNode( arrayElement.get( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED,
+                             "mock error1", null, null );
+
+        verifyJsonErrorNode( arrayElement.get( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED,
+                             "mock error2", null, null );
+    }
+
+    @Test
+    public void testToJsonResponseWithErrorInfo() throws Exception {
+
+        String errorInfo = "<address>1.2.3.4</address> <session-id>123</session-id>";
+        testJsonResponse( new RestconfDocumentedException( new RestconfError(
+                                               ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                                               "mock error", "mock-app-tag", errorInfo ) ),
+                          Status.BAD_REQUEST, ErrorType.APPLICATION,
+                          ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag",
+                          new ComplexErrorInfoVerifier( ImmutableMap.of(
+                                                "session-id", "123", "address", "1.2.3.4" ) ) );
+    }
+
+    @Test
+    public void testToJsonResponseWithExceptionCause() throws Exception {
+
+        Exception cause = new Exception( "mock exception cause" );
+        testJsonResponse( new RestconfDocumentedException( "mock error", cause ),
+                          Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION,
+                          ErrorTag.OPERATION_FAILED, "mock error", null,
+                          new SimpleErrorInfoVerifier( cause.getMessage() ) );
+    }
+
+    void testXMLResponse( RestconfDocumentedException ex, Status expStatus, ErrorType expErrorType,
+                          ErrorTag expErrorTag, String expErrorMessage,
+                          String expErrorAppTag, ErrorInfoVerifier errorInfoVerifier ) throws Exception
+    {
+        stageMockEx( ex );
+
+        Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get();
+
+        InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, expStatus );
+
+        verifyXMLResponseBody( stream, expErrorType, expErrorTag, expErrorMessage,
+                               expErrorAppTag, errorInfoVerifier );
+    }
+
+    @Test
+    public void testToXMLResponseWithMessageOnly() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error" ), Status.INTERNAL_SERVER_ERROR,
+                         ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error", null, null );
+
+        // To test verification code
+//        String xml =
+//            "<errors xmlns=\"urn:ietf:params:xml:ns:yang:ietf-restconf\">"+
+//            "  <error>" +
+//            "    <error-type>application</error-type>"+
+//            "    <error-tag>operation-failed</error-tag>"+
+//            "    <error-message>An error occurred</error-message>"+
+//            "    <error-info>" +
+//            "      <session-id>123</session-id>" +
+//            "      <address>1.2.3.4</address>" +
+//            "    </error-info>" +
+//            "  </error>" +
+//            "</errors>";
+//
+//        verifyXMLResponseBody( new java.io.StringBufferInputStream(xml), ErrorType.APPLICATION,
+//                ErrorTag.OPERATION_FAILED, "An error occurred", null,
+//                com.google.common.collect.ImmutableMap.of( "session-id", "123", "address", "1.2.3.4" ) );
+    }
+
+    @Test
+    public void testToXMLResponseWithInUseErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.IN_USE ),
+                         Status.CONFLICT, ErrorType.PROTOCOL,
+                         ErrorTag.IN_USE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithInvalidValueErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.RPC,
+                                                           ErrorTag.INVALID_VALUE ),
+                         Status.BAD_REQUEST, ErrorType.RPC,
+                         ErrorTag.INVALID_VALUE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithTooBigErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.TRANSPORT,
+                                                           ErrorTag.TOO_BIG ),
+                         Status.REQUEST_ENTITY_TOO_LARGE, ErrorType.TRANSPORT,
+                         ErrorTag.TOO_BIG, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithMissingAttributeErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.MISSING_ATTRIBUTE ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.MISSING_ATTRIBUTE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithBadAttributeErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.BAD_ATTRIBUTE ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.BAD_ATTRIBUTE, "mock error", null, null );
+    }
+    @Test
+    public void testToXMLResponseWithUnknownAttributeErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_ATTRIBUTE ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.UNKNOWN_ATTRIBUTE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithBadElementErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.BAD_ELEMENT ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.BAD_ELEMENT, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithUnknownElementErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_ELEMENT ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.UNKNOWN_ELEMENT, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithUnknownNamespaceErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.UNKNOWN_NAMESPACE ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.UNKNOWN_NAMESPACE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithMalformedMessageErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.MALFORMED_MESSAGE ),
+                         Status.BAD_REQUEST, ErrorType.PROTOCOL,
+                         ErrorTag.MALFORMED_MESSAGE, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithAccessDeniedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.ACCESS_DENIED ),
+                         Status.FORBIDDEN, ErrorType.PROTOCOL,
+                         ErrorTag.ACCESS_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithLockDeniedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.LOCK_DENIED ),
+                          Status.CONFLICT, ErrorType.PROTOCOL,
+                          ErrorTag.LOCK_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithResourceDeniedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.RESOURCE_DENIED ),
+                         Status.CONFLICT, ErrorType.PROTOCOL,
+                         ErrorTag.RESOURCE_DENIED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithRollbackFailedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.ROLLBACK_FAILED ),
+                         Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                         ErrorTag.ROLLBACK_FAILED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithDataExistsErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.DATA_EXISTS ),
+                         Status.CONFLICT, ErrorType.PROTOCOL,
+                         ErrorTag.DATA_EXISTS, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithDataMissingErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.DATA_MISSING ),
+                         Status.CONFLICT, ErrorType.PROTOCOL,
+                         ErrorTag.DATA_MISSING, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithOperationNotSupportedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.OPERATION_NOT_SUPPORTED ),
+                         Status.NOT_IMPLEMENTED, ErrorType.PROTOCOL,
+                         ErrorTag.OPERATION_NOT_SUPPORTED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithOperationFailedErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.OPERATION_FAILED ),
+                         Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                         ErrorTag.OPERATION_FAILED, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithPartialOperationErrorTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( "mock error", ErrorType.PROTOCOL,
+                                                           ErrorTag.PARTIAL_OPERATION ),
+                         Status.INTERNAL_SERVER_ERROR, ErrorType.PROTOCOL,
+                         ErrorTag.PARTIAL_OPERATION, "mock error", null, null );
+    }
+
+    @Test
+    public void testToXMLResponseWithErrorAppTag() throws Exception {
+
+        testXMLResponse( new RestconfDocumentedException( new RestconfError(
+                                              ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                                              "mock error", "mock-app-tag" ) ),
+                         Status.BAD_REQUEST, ErrorType.APPLICATION,
+                         ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag", null );
+    }
+
+    @Test
+    public void testToXMLResponseWithErrorInfo() throws Exception {
+
+        String errorInfo = "<address>1.2.3.4</address> <session-id>123</session-id>";
+        testXMLResponse( new RestconfDocumentedException( new RestconfError(
+                                               ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                                               "mock error", "mock-app-tag", errorInfo ) ),
+                         Status.BAD_REQUEST, ErrorType.APPLICATION,
+                         ErrorTag.INVALID_VALUE, "mock error", "mock-app-tag",
+                         new ComplexErrorInfoVerifier( ImmutableMap.of(
+                                              "session-id", "123", "address", "1.2.3.4" ) ) );
+    }
+
+    @Test
+    public void testToXMLResponseWithExceptionCause() throws Exception {
+
+        Exception cause = new Exception( "mock exception cause" );
+        testXMLResponse( new RestconfDocumentedException( "mock error", cause ),
+                         Status.INTERNAL_SERVER_ERROR, ErrorType.APPLICATION,
+                         ErrorTag.OPERATION_FAILED, "mock error", null,
+                         new SimpleErrorInfoVerifier( cause.getMessage() ) );
+    }
+
+    @Test
+    public void testToXMLResponseWithMultipleErrors() throws Exception {
+
+        List<RestconfError> errorList = Arrays.asList(
+                new RestconfError( ErrorType.APPLICATION, ErrorTag.LOCK_DENIED, "mock error1" ),
+                new RestconfError( ErrorType.RPC, ErrorTag.ROLLBACK_FAILED, "mock error2" ) );
+        stageMockEx( new RestconfDocumentedException( errorList ) );
+
+        Response resp = target("/operational/foo").request( MediaType.APPLICATION_XML ).get();
+
+        InputStream stream = verifyResponse( resp, MediaType.APPLICATION_XML, Status.CONFLICT );
+
+        Document doc = parseXMLDocument( stream );
+
+        NodeList children = getXMLErrorList( doc, 2 );
+
+        verifyXMLErrorNode( children.item( 0 ), ErrorType.APPLICATION, ErrorTag.LOCK_DENIED,
+                            "mock error1", null, null );
+
+        verifyXMLErrorNode( children.item( 1 ), ErrorType.RPC, ErrorTag.ROLLBACK_FAILED,
+                            "mock error2", null, null );
+    }
+
+    @Test
+    public void testToResponseWithAcceptHeader() throws Exception {
+
+        stageMockEx( new RestconfDocumentedException( "mock error" ) );
+
+        Response resp = target("/operational/foo")
+                                  .request().header( "Accept", MediaType.APPLICATION_JSON ).get();
+
+        InputStream stream = verifyResponse( resp, MediaType.APPLICATION_JSON,
+                                             Status.INTERNAL_SERVER_ERROR );
+
+        verifyJsonResponseBody( stream, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, "mock error",
+                                null, null );
+    }
+
+    @Test
+    public void testToResponseWithStatusOnly() throws Exception {
+
+        // The StructuredDataToJsonProvider should throw a RestconfDocumentedException with no data
+
+        when( mockRestConf.readOperationalData( any( String.class ) ) )
+            .thenReturn( new StructuredData( null, null, null ) );
+
+        Response resp = target("/operational/foo").request( MediaType.APPLICATION_JSON ).get();
+
+        verifyResponse( resp, MediaType.TEXT_PLAIN, Status.NOT_FOUND );
+    }
+
+    InputStream verifyResponse( Response resp, String expMediaType, Status expStatus ) {
+        assertEquals( "getMediaType", MediaType.valueOf( expMediaType ), resp.getMediaType() );
+        assertEquals( "getStatus", expStatus.getStatusCode(), resp.getStatus() );
+
+        Object entity = resp.getEntity();
+        assertEquals( "Response entity", true, entity instanceof InputStream );
+        InputStream stream = (InputStream)entity;
+        return stream;
+    }
+
+    void verifyJsonResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag,
+                                 String expErrorMessage, String expErrorAppTag,
+                                 ErrorInfoVerifier errorInfoVerifier ) throws Exception {
+
+        JsonArray arrayElement = parseJsonErrorArrayElement( stream );
+
+        assertEquals( "\"error\" Json array element length", 1, arrayElement.size() );
+
+        verifyJsonErrorNode( arrayElement.get( 0 ),  expErrorType, expErrorTag, expErrorMessage,
+                             expErrorAppTag, errorInfoVerifier );
+    }
+
+    private JsonArray parseJsonErrorArrayElement( InputStream stream ) throws IOException {
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteStreams.copy( stream, bos );
+
+        System.out.println("JSON: "+bos.toString());
+
+        JsonParser parser = new JsonParser();
+        JsonElement rootElement;
+
+        try {
+            rootElement = parser.parse(
+                          new InputStreamReader( new ByteArrayInputStream( bos.toByteArray() ) ) );
+        }
+        catch( Exception e ) {
+            throw new IllegalArgumentException( "Invalid JSON response:\n" + bos.toString(), e );
+        }
+
+        assertTrue( "Root element of Json is not an Object", rootElement.isJsonObject() );
+
+        Set<Entry<String, JsonElement>> errorsEntrySet = rootElement.getAsJsonObject().entrySet();
+        assertEquals( "Json Object element set count", 1, errorsEntrySet.size() );
+
+        Entry<String, JsonElement> errorsEntry = errorsEntrySet.iterator().next();
+        JsonElement errorsElement = errorsEntry.getValue();
+        assertEquals( "First Json element name", "errors", errorsEntry.getKey() );
+        assertTrue( "\"errors\" Json element is not an Object", errorsElement.isJsonObject() );
+
+        Set<Entry<String, JsonElement>> errorListEntrySet = errorsElement.getAsJsonObject().entrySet();
+        assertEquals( "Root \"errors\" element child count", 1, errorListEntrySet.size() );
+
+        JsonElement errorListElement = errorListEntrySet.iterator().next().getValue();
+        assertEquals( "\"errors\" child Json element name", "error",
+                      errorListEntrySet.iterator().next().getKey() );
+        assertTrue( "\"error\" Json element is not an Array", errorListElement.isJsonArray() );
+
+        return errorListElement.getAsJsonArray();
+    }
+
+    void verifyJsonErrorNode( JsonElement errorEntryElement, ErrorType expErrorType, ErrorTag expErrorTag,
+                              String expErrorMessage, String expErrorAppTag,
+                              ErrorInfoVerifier errorInfoVerifier ) {
+
+        JsonElement errorInfoElement = null;
+        Map<String, String> actualErrorInfo = null;
+        Map<String, String> leafMap = Maps.newHashMap();
+        for( Entry<String, JsonElement> entry: errorEntryElement.getAsJsonObject().entrySet() ) {
+            String leafName = entry.getKey();
+            JsonElement leafElement = entry.getValue();
+
+            if( "error-info".equals( leafName ) ) {
+                assertNotNull( "Found unexpected \"error-info\" element", errorInfoVerifier );
+                errorInfoElement = leafElement;
+            }
+            else {
+                assertTrue( "\"error\" leaf Json element " + leafName +
+                            " is not a Primitive", leafElement.isJsonPrimitive() );
+
+                leafMap.put( leafName, leafElement.getAsString() );
+            }
+        }
+
+        assertEquals( "error-type", expErrorType.getErrorTypeTag(), leafMap.remove( "error-type" ) );
+        assertEquals( "error-tag", expErrorTag.getTagValue(), leafMap.remove( "error-tag" ) );
+
+        verifyOptionalJsonLeaf( leafMap.remove( "error-message" ), expErrorMessage, "error-message" );
+        verifyOptionalJsonLeaf( leafMap.remove( "error-app-tag" ), expErrorAppTag, "error-app-tag" );
+
+        if( !leafMap.isEmpty() ) {
+            fail( "Found unexpected Json leaf elements for \"error\" element: " + leafMap );
+        }
+
+        if( errorInfoVerifier != null ) {
+            assertNotNull( "Missing \"error-info\" element", errorInfoElement );
+            errorInfoVerifier.verifyJson( errorInfoElement );
+        }
+    }
+
+    void verifyOptionalJsonLeaf( String actualValue, String expValue, String tagName ) {
+        if( expValue != null ) {
+            assertEquals( tagName, expValue, actualValue );
+        }
+        else {
+            assertNull( "Found unexpected \"error\" leaf entry for: " + tagName, actualValue );
+        }
+    }
+
+    void verifyXMLResponseBody( InputStream stream, ErrorType expErrorType, ErrorTag expErrorTag,
+                                String expErrorMessage, String expErrorAppTag,
+                                ErrorInfoVerifier errorInfoVerifier )
+                                                                    throws Exception {
+
+        Document doc = parseXMLDocument( stream );
+
+        NodeList children = getXMLErrorList( doc, 1 );
+
+        verifyXMLErrorNode( children.item( 0 ), expErrorType, expErrorTag, expErrorMessage,
+                            expErrorAppTag, errorInfoVerifier );
+    }
+
+    private Document parseXMLDocument( InputStream stream ) throws IOException {
+        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setCoalescing(true);
+        factory.setIgnoringElementContentWhitespace(true);
+        factory.setIgnoringComments(true);
+
+        ByteArrayOutputStream bos = new ByteArrayOutputStream();
+        ByteStreams.copy( stream, bos );
+
+        System.out.println("XML: "+bos.toString());
+
+        Document doc = null;
+        try {
+            doc = factory.newDocumentBuilder().parse( new ByteArrayInputStream( bos.toByteArray() ) );
+        }
+        catch( Exception e ) {
+            throw new IllegalArgumentException( "Invalid XML response:\n" + bos.toString(), e );
+        }
+        return doc;
+    }
+
+    void verifyXMLErrorNode( Node errorNode, ErrorType expErrorType, ErrorTag expErrorTag,
+                             String expErrorMessage, String expErrorAppTag,
+                             ErrorInfoVerifier errorInfoVerifier ) throws Exception {
+
+        String errorType = (String)ERROR_TYPE.evaluate( errorNode, XPathConstants.STRING );
+        assertEquals( "error-type", expErrorType.getErrorTypeTag(), errorType );
+
+        String errorTag = (String)ERROR_TAG.evaluate( errorNode, XPathConstants.STRING );
+        assertEquals( "error-tag", expErrorTag.getTagValue(), errorTag );
+
+        verifyOptionalXMLLeaf( errorNode, ERROR_MESSAGE, expErrorMessage, "error-message" );
+        verifyOptionalXMLLeaf( errorNode, ERROR_APP_TAG, expErrorAppTag, "error-app-tag" );
+
+        Node errorInfoNode = (Node)ERROR_INFO.evaluate( errorNode, XPathConstants.NODE );
+        if( errorInfoVerifier != null ) {
+            assertNotNull( "Missing \"error-info\" node", errorInfoNode );
+
+            errorInfoVerifier.verifyXML( errorInfoNode );
+        }
+        else {
+            assertNull( "Found unexpected \"error-info\" node", errorInfoNode );
+        }
+    }
+
+    void verifyOptionalXMLLeaf( Node fromNode, XPathExpression xpath, String expValue,
+                                String tagName ) throws Exception {
+        if( expValue != null ) {
+            String actual = (String)xpath.evaluate( fromNode, XPathConstants.STRING );
+            assertEquals( tagName, expValue, actual );
+        }
+        else {
+            assertNull( "Found unexpected \"error\" leaf entry for: " + tagName,
+                        xpath.evaluate( fromNode, XPathConstants.NODE ) );
+        }
+    }
+
+    NodeList getXMLErrorList( Node fromNode, int count ) throws Exception {
+        NodeList errorList = (NodeList)ERROR_LIST.evaluate( fromNode, XPathConstants.NODESET );
+        assertNotNull( "Root errors node is empty", errorList );
+        assertEquals( "Root errors node child count", count, errorList.getLength() );
+        return errorList;
+    }
+}
diff --git a/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java b/opendaylight/md-sal/sal-rest-connector/src/test/java/org/opendaylight/controller/sal/restconf/impl/test/RestconfErrorTest.java
new file mode 100644 (file)
index 0000000..70ad768
--- /dev/null
@@ -0,0 +1,236 @@
+/*
+* Copyright (c) 2014 Brocade Communications Systems, Inc. and others.  All rights reserved.
+*
+* This program and the accompanying materials are made available under the
+* terms of the Eclipse Public License v1.0 which accompanies this distribution,
+* and is available at http://www.eclipse.org/legal/epl-v10.html
+*/
+package org.opendaylight.controller.sal.restconf.impl.test;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.ws.rs.core.Response.Status;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.junit.Test;
+
+import static org.opendaylight.controller.sal.common.util.RpcErrors.getRpcError;
+
+import org.opendaylight.controller.sal.restconf.impl.RestconfError;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
+import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
+import org.opendaylight.yangtools.yang.common.RpcError;
+
+/**
+ * Unit tests for RestconfError.
+ *
+ * @author Devin Avery
+ * @author Thomas Pantelis
+ *
+ */
+public class RestconfErrorTest {
+
+    static class Contains extends BaseMatcher<String> {
+
+        private final String text;
+
+        public Contains( String text ) {
+            this.text = text;
+        }
+
+        @Override
+        public void describeTo( Description desc ) {
+            desc.appendText( "contains " ).appendValue( text );
+        }
+
+        @Override
+        public boolean matches( Object arg ) {
+            return arg != null && arg.toString().contains( text );
+        }
+    }
+
+    @Test
+    public void testErrorTagValueOf()
+    {
+        assertEquals( ErrorTag.IN_USE,
+                ErrorTag.valueOfCaseInsensitive( ErrorTag.IN_USE.getTagValue() ) );
+    }
+
+    @Test
+    public void testErrorTagValueOfIsLowercase()
+    {
+        assertEquals( "in-use",
+                ErrorTag.IN_USE.getTagValue() );
+    }
+
+    @Test
+    public void testErrorTypeGetErrorTypeTagIsLowerCase()
+    {
+       assertEquals( ErrorType.APPLICATION.name().toLowerCase(),
+               ErrorType.APPLICATION.getErrorTypeTag() );
+    }
+
+    @Test
+    public void testErrorTypeValueOf()
+    {
+       assertEquals( ErrorType.APPLICATION,
+                     ErrorType.valueOfCaseInsensitive( ErrorType.APPLICATION.getErrorTypeTag() ) );
+    }
+
+    @Test
+    public void testErrorTagStatusCodes()
+    {
+        Map<String,Status> lookUpMap = new HashMap<String,Status>();
+
+        lookUpMap.put( "in-use", Status.fromStatusCode(409));
+        lookUpMap.put( "invalid-value", Status.fromStatusCode(400));
+        lookUpMap.put( "too-big", Status.fromStatusCode(413));
+        lookUpMap.put( "missing-attribute", Status.fromStatusCode(400));
+        lookUpMap.put( "bad-attribute", Status.fromStatusCode(400));
+        lookUpMap.put( "unknown-attribute", Status.fromStatusCode(400));
+        lookUpMap.put( "bad-element", Status.fromStatusCode(400));
+        lookUpMap.put( "unknown-element", Status.fromStatusCode(400));
+        lookUpMap.put( "unknown-namespace", Status.fromStatusCode(400));
+        lookUpMap.put( "access-denied", Status.fromStatusCode(403));
+        lookUpMap.put( "lock-denied", Status.fromStatusCode(409));
+        lookUpMap.put( "resource-denied", Status.fromStatusCode(409));
+        lookUpMap.put( "rollback-failed", Status.fromStatusCode(500));
+        lookUpMap.put( "data-exists", Status.fromStatusCode(409));
+        lookUpMap.put( "data-missing", Status.fromStatusCode(409));
+        lookUpMap.put( "operation-not-supported", Status.fromStatusCode(501));
+        lookUpMap.put( "operation-failed", Status.fromStatusCode(500));
+        lookUpMap.put( "partial-operation", Status.fromStatusCode(500));
+        lookUpMap.put( "malformed-message", Status.fromStatusCode(400));
+
+        for( ErrorTag tag : ErrorTag.values() )
+        {
+            Status expectedStatusCode = lookUpMap.get( tag.getTagValue() );
+            assertNotNull( "Failed to find " + tag.getTagValue(), expectedStatusCode );
+            assertEquals( "Status Code does not match", expectedStatusCode, tag.getStatusCode() );
+        }
+    }
+
+    @Test
+    public void testRestConfDocumentedException_NoCause()
+    {
+        String expectedMessage = "Message";
+        ErrorType expectedErrorType = ErrorType.RPC;
+        ErrorTag expectedErrorTag = ErrorTag.IN_USE;
+        RestconfError e =
+                new RestconfError( expectedErrorType,
+                                                 expectedErrorTag, expectedMessage );
+
+        validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag,
+                              null, (String)null, e);
+    }
+
+    @Test
+    public void testRestConfDocumentedException_WithAppTag()
+    {
+        String expectedMessage = "Message";
+        ErrorType expectedErrorType = ErrorType.RPC;
+        ErrorTag expectedErrorTag = ErrorTag.IN_USE;
+        String expectedErrorAppTag = "application.tag";
+
+        RestconfError e =
+                new RestconfError( expectedErrorType,
+                                                 expectedErrorTag, expectedMessage, expectedErrorAppTag );
+
+        validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag,
+                              expectedErrorAppTag, (String)null, e);
+    }
+
+    @Test
+    public void testRestConfDocumentedException_WithAppTagErrorInfo()
+    {
+        String expectedMessage = "Message";
+        ErrorType expectedErrorType = ErrorType.RPC;
+        ErrorTag expectedErrorTag = ErrorTag.IN_USE;
+        String expectedErrorAppTag = "application.tag";
+        String errorInfo = "<extra><sessionid>session.id</sessionid></extra>";
+
+        RestconfError e = new RestconfError( expectedErrorType,
+                                             expectedErrorTag,
+                                             expectedMessage,
+                                             expectedErrorAppTag,
+                                             errorInfo );
+
+        validateRestConfError(expectedMessage, expectedErrorType, expectedErrorTag,
+                expectedErrorAppTag, errorInfo, e);
+    }
+
+    @Test
+    public void testRestConfErrorWithRpcError() {
+
+        // All fields set
+        RpcError rpcError = getRpcError( "mock app-tag", ErrorTag.BAD_ATTRIBUTE.getTagValue(),
+                                         "mock error-info", RpcError.ErrorSeverity.ERROR,
+                                         "mock error-message", RpcError.ErrorType.PROTOCOL,
+                                         new Exception( "mock cause" ) );
+
+        validateRestConfError( "mock error-message", ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE,
+                               "mock app-tag", "mock error-info",
+                               new RestconfError( rpcError ) );
+
+        // All fields set except 'info' - expect error-info set to 'cause'
+        rpcError = getRpcError( "mock app-tag", ErrorTag.BAD_ATTRIBUTE.getTagValue(),
+                                null, RpcError.ErrorSeverity.ERROR,
+                                "mock error-message", RpcError.ErrorType.PROTOCOL,
+                                new Exception( "mock cause" ) );
+
+        validateRestConfError( "mock error-message", ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE,
+                               "mock app-tag", new Contains( "mock cause" ),
+                               new RestconfError( rpcError ) );
+
+        // Some fields set - expect error-info set to ErrorSeverity
+        rpcError = getRpcError( null, ErrorTag.ACCESS_DENIED.getTagValue(),
+                                null, RpcError.ErrorSeverity.ERROR,
+                                null, RpcError.ErrorType.RPC, null );
+
+        validateRestConfError( null, ErrorType.RPC, ErrorTag.ACCESS_DENIED,
+                               null, "<severity>error</severity>",
+                               new RestconfError( rpcError ) );
+
+        // 'tag' field not mapped to ErrorTag - expect error-tag set to OPERATION_FAILED
+        rpcError = getRpcError( null, "not mapped",
+                                null, RpcError.ErrorSeverity.WARNING,
+                                null, RpcError.ErrorType.TRANSPORT, null );
+
+        validateRestConfError( null, ErrorType.TRANSPORT, ErrorTag.OPERATION_FAILED,
+                               null, "<severity>warning</severity>",
+                               new RestconfError( rpcError ) );
+
+        // No fields set - edge case
+        rpcError = getRpcError( null, null, null, null, null, null, null );
+
+        validateRestConfError( null, ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED,
+                               null, (String)null, new RestconfError( rpcError ) );
+    }
+
+    private void validateRestConfError(String expectedMessage, ErrorType expectedErrorType,
+            ErrorTag expectedErrorTag, String expectedErrorAppTag, String errorInfo, RestconfError e) {
+
+        validateRestConfError( expectedMessage, expectedErrorType, expectedErrorTag,
+                               expectedErrorAppTag, equalTo( errorInfo ), e );
+    }
+
+    private void validateRestConfError(String expectedMessage, ErrorType expectedErrorType,
+            ErrorTag expectedErrorTag, String expectedErrorAppTag,
+            Matcher<String> errorInfoMatcher, RestconfError e) {
+
+        assertEquals( "getErrorMessage", expectedMessage, e.getErrorMessage() );
+        assertEquals( "getErrorType", expectedErrorType, e.getErrorType() );
+        assertEquals( "getErrorTag", expectedErrorTag, e.getErrorTag() );
+        assertEquals( "getErrorAppTag", expectedErrorAppTag, e.getErrorAppTag() );
+        assertThat( "getErrorInfo", e.getErrorInfo(), errorInfoMatcher );
+        e.toString(); // really just checking for NPE etc. Don't care about contents.
+    }
+}
index eef9e41..33d4b32 100644 (file)
@@ -26,7 +26,7 @@ import org.opendaylight.controller.sal.core.api.mount.MountService;
 import org.opendaylight.controller.sal.restconf.impl.BrokerFacade;
 import org.opendaylight.controller.sal.restconf.impl.ControllerContext;
 import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
-import org.opendaylight.controller.sal.restconf.impl.ResponseException;
+import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
 import org.opendaylight.controller.sal.restconf.impl.RestconfImpl;
 import org.opendaylight.yangtools.yang.data.api.InstanceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
@@ -67,15 +67,13 @@ public class URITest {
 
     @Test
     public void testToInstanceIdentifierListWithNullKey() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes:user/null/boo");
     }
 
     @Test
     public void testToInstanceIdentifierListWithMissingKey() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes:user/foo");
     }
 
@@ -96,29 +94,25 @@ public class URITest {
 
     @Test
     public void testToInstanceIdentifierChoiceException() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes:food/snack");
     }
 
     @Test
     public void testToInstanceIdentifierCaseException() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes:food/sports-arena");
     }
 
     @Test
     public void testToInstanceIdentifierChoiceCaseException() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes:food/snack/sports-arena");
     }
-    
+
     @Test
     public void testToInstanceIdentifierWithoutNode() {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request");
+        exception.expect(RestconfDocumentedException.class);
         controllerContext.toInstanceIdentifier("simple-nodes");
     }
 
@@ -142,24 +136,22 @@ public class URITest {
 
     @Test
     public void testMountPointWithoutMountService() throws FileNotFoundException {
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 503 Service Unavailable"); 
-        
+        exception.expect(RestconfDocumentedException.class);
+
         controllerContext.setMountService(null);
         InstanceIdWithSchemaNode instanceIdentifier = controllerContext
                 .toInstanceIdentifier("simple-nodes:users/yang-ext:mount/test-interface2:class/student/name");
     }
-    
+
     @Test
     public void testMountPointWithoutMountPointSchema() {
         initMountService(false);
-        exception.expect(ResponseException.class);
-        exception.expectMessage("HTTP 400 Bad Request"); 
-        
+        exception.expect(RestconfDocumentedException.class);
+
         InstanceIdWithSchemaNode instanceIdentifier = controllerContext
                 .toInstanceIdentifier("simple-nodes:users/yang-ext:mount/test-interface2:class");
     }
-    
+
     public void initMountService(boolean withSchema) {
         MountService mountService = mock(MountService.class);
         controllerContext.setMountService(mountService);
index b4da5a3..2ecd7e7 100644 (file)
@@ -37,7 +37,9 @@ import org.opendaylight.controller.sal.binding.api.data.DataChangeListener;
 import org.opendaylight.yangtools.yang.binding.DataObject;
 import org.opendaylight.yangtools.yang.binding.InstanceIdentifier;
 import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorType;
 import org.opendaylight.yangtools.yang.common.RpcResult;
+import org.opendaylight.yangtools.yang.common.RpcError.ErrorSeverity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -157,14 +159,15 @@ public class OpendaylightToaster implements ToasterService, ToasterProviderRunti
                 LOG.info( "Toaster is already making toast" );
 
                 RpcResult<Void> result = Rpcs.<Void> getRpcResult(false, null, Arrays.asList(
-                        RpcErrors.getRpcError( null, null, null, null,
-                                               "Toaster is busy", null, null ) ) );
+                        RpcErrors.getRpcError( "", "in-use", null, ErrorSeverity.WARNING,
+                                               "Toaster is busy", ErrorType.APPLICATION, null ) ) );
                 return Futures.immediateFuture(result);
             }
             else if( outOfBread() ) {
                 RpcResult<Void> result = Rpcs.<Void> getRpcResult(false, null, Arrays.asList(
-                        RpcErrors.getRpcError( null, null, null, null,
-                                               "Toaster is out of bread", null, null ) ) );
+                        RpcErrors.getRpcError( "out-of-stock", "resource-denied", null, null,
+                                               "Toaster is out of bread",
+                                               ErrorType.APPLICATION, null ) ) );
                 return Futures.immediateFuture(result);
             }
             else {

©2013 OpenDaylight, A Linux Foundation Collaborative Project. All Rights Reserved.
OpenDaylight is a registered trademark of The OpenDaylight Project, Inc.
Linux Foundation and OpenDaylight are registered trademarks of the Linux Foundation.
Linux is a registered trademark of Linus Torvalds.