Update RESTCONF error mapping 09/96709/1
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 25 Jun 2021 21:26:01 +0000 (23:26 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Tue, 29 Jun 2021 09:27:49 +0000 (11:27 +0200)
yang.common.YangError is an attachment used to propagate RFC6020-defined
failure modes enforced by YANG Tools as exceptions. One such example is
YangInvalidValueException, which is reporting YANG constraint
violations.

Recognize these when mapping to RestconfDocumentedException, so that
their error-app-tag and error-message are properly propagated.

JIRA: NETCONF-786
Change-Id: Id6ea1877e03f42fc193942a4e2b83ecb6daeb631
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit d09a589e9a4ac5134083cf7c09a8e5370142f3ca)

13 files changed:
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfDocumentedException.java
restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfError.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java
restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyReader.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyReader.java
restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/AbstractBodyReaderTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/JsonBodyReaderTest.java
restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/XmlBodyReaderTest.java
restconf/restconf-nb-rfc8040/src/test/resources/modules/netconf786.yang [new file with mode: 0644]

index 1d7870173b8451f8a7d6d3f56cdd7fb20e346e8d..6bc02fe73372fe1e210b38cfbd6b80d10e8d4d0d 100644 (file)
@@ -21,7 +21,9 @@ import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
 import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
 import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.RpcError;
+import org.opendaylight.yangtools.yang.common.YangError;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.codec.YangInvalidValueException;
 
 /**
  * Unchecked exception to communicate error information, as defined in the ietf restcong draft, to be sent to the
@@ -229,6 +231,21 @@ public class RestconfDocumentedException extends WebApplicationException {
         return obj;
     }
 
+    /**
+     * Throw an instance of this exception if the specified exception has a {@link YangError} attachment.
+     *
+     * @param cause Proposed cause of a RestconfDocumented exception
+     */
+    public static void throwIfYangError(final Throwable cause) {
+        if (cause instanceof YangError) {
+            final YangError error = (YangError) cause;
+            throw new RestconfDocumentedException(cause, new RestconfError(ErrorType.valueOf(error.getErrorType()),
+                // FIXME: this is a special-case until we have YangError.getTag()
+                cause instanceof YangInvalidValueException ? ErrorTag.INVALID_VALUE : ErrorTag.MALFORMED_MESSAGE,
+                    error.getErrorMessage().orElse(null), error.getErrorAppTag().orElse(null)));
+        }
+    }
+
     private static List<RestconfError> convertToRestconfErrors(final Collection<? extends RpcError> rpcErrors) {
         final List<RestconfError> errorList = new ArrayList<>();
         if (rpcErrors != null) {
index d88652b9149af9600f45d319079659125eb9fe7d..13eb681aacf497f6bbd0a434f19b327b6bee0665 100644 (file)
@@ -11,6 +11,7 @@ import static java.util.Objects.requireNonNull;
 
 import java.io.Serializable;
 import java.util.Locale;
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.common.RpcError;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
 import org.slf4j.Logger;
@@ -30,6 +31,7 @@ public class RestconfError implements Serializable {
     private static final Logger LOG = LoggerFactory.getLogger(RestconfError.class);
     private static final long serialVersionUID = 1L;
 
+    // FIXME: remove this enum in favor of RpcError.ErrorType (or its equivalent)
     public enum ErrorType {
         /**
          * Errors relating to the transport layer.
@@ -59,6 +61,20 @@ public class RestconfError implements Serializable {
                 return APPLICATION;
             }
         }
+
+        public static @NonNull ErrorType valueOf(final RpcError.ErrorType errorType) {
+            switch (errorType) {
+                case PROTOCOL:
+                    return PROTOCOL;
+                case RPC:
+                    return RPC;
+                case TRANSPORT:
+                    return TRANSPORT;
+                case APPLICATION:
+                default:
+                    return APPLICATION;
+            }
+        }
     }
 
     public enum ErrorTag {
index 5d3e1da407a6898e342cb9c905e40e88baed2be3..5ddfb6a8c511b36876c862de3400db171786a579 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.netconf.sal.rest.impl;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.gson.stream.JsonReader;
 import java.io.IOException;
@@ -166,19 +167,15 @@ public class JsonNormalizedNodeBodyReader
     }
 
     private static void propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
-        if (exception instanceof RestconfDocumentedException) {
-            throw (RestconfDocumentedException)exception;
-        }
+        Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
+        LOG.debug("Error parsing json input", exception);
 
         if (exception instanceof ResultAlreadySetException) {
-            LOG.debug("Error parsing json input:", exception);
-
             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "
                     + "Are you creating multiple resources/subresources in POST request?", exception);
         }
 
-        LOG.debug("Error parsing json input", exception);
-
+        RestconfDocumentedException.throwIfYangError(exception);
         throw new RestconfDocumentedException("Error parsing input: " + exception.getMessage(), ErrorType.PROTOCOL,
                 ErrorTag.MALFORMED_MESSAGE, exception);
     }
index de744eeef818773169e8d07064b3ab8649978e00..8589e6f2bf980111139dd3dcfd59761ed9d096d7 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.netconf.sal.rest.impl;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
@@ -67,7 +68,7 @@ public class JsonToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider
 
     private static final Logger LOG = LoggerFactory.getLogger(JsonToPatchBodyReader.class);
 
-    public JsonToPatchBodyReader(ControllerContext controllerContext) {
+    public JsonToPatchBodyReader(final ControllerContext controllerContext) {
         super(controllerContext);
     }
 
@@ -118,15 +119,14 @@ public class JsonToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider
     }
 
     private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
-        if (exception instanceof RestconfDocumentedException) {
-            throw (RestconfDocumentedException)exception;
-        }
+        Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
+        LOG.debug("Error parsing json input", exception);
 
         if (exception instanceof ResultAlreadySetException) {
-            LOG.debug("Error parsing json input:", exception);
             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
         }
 
+        RestconfDocumentedException.throwIfYangError(exception);
         throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
                 ErrorTag.MALFORMED_MESSAGE, exception);
     }
index 8246d3bc9f013a3a1c9c827ad987bb8a550f2681..e799778815ed75dea9dc3bdd3ea11b7f730f9c6e 100644 (file)
@@ -97,7 +97,7 @@ public class XmlNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsPro
             throw e;
         } catch (final Exception e) {
             LOG.debug("Error parsing xml input", e);
-
+            RestconfDocumentedException.throwIfYangError(e);
             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
                     ErrorTag.MALFORMED_MESSAGE, e);
         }
index ef5be04b77866049f851e65befceff4af4c52071..2cf5acb85026314538b976b1530a4b3b345c2295 100644 (file)
@@ -110,7 +110,7 @@ public class XmlToPatchBodyReader extends AbstractIdentifierAwareJaxRsProvider i
             throw e;
         } catch (final Exception e) {
             LOG.debug("Error parsing xml input", e);
-
+            RestconfDocumentedException.throwIfYangError(e);
             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
                     ErrorTag.MALFORMED_MESSAGE, e);
         }
index c3c82df7561abb10e6dd5bb8dfe7bb1f9c757e48..83db709a49d5625e3ec9e2c0bef5ecd04b61181b 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.jersey.providers;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.Iterables;
 import com.google.gson.stream.JsonReader;
 import java.io.InputStream;
@@ -133,19 +134,15 @@ public class JsonNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyRead
     }
 
     private static void propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
-        if (exception instanceof RestconfDocumentedException) {
-            throw (RestconfDocumentedException)exception;
-        }
+        Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
+        LOG.debug("Error parsing json input", exception);
 
         if (exception instanceof ResultAlreadySetException) {
-            LOG.debug("Error parsing json input:", exception);
-
             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. "
                     + "Are you creating multiple resources/subresources in POST request?", exception);
         }
 
-        LOG.debug("Error parsing json input", exception);
-
+        RestconfDocumentedException.throwIfYangError(exception);
         throw new RestconfDocumentedException("Error parsing input: " + exception.getMessage(), ErrorType.PROTOCOL,
                 ErrorTag.MALFORMED_MESSAGE, exception);
     }
index 4403c1cb7f6de01f12365833962195c447baa65d..a045290abe060dd2b4a897da4a24f87235bd8850 100644 (file)
@@ -81,7 +81,7 @@ public class XmlNormalizedNodeBodyReader extends AbstractNormalizedNodeBodyReade
             throw e;
         } catch (final Exception e) {
             LOG.debug("Error parsing xml input", e);
-
+            RestconfDocumentedException.throwIfYangError(e);
             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
                     ErrorTag.MALFORMED_MESSAGE, e);
         }
index 71f48a4734e9c7dac5c922cb93257d87642f9ba6..142af2989639cc48a9d81b87410eac05ba723db6 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.restconf.nb.rfc8040.jersey.providers.patch;
 import static com.google.common.base.Preconditions.checkArgument;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
@@ -102,15 +103,14 @@ public class JsonToPatchBodyReader extends AbstractToPatchBodyReader {
     }
 
     private static RuntimeException propagateExceptionAs(final Exception exception) throws RestconfDocumentedException {
-        if (exception instanceof RestconfDocumentedException) {
-            throw (RestconfDocumentedException)exception;
-        }
+        Throwables.throwIfInstanceOf(exception, RestconfDocumentedException.class);
+        LOG.debug("Error parsing json input", exception);
 
         if (exception instanceof ResultAlreadySetException) {
-            LOG.debug("Error parsing json input:", exception);
             throw new RestconfDocumentedException("Error parsing json input: Failed to create new parse result data. ");
         }
 
+        RestconfDocumentedException.throwIfYangError(exception);
         throw new RestconfDocumentedException("Error parsing json input: " + exception.getMessage(), ErrorType.PROTOCOL,
                 ErrorTag.MALFORMED_MESSAGE, exception);
     }
index b1257c274f085b7625ea7936208f99afd39361a6..45806f4e228f8214b5cb069ba5fffb64221dbe4f 100644 (file)
@@ -7,24 +7,33 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.doReturn;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Optional;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedHashMap;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Request;
+import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
+import org.junit.function.ThrowingRunnable;
 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
 import org.opendaylight.mdsal.dom.api.DOMSchemaService;
 import org.opendaylight.mdsal.dom.spi.FixedDOMSchemaService;
 import org.opendaylight.restconf.common.context.NormalizedNodeContext;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.common.errors.RestconfError;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorTag;
+import org.opendaylight.restconf.common.errors.RestconfError.ErrorType;
 import org.opendaylight.restconf.common.patch.PatchContext;
 import org.opendaylight.restconf.nb.rfc8040.TestRestconfUtils;
 import org.opendaylight.restconf.nb.rfc8040.TestUtils;
@@ -123,4 +132,17 @@ public abstract class AbstractBodyReaderTest {
             .orElse(null);
     }
 
+    protected static void assertRangeViolation(final ThrowingRunnable runnable) {
+        final RestconfDocumentedException ex = assertThrows(RestconfDocumentedException.class, runnable);
+        assertEquals(Status.BAD_REQUEST, ex.getResponse().getStatusInfo());
+
+        final List<RestconfError> errors = ex.getErrors();
+        assertEquals(1, errors.size());
+
+        final RestconfError error = errors.get(0);
+        assertEquals(ErrorType.PROTOCOL, error.getErrorType());
+        assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
+        assertEquals("bar error app tag", error.getErrorAppTag());
+        assertEquals("bar error message", error.getErrorMessage());
+    }
 }
index 8480d1f4f75340a8eed2cf19a1e36b945bc0b11e..1d0278db98668b2a0ded2f8a5f88c1ddfd221765 100644 (file)
@@ -5,7 +5,6 @@
  * 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.restconf.nb.rfc8040.jersey.providers.test;
 
 import static org.junit.Assert.assertEquals;
@@ -13,10 +12,12 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.collect.Sets;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Optional;
 import javax.ws.rs.core.MediaType;
@@ -176,6 +177,19 @@ public class JsonBodyReaderTest extends AbstractBodyReaderTest {
         checkExpectValueNormalizeNodeContext(dataSchemaNode, returnValue, dataII);
     }
 
+    @Test
+    public void testRangeViolation() throws Exception {
+        mockBodyReader("netconf786:foo", this.jsonBodyReader, false);
+
+        final InputStream inputStream = new ByteArrayInputStream(("{\n"
+            + "  \"netconf786:foo\": {\n"
+            + "    \"bar\": 100\n"
+            + "  }\n"
+            + "}").getBytes(StandardCharsets.UTF_8));
+
+        assertRangeViolation(() -> jsonBodyReader.readFrom(null, null, null, this.mediaType, null, inputStream));
+    }
+
     private static void checkExpectValueNormalizeNodeContext(final DataSchemaNode dataSchemaNode,
             final NormalizedNodeContext nnContext, final YangInstanceIdentifier dataNodeIdent) {
         assertEquals(dataSchemaNode, nnContext.getInstanceIdentifierContext().getSchemaNode());
index 51926fee805b52a8429f72a92d00bf6ab40b4723..42873eca37bce8cd8f62c5551c4deba2804d998a 100644 (file)
@@ -5,7 +5,6 @@
  * 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.restconf.nb.rfc8040.jersey.providers.test;
 
 import static org.junit.Assert.assertEquals;
@@ -14,9 +13,11 @@ import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
 import com.google.common.collect.Sets;
+import java.io.ByteArrayInputStream;
 import java.io.File;
 import java.io.InputStream;
 import java.net.URI;
+import java.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Optional;
 import javax.ws.rs.core.MediaType;
@@ -285,4 +286,13 @@ public class XmlBodyReaderTest extends AbstractBodyReaderTest {
         }
     }
 
+    @Test
+    public void testRangeViolation() throws Exception {
+        mockBodyReader("netconf786:foo", this.xmlBodyReader, false);
+
+        final InputStream inputStream = new ByteArrayInputStream(
+            "<foo xmlns=\"netconf786\"><bar>100</bar></foo>".getBytes(StandardCharsets.UTF_8));
+
+        assertRangeViolation(() -> xmlBodyReader.readFrom(null, null, null, this.mediaType, null, inputStream));
+    }
 }
diff --git a/restconf/restconf-nb-rfc8040/src/test/resources/modules/netconf786.yang b/restconf/restconf-nb-rfc8040/src/test/resources/modules/netconf786.yang
new file mode 100644 (file)
index 0000000..3928f3e
--- /dev/null
@@ -0,0 +1,15 @@
+module netconf786 {
+  namespace netconf786;
+  prefix netconf786;
+
+  container foo {
+    leaf bar {
+      type uint32 {
+        range 1000..2000 {
+          error-message "bar error message";
+          error-app-tag "bar error app tag";
+        }
+      }
+    }
+  }
+}