XML: Resolve 500 response from device exception 11/108611/16
authorYaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
Tue, 24 Oct 2023 07:45:17 +0000 (10:45 +0300)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Fri, 12 Jan 2024 14:03:56 +0000 (14:03 +0000)
Utilize a custom XmlWriter to prepare the ietf-restconf
error response body. To emit the error-path value,
use the XmlCodec from the device to generate the correct
path format based on the device's model context.

JIRA: NETCONF-1130
Change-Id: I04ae0df51475d7bd49296b7e166fab835afdbaf3
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/RestconfDocumentedExceptionMapper.java

index a4ebf256e890dbbe339c30a169d0a0e2118dbd8b..52db8ff6ea4d1831d086d1c14d5f5d1924d7424d 100644 (file)
@@ -15,6 +15,8 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.nio.charset.StandardCharsets;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -29,8 +31,14 @@ 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.stream.XMLOutputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
 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;
@@ -38,14 +46,7 @@ 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;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
-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;
 import org.slf4j.LoggerFactory;
 
@@ -72,6 +73,12 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     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 static final XMLOutputFactory XML_OUTPUT_FACTORY;
+
+    static {
+        XML_OUTPUT_FACTORY = XMLOutputFactory.newFactory();
+        XML_OUTPUT_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
+    }
 
     private final DatabindProvider databindProvider;
 
@@ -101,13 +108,12 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
             LOG.warn("Input exception has a family of 4xx but does not contain any descriptive errors", exception);
         }
 
-        final ContainerNode errorsContainer = buildErrorsContainer(exception);
         final String serializedResponseBody;
         final MediaType responseMediaType = transformToResponseMediaType(getSupportedMediaType());
         if (JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON.equals(responseMediaType)) {
             serializedResponseBody = serializeExceptionToJson(exception, databindProvider);
         } else {
-            serializedResponseBody = serializeErrorsContainerToXml(errorsContainer);
+            serializedResponseBody = serializeExceptionToXml(exception, databindProvider);
         }
 
         final Response preparedResponse = Response.status(responseStatus)
@@ -119,57 +125,6 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
         return preparedResponse;
     }
 
-    /**
-     * Filling up of the errors container with data from input {@link RestconfDocumentedException}.
-     *
-     * @param exception Thrown exception.
-     * @return Built errors container.
-     */
-    private static ContainerNode buildErrorsContainer(final RestconfDocumentedException exception) {
-        return Builders.containerBuilder()
-            .withNodeIdentifier(NodeIdentifier.create(Errors.QNAME))
-            .withChild(Builders.unkeyedListBuilder()
-                .withNodeIdentifier(NodeIdentifier.create(Error.QNAME))
-                .withValue(exception.getErrors().stream()
-                    .map(RestconfDocumentedExceptionMapper::createErrorEntry)
-                    .collect(Collectors.toList()))
-                .build())
-            .build();
-    }
-
-    /**
-     * Building of one error entry using provided {@link RestconfError}.
-     *
-     * @param restconfError Error details.
-     * @return Built list entry.
-     */
-    private static UnkeyedListEntryNode createErrorEntry(final RestconfError restconfError) {
-        // filling in mandatory leafs
-        final var entryBuilder = Builders.unkeyedListEntryBuilder()
-            .withNodeIdentifier(NodeIdentifier.create(Error.QNAME))
-            .withChild(ImmutableNodes.leafNode(ERROR_TYPE_QNAME, restconfError.getErrorType().elementBody()))
-            .withChild(ImmutableNodes.leafNode(ERROR_TAG_QNAME, restconfError.getErrorTag().elementBody()));
-
-        // filling in optional fields
-        if (restconfError.getErrorMessage() != null) {
-            entryBuilder.withChild(ImmutableNodes.leafNode(ERROR_MESSAGE_QNAME, restconfError.getErrorMessage()));
-        }
-        if (restconfError.getErrorAppTag() != null) {
-            entryBuilder.withChild(ImmutableNodes.leafNode(ERROR_APP_TAG_QNAME, restconfError.getErrorAppTag()));
-        }
-        if (restconfError.getErrorInfo() != null) {
-            // Oddly, error-info is defined as an empty container in the restconf yang. Apparently the
-            // intention is for implementors to define their own data content so we'll just treat it as a leaf
-            // with string data.
-            entryBuilder.withChild(ImmutableNodes.leafNode(ERROR_INFO_QNAME, restconfError.getErrorInfo()));
-        }
-
-        if (restconfError.getErrorPath() != null) {
-            entryBuilder.withChild(ImmutableNodes.leafNode(ERROR_PATH_QNAME, restconfError.getErrorPath()));
-        }
-        return entryBuilder.build();
-    }
-
     /**
      * Serialization exceptions into JSON representation.
      *
@@ -224,28 +179,77 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     }
 
     /**
-     * Serialization of the errors container into XML representation.
+     * Serialization exceptions into XML representation.
      *
-     * @param errorsContainer To be serialized errors container.
-     * @return XML representation of the errors container.
+     * @param exception To be serialized exception.
+     * @param databindProvider Holder of current {@code DatabindContext}.
+     * @return XML representation of the exception.
      */
-    private String serializeErrorsContainerToXml(final ContainerNode errorsContainer) {
-        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-            return writeNormalizedNode(errorsContainer, outputStream,
-                new XmlStreamWriterWithDisabledValidation(databindProvider.currentDatabind(), outputStream));
-        } catch (IOException e) {
-            throw new IllegalStateException("Cannot close some of the output XML writers", e);
-        }
-    }
+    private static String serializeExceptionToXml(final RestconfDocumentedException exception,
+            final DatabindProvider databindProvider) {
+        try (var outputStream = new ByteArrayOutputStream()) {
+            final var xmlWriter = XML_OUTPUT_FACTORY.createXMLStreamWriter(outputStream,
+                StandardCharsets.UTF_8.name());
+            xmlWriter.writeStartDocument();
+            xmlWriter.writeStartElement(Errors.QNAME.getLocalName());
+            xmlWriter.writeNamespace("xmlns", Errors.QNAME.getNamespace().toString());
+            if (exception.getErrors() != null && !exception.getErrors().isEmpty()) {
+                for (final var error : exception.getErrors()) {
+                    xmlWriter.writeStartElement(Error.QNAME.getLocalName());
+                    // Write error-type element
+                    xmlWriter.writeStartElement(ERROR_TYPE_QNAME.getLocalName());
+                    xmlWriter.writeCharacters(error.getErrorType().elementBody());
+                    xmlWriter.writeEndElement();
 
-    private static String writeNormalizedNode(final NormalizedNode errorsContainer,
-            final ByteArrayOutputStream outputStream, final StreamWriterWithDisabledValidation streamWriter) {
-        try (NormalizedNodeWriter nnWriter = NormalizedNodeWriter.forStreamWriter(streamWriter)) {
-            nnWriter.write(errorsContainer);
-        } catch (IOException e) {
-            throw new IllegalStateException("Cannot write error response body", e);
+                    if (error.getErrorPath() != null) {
+                        xmlWriter.writeStartElement(ERROR_PATH_QNAME.getLocalName());
+                        databindProvider.currentDatabind().xmlCodecs().instanceIdentifierCodec()
+                            .writeValue(xmlWriter, error.getErrorPath());
+                        xmlWriter.writeEndElement();
+                    }
+                    if (error.getErrorMessage() != null) {
+                        xmlWriter.writeStartElement(ERROR_MESSAGE_QNAME.getLocalName());
+                        xmlWriter.writeCharacters(error.getErrorMessage());
+                        xmlWriter.writeEndElement();
+                    }
+
+                    // Write error-tag element
+                    xmlWriter.writeStartElement(ERROR_TAG_QNAME.getLocalName());
+                    xmlWriter.writeCharacters(error.getErrorTag().elementBody());
+                    xmlWriter.writeEndElement();
+
+                    if (error.getErrorAppTag() != null) {
+                        xmlWriter.writeStartElement(ERROR_APP_TAG_QNAME.getLocalName());
+                        xmlWriter.writeCharacters(error.getErrorAppTag());
+                        xmlWriter.writeEndElement();
+                    }
+                    if (error.getErrorInfo() != null) {
+                        xmlWriter.writeStartElement(ERROR_INFO_QNAME.getLocalName());
+                        xmlWriter.writeCharacters(error.getErrorInfo());
+                        xmlWriter.writeEndElement();
+                    }
+                    xmlWriter.writeEndElement();
+                }
+            }
+            xmlWriter.writeEndElement();
+            xmlWriter.writeEndDocument();
+            xmlWriter.close();
+
+            final var transformerFactory = TransformerFactory.newInstance();
+            final var transformer = transformerFactory.newTransformer();
+            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
+            // 2 spaces for indentation
+            transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount",
+                String.valueOf(DEFAULT_INDENT_SPACES_NUM));
+            transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
+            final var xmlSource = new StreamSource(new StringReader(outputStream.toString(StandardCharsets.UTF_8)));
+            final var stringWriter = new StringWriter();
+            final var streamResult = new StreamResult(stringWriter);
+            transformer.transform(xmlSource, streamResult);
+            return stringWriter.toString();
+        } catch (IOException | XMLStreamException | TransformerException e) {
+            throw new IllegalStateException("Error while serializing restconf exception into XML", e);
         }
-        return outputStream.toString(StandardCharsets.UTF_8);
     }
 
     /**