From d09a589e9a4ac5134083cf7c09a8e5370142f3ca Mon Sep 17 00:00:00 2001 From: Robert Varga Date: Fri, 25 Jun 2021 23:26:01 +0200 Subject: [PATCH] Update RESTCONF error mapping 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 --- .../errors/RestconfDocumentedException.java | 17 ++++++++++++++ .../restconf/common/errors/RestconfError.java | 16 ++++++++++++++ .../impl/JsonNormalizedNodeBodyReader.java | 11 ++++------ .../sal/rest/impl/JsonToPatchBodyReader.java | 8 +++---- .../impl/XmlNormalizedNodeBodyReader.java | 2 +- .../sal/rest/impl/XmlToPatchBodyReader.java | 2 +- .../JsonNormalizedNodeBodyReader.java | 11 ++++------ .../XmlNormalizedNodeBodyReader.java | 2 +- .../patch/JsonToPatchBodyReader.java | 8 +++---- .../test/AbstractBodyReaderTest.java | 22 +++++++++++++++++++ .../providers/test/JsonBodyReaderTest.java | 15 +++++++++++++ .../providers/test/XmlBodyReaderTest.java | 12 ++++++++++ .../test/resources/modules/netconf786.yang | 15 +++++++++++++ 13 files changed, 116 insertions(+), 25 deletions(-) create mode 100644 restconf/restconf-nb-rfc8040/src/test/resources/modules/netconf786.yang diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfDocumentedException.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfDocumentedException.java index 1d7870173b..6bc02fe733 100644 --- a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfDocumentedException.java +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfDocumentedException.java @@ -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 convertToRestconfErrors(final Collection rpcErrors) { final List errorList = new ArrayList<>(); if (rpcErrors != null) { diff --git a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfError.java b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfError.java index d88652b914..13eb681aac 100644 --- a/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfError.java +++ b/restconf/restconf-common/src/main/java/org/opendaylight/restconf/common/errors/RestconfError.java @@ -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 { diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java index efa534d1ce..3b03a0d6fb 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonNormalizedNodeBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java index bee6e05f2a..bd1b47803c 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/JsonToPatchBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java index c4a86585c4..79ad5ad162 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlNormalizedNodeBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java index c87e204ca4..6b3e685acf 100644 --- a/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java +++ b/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/rest/impl/XmlToPatchBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyReader.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyReader.java index 0cd836d55b..6309301252 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyReader.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/JsonNormalizedNodeBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyReader.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyReader.java index bdde8bf53b..aa37f11b08 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyReader.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/XmlNormalizedNodeBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java index 3ed78638bb..fc1df8de86 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/patch/JsonToPatchBodyReader.java @@ -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); } diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/AbstractBodyReaderTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/AbstractBodyReaderTest.java index c3ab2457f0..7d2fee7327 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/AbstractBodyReaderTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/AbstractBodyReaderTest.java @@ -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 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()); + } } diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/JsonBodyReaderTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/JsonBodyReaderTest.java index f53c402d59..8e42387080 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/JsonBodyReaderTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/JsonBodyReaderTest.java @@ -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()); diff --git a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/XmlBodyReaderTest.java b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/XmlBodyReaderTest.java index 15ba17429a..ecc7d860a5 100644 --- a/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/XmlBodyReaderTest.java +++ b/restconf/restconf-nb-rfc8040/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/test/XmlBodyReaderTest.java @@ -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( + "100".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 index 0000000000..3928f3ece8 --- /dev/null +++ b/restconf/restconf-nb-rfc8040/src/test/resources/modules/netconf786.yang @@ -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"; + } + } + } + } +} -- 2.36.6