Update RESTCONF error mapping 83/96683/4
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 25 Jun 2021 21:26:01 +0000 (23:26 +0200)
committerRobert Varga <robert.varga@pantheon.tech>
Mon, 28 Jun 2021 12:20:30 +0000 (14:20 +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>
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 efa534d1ce395d9513ee77d08104747b15558120..3b03a0d6fb30abc601c77ea8480e329c02c9c968 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;
@@ -164,19 +165,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 bee6e05f2a3631953bbf1d52c08eb9639b87d7f4..bd1b47803c307c09a5c04004dcab2f53f0628f77 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.netconf.sal.rest.impl;
 
 import static com.google.common.base.Verify.verify;
 
+import com.google.common.base.Throwables;
 import com.google.common.collect.ImmutableList;
 import com.google.gson.stream.JsonReader;
 import com.google.gson.stream.JsonToken;
@@ -122,15 +123,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 c4a86585c437bc44ef0c3387408a4f6ccf1f2a4d..79ad5ad162cb951609859a89e292763889be4db6 100644 (file)
@@ -98,7 +98,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 c87e204ca4774fcd239f24d3f0ecad8ec20ca230..6b3e685acf1862aa86ea0b423cf68735de72037e 100644 (file)
@@ -114,7 +114,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 0cd836d55b8b1ec6e036fa1094e7ced45206258a..63093012522b37ff59fa6d8cb4a10710b467a36b 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;
@@ -134,19 +135,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 bdde8bf53b97432ee4215a6a08ed05d0a2649a71..aa37f11b08bd89aa162048f814f7b2075d30fc5a 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 3ed78638bbc625c983fc3fce34ccbf09789492eb..fc1df8de86e4e231292b9c81e286d287daaa1079 100644 (file)
@@ -11,6 +11,7 @@ import static com.google.common.base.Preconditions.checkArgument;
 import static com.google.common.base.Verify.verify;
 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;
@@ -100,15 +101,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 c3ab2457f0e00c0c87b12706623541f49e03a20b..7d2fee7327d37da690763e6169fa0698eb2c0aba 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;
@@ -119,4 +128,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 f53c402d594e59c57e3f4ab89cf966f2f05bf84b..8e423870803d3bf7a85c389f07ddf36b8e965281 100644 (file)
@@ -13,9 +13,11 @@ 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.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Optional;
 import javax.ws.rs.core.MediaType;
@@ -176,6 +178,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 15ba17429a8688f11fbce9bf120baecc809b3670..ecc7d860a573daf20ea947f447126dbb42fd3ce5 100644 (file)
@@ -13,8 +13,10 @@ 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.nio.charset.StandardCharsets;
 import java.util.Collection;
 import java.util.Optional;
 import javax.ws.rs.core.MediaType;
@@ -283,4 +285,14 @@ 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";
+        }
+      }
+    }
+  }
+}