JSON: Resolve 500 response from device exception
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / jersey / providers / errors / RestconfDocumentedExceptionMapper.java
index 8b94593e5fda2c539bde2c174e0abcdb10f4dd04..a4ebf256e890dbbe339c30a169d0a0e2118dbd8b 100644 (file)
@@ -7,7 +7,7 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.jersey.providers.errors;
 
-import static com.google.common.base.Preconditions.checkState;
+import static java.util.Objects.requireNonNull;
 import static org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.$YangModuleInfoImpl.qnameOf;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -29,56 +29,62 @@ 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 org.opendaylight.restconf.common.ErrorTags;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
-import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
-import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler;
+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;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.XMLNamespace;
 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.opendaylight.yangtools.yang.model.api.SchemaPath;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- *  Mapper that is responsible for transformation of thrown {@link RestconfDocumentedException} to errors structure
- *  that is modelled by RESTCONF module (see section 8 of RFC-8040).
+ * An {@link ExceptionMapper} that is responsible for transformation of thrown {@link RestconfDocumentedException} to
+ * {@code errors} structure that is modelled by RESTCONF module (see section 8 of RFC-8040).
+ *
+ * @see Errors
  */
+// FIXME: NETCONF-1188: eliminate the need for this class by having a separate exception which a has a HTTP status and
+//                      optionally holds an ErrorsBody -- i.e. the equivalent of Errors, perhaps as NormalizedNode,
+//                      with sufficient context to send it to JSON or XML -- very similar to a NormalizedNodePayload
+@Deprecated
 @Provider
 public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<RestconfDocumentedException> {
     private static final Logger LOG = LoggerFactory.getLogger(RestconfDocumentedExceptionMapper.class);
     private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON_TYPE;
     private static final Status DEFAULT_STATUS_CODE = Status.INTERNAL_SERVER_ERROR;
-    // Note: we are using container's QName reference to trim imports
-    private static final SchemaPath ERRORS_GROUPING_PATH = SchemaPath.create(true, Errors.QNAME);
     private static final QName ERROR_TYPE_QNAME = qnameOf("error-type");
     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_INFO_QNAME = qnameOf("error-info");
+    // 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 XMLNamespace IETF_RESTCONF_URI = Errors.QNAME.getModule().getNamespace();
+    private static final int DEFAULT_INDENT_SPACES_NUM = 2;
+
+    private final DatabindProvider databindProvider;
 
     @Context
     private HttpHeaders headers;
-    private final SchemaContextHandler schemaContextHandler;
 
     /**
      * Initialization of the exception mapper.
      *
-     * @param schemaContextHandler Handler that provides actual schema context.
+     * @param databindProvider A {@link DatabindProvider}
      */
-    public RestconfDocumentedExceptionMapper(final SchemaContextHandler schemaContextHandler) {
-        this.schemaContextHandler = schemaContextHandler;
+    public RestconfDocumentedExceptionMapper(final DatabindProvider databindProvider) {
+        this.databindProvider = requireNonNull(databindProvider);
     }
 
     @Override
@@ -98,8 +104,8 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
         final ContainerNode errorsContainer = buildErrorsContainer(exception);
         final String serializedResponseBody;
         final MediaType responseMediaType = transformToResponseMediaType(getSupportedMediaType());
-        if (MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE.equals(responseMediaType)) {
-            serializedResponseBody = serializeErrorsContainerToJson(errorsContainer);
+        if (JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON.equals(responseMediaType)) {
+            serializedResponseBody = serializeExceptionToJson(exception, databindProvider);
         } else {
             serializedResponseBody = serializeErrorsContainerToXml(errorsContainer);
         }
@@ -165,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(
-                ERROR_INFO_QNAME, streamStreamWriter, ERRORS_GROUPING_PATH, IETF_RESTCONF_URI, schemaContextHandler));
+    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);
         }
     }
 
@@ -189,8 +231,8 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
      */
     private String serializeErrorsContainerToXml(final ContainerNode errorsContainer) {
         try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
-            return writeNormalizedNode(errorsContainer, outputStream, new XmlStreamWriterWithDisabledValidation(
-                ERROR_INFO_QNAME, outputStream, ERRORS_GROUPING_PATH, schemaContextHandler));
+            return writeNormalizedNode(errorsContainer, outputStream,
+                new XmlStreamWriterWithDisabledValidation(databindProvider.currentDatabind(), outputStream));
         } catch (IOException e) {
             throw new IllegalStateException("Cannot close some of the output XML writers", e);
         }
@@ -216,29 +258,24 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
      * @return Derived status code.
      */
     private static Status getResponseStatusCode(final RestconfDocumentedException exception) {
-        final Status status = exception.getStatus();
-        if (status != null) {
-            // status code that is specified directly as field in exception has the precedence over error entries
-            return status;
-        }
-
-        final List<RestconfError> errors = exception.getErrors();
+        final var errors = exception.getErrors();
         if (errors.isEmpty()) {
             // if the module, that thrown exception, doesn't specify status code, it is treated as internal
             // server error
             return DEFAULT_STATUS_CODE;
         }
 
-        final Set<Status> allStatusCodesOfErrorEntries = errors.stream()
+        final var allStatusCodesOfErrorEntries = errors.stream()
                 .map(restconfError -> ErrorTags.statusOf(restconfError.getErrorTag()))
                 // we would like to preserve iteration order in collected entries - hence usage of LinkedHashSet
                 .collect(Collectors.toCollection(LinkedHashSet::new));
         // choosing of the first status code from appended errors, if there are different status codes in error
         // entries, we should create WARN message
         if (allStatusCodesOfErrorEntries.size() > 1) {
-            LOG.warn("An unexpected error occurred during translation of exception {} to response: "
-                    + "Different status codes have been found in appended error entries: {}. The first error "
-                    + "entry status code is chosen for response.", exception, allStatusCodesOfErrorEntries);
+            LOG.warn("""
+                An unexpected error occurred during translation of exception {} to response: Different status codes
+                have been found in appended error entries: {}. The first error entry status code is chosen for
+                response.""", exception, allStatusCodesOfErrorEntries);
         }
         return allStatusCodesOfErrorEntries.iterator().next();
     }
@@ -297,9 +334,7 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
     }
 
     private static MediaType chooseAndCheckMediaType(final List<MediaType> options) {
-        final Optional<MediaType> mediaTypeOpt = chooseMediaType(options);
-        checkState(mediaTypeOpt.isPresent());
-        return mediaTypeOpt.get();
+        return chooseMediaType(options).orElseThrow(IllegalStateException::new);
     }
 
     /**
@@ -330,9 +365,9 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
      */
     private static MediaType transformToResponseMediaType(final MediaType mediaTypeBase) {
         if (isJsonCompatibleMediaType(mediaTypeBase)) {
-            return MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE;
+            return JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON;
         } else if (isXmlCompatibleMediaType(mediaTypeBase)) {
-            return MediaTypes.APPLICATION_YANG_DATA_XML_TYPE;
+            return JaxRsMediaTypes.APPLICATION_YANG_DATA_XML;
         } else {
             throw new IllegalStateException(String.format("Unexpected input media-type %s "
                     + "- it should be JSON/XML compatible type.", mediaTypeBase));
@@ -345,14 +380,14 @@ public final class RestconfDocumentedExceptionMapper implements ExceptionMapper<
 
     private static boolean isJsonCompatibleMediaType(final MediaType mediaType) {
         return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE)
-                || mediaType.isCompatible(MediaTypes.APPLICATION_YANG_DATA_JSON_TYPE)
-                || mediaType.isCompatible(MediaTypes.APPLICATION_YANG_PATCH_JSON_TYPE);
+                || mediaType.isCompatible(JaxRsMediaTypes.APPLICATION_YANG_DATA_JSON)
+                || mediaType.isCompatible(JaxRsMediaTypes.APPLICATION_YANG_PATCH_JSON);
     }
 
     private static boolean isXmlCompatibleMediaType(final MediaType mediaType) {
         return mediaType.isCompatible(MediaType.APPLICATION_XML_TYPE)
-                || mediaType.isCompatible(MediaTypes.APPLICATION_YANG_DATA_XML_TYPE)
-                || mediaType.isCompatible(MediaTypes.APPLICATION_YANG_PATCH_XML_TYPE);
+                || mediaType.isCompatible(JaxRsMediaTypes.APPLICATION_YANG_DATA_XML)
+                || mediaType.isCompatible(JaxRsMediaTypes.APPLICATION_YANG_PATCH_XML);
     }
 
     /**