JSON: Resolve 500 response from device exception 04/107904/27
authorPeter Suna <peter.suna@pantheon.tech>
Mon, 18 Sep 2023 11:21:18 +0000 (13:21 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 12 Jan 2024 14:03:56 +0000 (14:03 +0000)
Utilize a custom JsonWriter to prepare the ietf-restconf
error response body. To emit the error-path value,
use the JSONCodec from the device to generate the correct
path format based on the device's model context.

JIRA: NETCONF-1130
Change-Id: Id82849cabf3fd99b22b4d95eaf9ff5a3ce815b8b
Signed-off-by: Peter Suna <peter.suna@pantheon.tech>
Signed-off-by: Yaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Signed-off-by: Ivan Hrasko <ivan.hrasko@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/JsonStreamWriterWithDisabledValidation.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapper.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/StreamWriterWithDisabledValidation.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/XmlStreamWriterWithDisabledValidation.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/nb/rfc8040/jersey/providers/errors/RestconfDocumentedExceptionMapperTest.java

index 0c6a790af6331308a88c13d927a916eef6372b23..a2472f235f9dea49a92b3ba756e951970b539852 100644 (file)
@@ -21,6 +21,7 @@ import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
 /**
  * JSON stream-writer with disabled leaf-type validation for specified QName.
  */
+// FIXME remove this class
 final class JsonStreamWriterWithDisabledValidation extends StreamWriterWithDisabledValidation {
     private static final int DEFAULT_INDENT_SPACES_NUM = 2;
     private static final XMLNamespace IETF_RESTCONF_URI = Errors.QNAME.getModule().getNamespace();
index 13e27e5b74bf97055f7b7901aa9e2113ba457b37..a4ebf256e890dbbe339c30a169d0a0e2118dbd8b 100644 (file)
@@ -33,6 +33,7 @@ import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.nb.jaxrs.JaxRsMediaTypes;
 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
+import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.restconf.server.spi.DatabindProvider;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.errors.Errors;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.errors.errors.Error;
@@ -42,6 +43,7 @@ import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
 import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
+import org.opendaylight.yangtools.yang.data.codec.gson.JsonWriterFactory;
 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
 import org.slf4j.Logger;
@@ -66,8 +68,10 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     private static final QName ERROR_TAG_QNAME = qnameOf("error-tag");
     private static final QName ERROR_APP_TAG_QNAME = qnameOf("error-app-tag");
     private static final QName ERROR_MESSAGE_QNAME = qnameOf("error-message");
-    private static final QName ERROR_PATH_QNAME = qnameOf("error-path");
+    // FIXME make this private
     static final QName ERROR_INFO_QNAME = qnameOf("error-info");
+    private static final QName ERROR_PATH_QNAME = qnameOf("error-path");
+    private static final int DEFAULT_INDENT_SPACES_NUM = 2;
 
     private final DatabindProvider databindProvider;
 
@@ -101,7 +105,7 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
         final String serializedResponseBody;
         final MediaType responseMediaType = transformToResponseMediaType(getSupportedMediaType());
         if (JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON.equals(responseMediaType)) {
-            serializedResponseBody = serializeErrorsContainerToJson(errorsContainer);
+            serializedResponseBody = serializeExceptionToJson(exception, databindProvider);
         } else {
             serializedResponseBody = serializeErrorsContainerToXml(errorsContainer);
         }
@@ -167,19 +171,55 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     }
 
     /**
-     * Serialization of the errors container into JSON representation.
+     * Serialization exceptions into JSON representation.
      *
-     * @param errorsContainer To be serialized errors container.
-     * @return JSON representation of the errors container.
+     * @param exception To be serialized exception.
+     * @param databindProvider Holder of current {@code DatabindContext}.
+     * @return JSON representation of the exception.
      */
-    private String serializeErrorsContainerToJson(final ContainerNode errorsContainer) {
-        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-             OutputStreamWriter streamStreamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
-        ) {
-            return writeNormalizedNode(errorsContainer, outputStream,
-                new JsonStreamWriterWithDisabledValidation(databindProvider.currentDatabind(), streamStreamWriter));
+    private static String serializeExceptionToJson(final RestconfDocumentedException exception,
+            final DatabindProvider databindProvider) {
+        try (var outputStream = new ByteArrayOutputStream();
+             var streamWriter = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
+             var jsonWriter = JsonWriterFactory.createJsonWriter(streamWriter, DEFAULT_INDENT_SPACES_NUM)) {
+            final var currentDatabindContext = exception.modelContext() != null
+                ? DatabindContext.ofModel(exception.modelContext()) : databindProvider.currentDatabind();
+            jsonWriter.beginObject();
+            final var errors = exception.getErrors();
+            if (errors != null && !errors.isEmpty()) {
+                jsonWriter.name(Errors.QNAME.getLocalName()).beginObject();
+                jsonWriter.name(Error.QNAME.getLocalName()).beginArray();
+                for (final var error : errors) {
+                    jsonWriter.beginObject()
+                        .name(ERROR_TAG_QNAME.getLocalName()).value(error.getErrorTag().elementBody());
+                    final var errorAppTag = error.getErrorAppTag();
+                    if (errorAppTag != null) {
+                        jsonWriter.name(ERROR_APP_TAG_QNAME.getLocalName()).value(errorAppTag);
+                    }
+                    final var errorInfo = error.getErrorInfo();
+                    if (errorInfo != null) {
+                        jsonWriter.name(ERROR_INFO_QNAME.getLocalName()).value(errorInfo);
+                    }
+                    final var errorMessage = error.getErrorMessage();
+                    if (errorMessage != null) {
+                        jsonWriter.name(ERROR_MESSAGE_QNAME.getLocalName()).value(errorMessage);
+                    }
+                    final var errorPath = error.getErrorPath();
+                    if (errorPath != null) {
+                        jsonWriter.name(ERROR_PATH_QNAME.getLocalName());
+                        currentDatabindContext.jsonCodecs().instanceIdentifierCodec()
+                            .writeValue(jsonWriter, errorPath);
+                    }
+                    jsonWriter.name(ERROR_TYPE_QNAME.getLocalName()).value(error.getErrorType().elementBody());
+                    jsonWriter.endObject();
+                }
+                jsonWriter.endArray().endObject();
+            }
+            jsonWriter.endObject();
+            streamWriter.flush();
+            return outputStream.toString(StandardCharsets.UTF_8);
         } catch (IOException e) {
-            throw new IllegalStateException("Cannot close some of the output JSON writers", e);
+            throw new IllegalStateException("Error while serializing restconf exception into JSON", e);
         }
     }
 
index 37c2523b404bd6f55cfdb80bd08117fa7b23e895..e81b39517f4c372acbf49d9df025df1b19541576 100644 (file)
@@ -21,6 +21,7 @@ import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference
  * the schema and thus will expect a LeafSchemaNode but the schema has a ContainerSchemaNode so, to avoid an error,
  * we override the leafNode behavior for error-info.
  */
+// FIXME remove this class
 abstract class StreamWriterWithDisabledValidation extends ForwardingNormalizedNodeStreamWriter {
     private boolean inOurLeaf;
 
index b8303d1afd5ff16fe5296e61b0590d4bd88080f1..18ace4e71ab6b9ff9a03b06b99a0adc2b3a79ce4 100644 (file)
@@ -23,6 +23,7 @@ import org.opendaylight.yangtools.yang.data.codec.xml.XMLStreamNormalizedNodeStr
 /**
  * XML stream-writer with disabled leaf-type validation for specified QName.
  */
+// FIXME remove this class
 final class XmlStreamWriterWithDisabledValidation extends StreamWriterWithDisabledValidation {
     private static final XMLOutputFactory XML_FACTORY;
 
index bda623865de6e6c61b7b218e13823fd0ce8a4c55..a93f116b5c1d9f92c8741eb3219dad0804aec75e 100644 (file)
@@ -216,7 +216,7 @@ public class RestconfDocumentedExceptionMapperTest {
     }
 
     @Test
-    public void testFormatingJson() throws JSONException {
+    public void testFormattingJson() throws JSONException {
         assumeTrue(expectedResponse.getMediaType().equals(JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON));
 
         exceptionMapper.setHttpHeaders(httpHeaders);
@@ -246,4 +246,4 @@ public class RestconfDocumentedExceptionMapperTest {
             JSONAssert.assertEquals(expectedResponseInJson, actualResponseInJson, true);
         }
     }
-}
\ No newline at end of file
+}