Eliminate NormalizedNodePayload
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / jaxrs / JaxRsRestconf.java
index 58ee874343ab49f8425c445ea36ffcadd7ba10f1..8e8135ad4ef4d803d8a79d46d6aadaa66170e2cc 100644 (file)
@@ -9,12 +9,16 @@ package org.opendaylight.restconf.nb.jaxrs;
 
 import static java.util.Objects.requireNonNull;
 
+import java.io.IOException;
 import java.io.InputStream;
-import java.time.Clock;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
+import java.io.Reader;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.text.ParseException;
+import java.util.Date;
 import java.util.List;
-import java.util.function.Function;
+import javax.inject.Singleton;
+import javax.ws.rs.BadRequestException;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.Encoded;
@@ -25,57 +29,128 @@ import javax.ws.rs.PUT;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
 import javax.ws.rs.container.AsyncResponse;
 import javax.ws.rs.container.Suspended;
+import javax.ws.rs.core.CacheControl;
 import javax.ws.rs.core.Context;
+import javax.ws.rs.core.EntityTag;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import javax.ws.rs.core.Response.Status;
+import javax.ws.rs.core.Response.ResponseBuilder;
 import javax.ws.rs.core.UriInfo;
+import javax.ws.rs.ext.ParamConverter;
+import javax.ws.rs.ext.ParamConverterProvider;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.FormattableBody;
+import org.opendaylight.restconf.api.HttpStatusCode;
 import org.opendaylight.restconf.api.MediaTypes;
+import org.opendaylight.restconf.api.QueryParameters;
+import org.opendaylight.restconf.api.query.PrettyPrintParam;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
-import org.opendaylight.restconf.common.patch.PatchStatusContext;
-import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonChildBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonDataPostBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlDataPostBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
-import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
-import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
+import org.opendaylight.restconf.nb.rfc8040.ErrorTagMapping;
+import org.opendaylight.restconf.nb.rfc8040.URLConstants;
+import org.opendaylight.restconf.server.api.ConfigurationMetadata;
+import org.opendaylight.restconf.server.api.CreateResourceResult;
+import org.opendaylight.restconf.server.api.DataGetResult;
+import org.opendaylight.restconf.server.api.DataPatchResult;
 import org.opendaylight.restconf.server.api.DataPostResult;
-import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
-import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
 import org.opendaylight.restconf.server.api.DataPutResult;
-import org.opendaylight.restconf.server.api.OperationsGetResult;
+import org.opendaylight.restconf.server.api.DataYangPatchResult;
+import org.opendaylight.restconf.server.api.InvokeResult;
+import org.opendaylight.restconf.server.api.JsonChildBody;
+import org.opendaylight.restconf.server.api.JsonDataPostBody;
+import org.opendaylight.restconf.server.api.JsonOperationInputBody;
+import org.opendaylight.restconf.server.api.JsonPatchBody;
+import org.opendaylight.restconf.server.api.JsonResourceBody;
+import org.opendaylight.restconf.server.api.ModulesGetResult;
+import org.opendaylight.restconf.server.api.OperationInputBody;
+import org.opendaylight.restconf.server.api.PatchStatusContext;
 import org.opendaylight.restconf.server.api.RestconfServer;
-import org.opendaylight.restconf.server.spi.OperationOutput;
+import org.opendaylight.restconf.server.api.ServerRequest;
+import org.opendaylight.restconf.server.api.XmlChildBody;
+import org.opendaylight.restconf.server.api.XmlDataPostBody;
+import org.opendaylight.restconf.server.api.XmlOperationInputBody;
+import org.opendaylight.restconf.server.api.XmlPatchBody;
+import org.opendaylight.restconf.server.api.XmlResourceBody;
+import org.opendaylight.restconf.server.spi.YangPatchStatusBody;
 import org.opendaylight.yangtools.yang.common.Empty;
-import org.opendaylight.yangtools.yang.common.Revision;
+import org.opendaylight.yangtools.yang.common.YangConstants;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Baseline RESTCONF implementation with JAX-RS. Interfaces to a {@link RestconfServer}.
+ * Baseline RESTCONF implementation with JAX-RS. Interfaces to a {@link RestconfServer}. Since we need {@link ApiPath}
+ * arguments, we also implement {@link ParamConverterProvider} and provide the appropriate converter. This has the nice
+ * side-effect of suppressing <a href="https://github.com/eclipse-ee4j/jersey/issues/3700">Jersey warnings</a>.
  */
 @Path("/")
-public final class JaxRsRestconf {
+@Singleton
+public final class JaxRsRestconf implements ParamConverterProvider {
     private static final Logger LOG = LoggerFactory.getLogger(JaxRsRestconf.class);
-    private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
+    private static final CacheControl NO_CACHE = CacheControl.valueOf("no-cache");
+    private static final ParamConverter<ApiPath> API_PATH_CONVERTER = new ParamConverter<>() {
+        @Override
+        public ApiPath fromString(final String value) {
+            final var str = nonnull(value);
+            try {
+                return ApiPath.parseUrl(str);
+            } catch (ParseException e) {
+                throw new IllegalArgumentException(e.getMessage(), e);
+            }
+        }
+
+        @Override
+        public String toString(final ApiPath value) {
+            return nonnull(value).toString();
+        }
+
+        private static <T> @NonNull T nonnull(final @Nullable T value) {
+            if (value == null) {
+                throw new IllegalArgumentException("value must not be null");
+            }
+            return value;
+        }
+    };
 
-    private final RestconfServer server;
+    private final @NonNull RestconfServer server;
+    private final @NonNull ServerRequest emptyRequest;
+    private final @NonNull PrettyPrintParam prettyPrint;
+    private final @NonNull ErrorTagMapping errorTagMapping;
 
-    public JaxRsRestconf(final RestconfServer server) {
+    public JaxRsRestconf(final RestconfServer server, final ErrorTagMapping errorTagMapping,
+            final PrettyPrintParam prettyPrint) {
         this.server = requireNonNull(server);
+        this.errorTagMapping = requireNonNull(errorTagMapping);
+        this.prettyPrint = requireNonNull(prettyPrint);
+        emptyRequest = ServerRequest.of(QueryParameters.of(), prettyPrint);
+
+        LOG.info("RESTCONF data-missing condition is reported as HTTP status {}", switch (errorTagMapping) {
+            case ERRATA_5565 -> "404 (Errata 5565)";
+            case RFC8040 -> "409 (RFC8040)";
+        });
+    }
+
+    private @NonNull ServerRequest requestOf(final UriInfo uriInfo) {
+        final QueryParameters params;
+        try {
+            params = QueryParameters.ofMultiValue(uriInfo.getQueryParameters());
+        } catch (IllegalArgumentException e) {
+            throw new BadRequestException(e.getMessage(), e);
+        }
+        return params.isEmpty() ? emptyRequest : ServerRequest.of(params, prettyPrint);
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T> ParamConverter<T> getConverter(final Class<T> rawType, final Type genericType,
+            final Annotation[] annotations) {
+        return ApiPath.class.equals(rawType) ? (ParamConverter<T>) API_PATH_CONVERTER : null;
     }
 
     /**
@@ -87,9 +162,9 @@ public final class JaxRsRestconf {
     @DELETE
     @Path("/data/{identifier:.+}")
     @SuppressWarnings("checkstyle:abbreviationAsWordInName")
-    public void dataDELETE(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
+    public void dataDELETE(@Encoded @PathParam("identifier") final ApiPath identifier,
             @Suspended final AsyncResponse ar) {
-        server.dataDELETE(identifier.apiPath).addCallback(new JaxRsRestconfCallback<>(ar) {
+        server.dataDELETE(emptyRequest, identifier).addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
             Response transform(final Empty result) {
                 return Response.noContent().build();
@@ -113,8 +188,8 @@ public final class JaxRsRestconf {
         MediaType.TEXT_XML
     })
     public void dataGET(@Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        final var readParams = QueryParams.newReadDataParams(uriInfo);
-        completeDataGET(server.dataGET(readParams), readParams, ar);
+        final var request = requestOf(uriInfo);
+        completeDataGET(server.dataGET(request), request.prettyPrint(), ar);
     }
 
     /**
@@ -133,34 +208,38 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void dataGET(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        final var readParams = QueryParams.newReadDataParams(uriInfo);
-        completeDataGET(server.dataGET(identifier.apiPath, readParams), readParams, ar);
+    public void dataGET(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
+            @Suspended final AsyncResponse ar) {
+        final var request = requestOf(uriInfo);
+        completeDataGET(server.dataGET(request, identifier), request.prettyPrint(), ar);
     }
 
-    private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
-            final ReadDataParams readParams, final AsyncResponse ar) {
+    @NonNullByDefault
+    private static void completeDataGET(final RestconfFuture<DataGetResult> future, final PrettyPrintParam prettyPrint,
+            final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            Response transform(final NormalizedNodePayload result) {
-                return switch (readParams.content()) {
-                    case ALL, CONFIG -> {
-                        final var type = result.data().name().getNodeType();
-                        yield Response.status(Status.OK)
-                            .entity(result)
-                            // FIXME: is this ETag okay?
-                            .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null)
-                                + "-" + type.getLocalName() + '"')
-                            .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
-                            .build();
-                    }
-                    case NONCONFIG -> Response.status(Status.OK).entity(result).build();
-                };
+            Response transform(final DataGetResult result) {
+                final var builder = Response.ok()
+                    .entity(new JaxRsFormattableBody(result.body(), prettyPrint))
+                    .cacheControl(NO_CACHE);
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
         });
     }
 
+    private static void fillConfigurationMetadata(final ResponseBuilder builder, final ConfigurationMetadata metadata) {
+        final var etag = metadata.entityTag();
+        if (etag != null) {
+            builder.tag(new EntityTag(etag.value(), etag.weak()));
+        }
+        final var lastModified = metadata.lastModified();
+        if (lastModified != null) {
+            builder.lastModified(Date.from(lastModified));
+        }
+    }
+
     /**
      * Partially modify the target data store, as defined in
      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
@@ -177,7 +256,7 @@ public final class JaxRsRestconf {
     })
     public void dataXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            completeDataPATCH(server.dataPATCH(xmlBody), ar);
+            completeDataPATCH(server.dataPATCH(emptyRequest, xmlBody), ar);
         }
     }
 
@@ -196,10 +275,10 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void dataXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
+    public void dataXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
             @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            completeDataPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
+            completeDataPATCH(server.dataPATCH(emptyRequest, identifier, xmlBody), ar);
         }
     }
 
@@ -218,7 +297,7 @@ public final class JaxRsRestconf {
     })
     public void dataJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            completeDataPATCH(server.dataPATCH(jsonBody), ar);
+            completeDataPATCH(server.dataPATCH(emptyRequest, jsonBody), ar);
         }
     }
 
@@ -236,18 +315,20 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaType.APPLICATION_JSON,
     })
-    public void dataJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
+    public void dataJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
             @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            completeDataPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
+            completeDataPATCH(server.dataPATCH(emptyRequest, identifier, jsonBody), ar);
         }
     }
 
-    private static void completeDataPATCH(final RestconfFuture<Empty> future, final AsyncResponse ar) {
+    private static void completeDataPATCH(final RestconfFuture<DataPatchResult> future, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            Response transform(final Empty result) {
-                return Response.ok().build();
+            Response transform(final DataPatchResult result) {
+                final var builder = Response.ok();
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
         });
     }
@@ -257,6 +338,7 @@ public final class JaxRsRestconf {
      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
      *
      * @param body YANG Patch body
+     * @param uriInfo URI info
      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
      */
     @PATCH
@@ -266,9 +348,10 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaTypes.APPLICATION_YANG_DATA_XML
     })
-    public void dataYangJsonPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataYangJsonPATCH(final InputStream body, @Context final UriInfo uriInfo,
+            @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonPatchBody(body)) {
-            completeDataYangPATCH(server.dataPATCH(jsonBody), ar);
+            completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), jsonBody), ar);
         }
     }
 
@@ -278,6 +361,7 @@ public final class JaxRsRestconf {
      *
      * @param identifier path to target
      * @param body YANG Patch body
+     * @param uriInfo URI info
      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
      */
     @PATCH
@@ -287,10 +371,10 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaTypes.APPLICATION_YANG_DATA_XML
     })
-    public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataYangJsonPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonPatchBody(body)) {
-            completeDataYangPATCH(server.dataPATCH(identifier.apiPath, jsonBody), ar);
+            completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), identifier, jsonBody), ar);
         }
     }
 
@@ -299,6 +383,7 @@ public final class JaxRsRestconf {
      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
      *
      * @param body YANG Patch body
+     * @param uriInfo URI info
      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
      */
     @PATCH
@@ -308,9 +393,10 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaTypes.APPLICATION_YANG_DATA_XML
     })
-    public void dataYangXmlPATCH(final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataYangXmlPATCH(final InputStream body, @Context final UriInfo uriInfo,
+            @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlPatchBody(body)) {
-            completeDataYangPATCH(server.dataPATCH(xmlBody), ar);
+            completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), xmlBody), ar);
         }
     }
 
@@ -319,6 +405,7 @@ public final class JaxRsRestconf {
      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
      *
      * @param identifier path to target
+     * @param uriInfo URI info
      * @param body YANG Patch body
      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
      */
@@ -329,23 +416,29 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaTypes.APPLICATION_YANG_DATA_XML
     })
-    public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataYangXmlPATCH(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlPatchBody(body)) {
-            completeDataYangPATCH(server.dataPATCH(identifier.apiPath, xmlBody), ar);
+            completeDataYangPATCH(server.dataPATCH(requestOf(uriInfo), identifier, xmlBody), ar);
         }
     }
 
-    private static void completeDataYangPATCH(final RestconfFuture<PatchStatusContext> future, final AsyncResponse ar) {
+    private void completeDataYangPATCH(final RestconfFuture<DataYangPatchResult> future,
+            final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            Response transform(final PatchStatusContext result) {
-                return Response.status(statusOf(result)).entity(result).build();
+            Response transform(final DataYangPatchResult result) {
+                final var patchStatus = result.status();
+                final var statusCode = statusOf(patchStatus);
+                final var builder = Response.status(statusCode.code(), statusCode.phrase())
+                    .entity(new YangPatchStatusBody(patchStatus));
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
 
-            private static Status statusOf(final PatchStatusContext result) {
+            private HttpStatusCode statusOf(final PatchStatusContext result) {
                 if (result.ok()) {
-                    return Status.OK;
+                    return HttpStatusCode.OK;
                 }
                 final var globalErrors = result.globalErrors();
                 if (globalErrors != null && !globalErrors.isEmpty()) {
@@ -359,11 +452,11 @@ public final class JaxRsRestconf {
                         }
                     }
                 }
-                return Status.INTERNAL_SERVER_ERROR;
+                return HttpStatusCode.INTERNAL_SERVER_ERROR;
             }
 
-            private static Status statusOfFirst(final List<RestconfError> error) {
-                return ErrorTags.statusOf(error.get(0).getErrorTag());
+            private @NonNull HttpStatusCode statusOfFirst(final List<RestconfError> error) {
+                return errorTagMapping.statusOf(error.get(0).getErrorTag());
             }
         });
     }
@@ -384,7 +477,8 @@ public final class JaxRsRestconf {
     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
             @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonChildBody(body)) {
-            completeDataPOST(server.dataPOST(jsonBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
+            final var request = requestOf(uriInfo);
+            completeDataPOST(server.dataPOST(request, jsonBody), request.prettyPrint(), uriInfo, ar);
         }
     }
 
@@ -402,10 +496,11 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaType.APPLICATION_JSON,
     })
-    public void postDataJSON(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
+    public void postDataJSON(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        completeDataPOST(server.dataPOST(identifier.apiPath, new JsonDataPostBody(body),
-            QueryParams.normalize(uriInfo)), uriInfo, ar);
+        final var request = requestOf(uriInfo);
+        completeDataPOST(server.dataPOST(request, identifier, new JsonDataPostBody(body)), request.prettyPrint(),
+            uriInfo, ar);
     }
 
     /**
@@ -424,7 +519,8 @@ public final class JaxRsRestconf {
     })
     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlChildBody(body)) {
-            completeDataPOST(server.dataPOST(xmlBody, QueryParams.normalize(uriInfo)), uriInfo, ar);
+            final var request = requestOf(uriInfo);
+            completeDataPOST(server.dataPOST(request, xmlBody), request.prettyPrint(), uriInfo, ar);
         }
     }
 
@@ -443,28 +539,30 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void postDataXML(@Encoded @PathParam("identifier") final JaxRsApiPath identifier, final InputStream body,
+    public void postDataXML(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
-        completeDataPOST(server.dataPOST(identifier.apiPath, new XmlDataPostBody(body), QueryParams.normalize(uriInfo)),
+        final var request = requestOf(uriInfo);
+        completeDataPOST(server.dataPOST(request, identifier, new XmlDataPostBody(body)), request.prettyPrint(),
             uriInfo, ar);
     }
 
-    private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future, final UriInfo uriInfo,
-            final AsyncResponse ar) {
+    private static void completeDataPOST(final RestconfFuture<? extends DataPostResult> future,
+            final PrettyPrintParam prettyPrint, final UriInfo uriInfo, final AsyncResponse ar) {
         future.addCallback(new JaxRsRestconfCallback<DataPostResult>(ar) {
             @Override
             Response transform(final DataPostResult result) {
-                if (result instanceof CreateResource createResource) {
-                    return Response.created(uriInfo.getBaseUriBuilder()
-                            .path("data")
-                            .path(createResource.createdPath())
-                            .build())
-                        .build();
+                if (result instanceof CreateResourceResult createResource) {
+                    final var builder = Response.created(uriInfo.getBaseUriBuilder()
+                        .path("data")
+                        .path(createResource.createdPath().toString())
+                        .build());
+                    fillConfigurationMetadata(builder, createResource);
+                    return builder.build();
                 }
-                if (result instanceof InvokeOperation invokeOperation) {
+                if (result instanceof InvokeResult invokeOperation) {
                     final var output = invokeOperation.output();
-                    return output == null ? Response.status(Status.NO_CONTENT).build()
-                        : Response.status(Status.OK).entity(output).build();
+                    return output == null ? Response.noContent().build()
+                        : Response.ok().entity(new JaxRsFormattableBody(output, prettyPrint)).build();
                 }
                 LOG.error("Unhandled result {}", result);
                 return Response.serverError().build();
@@ -487,7 +585,7 @@ public final class JaxRsRestconf {
     })
     public void dataJsonPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            completeDataPUT(server.dataPUT(jsonBody, QueryParams.normalize(uriInfo)), ar);
+            completeDataPUT(server.dataPUT(requestOf(uriInfo), jsonBody), ar);
         }
     }
 
@@ -505,10 +603,10 @@ public final class JaxRsRestconf {
         MediaTypes.APPLICATION_YANG_DATA_JSON,
         MediaType.APPLICATION_JSON,
     })
-    public void dataJsonPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataJsonPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
+            final InputStream body, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonResourceBody(body)) {
-            completeDataPUT(server.dataPUT(identifier.apiPath, jsonBody, QueryParams.normalize(uriInfo)), ar);
+            completeDataPUT(server.dataPUT(requestOf(uriInfo), identifier, jsonBody), ar);
         }
     }
 
@@ -528,7 +626,7 @@ public final class JaxRsRestconf {
     })
     public void dataXmlPUT(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            completeDataPUT(server.dataPUT(xmlBody, QueryParams.normalize(uriInfo)), ar);
+            completeDataPUT(server.dataPUT(requestOf(uriInfo), xmlBody), ar);
         }
     }
 
@@ -547,10 +645,10 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void dataXmlPUT(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
+    public void dataXmlPUT(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
+            final InputStream body, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlResourceBody(body)) {
-            completeDataPUT(server.dataPUT(identifier.apiPath, xmlBody, QueryParams.normalize(uriInfo)), ar);
+            completeDataPUT(server.dataPUT(requestOf(uriInfo), identifier, xmlBody), ar);
         }
     }
 
@@ -558,83 +656,44 @@ public final class JaxRsRestconf {
         future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
             Response transform(final DataPutResult result) {
-                return switch (result) {
-                    // Note: no Location header, as it matches the request path
-                    case CREATED -> Response.status(Status.CREATED).build();
-                    case REPLACED -> Response.noContent().build();
-                };
+                // Note: no Location header, as it matches the request path
+                final var builder = result.created() ? Response.created(null) : Response.noContent();
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
         });
     }
 
     /**
-     * List RPC and action operations in RFC7951 format.
+     * List RPC and action operations.
      *
      * @param ar {@link AsyncResponse} which needs to be completed
      */
     @GET
     @Path("/operations")
-    @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
-    public void operationsJsonGET(@Suspended final AsyncResponse ar) {
-        completeOperationsJsonGet(server.operationsGET(), ar);
-    }
-
-    /**
-     * Retrieve list of operations and actions supported by the server or device in JSON format.
-     *
-     * @param operation path parameter to identify device and/or operation
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @GET
-    @Path("/operations/{operation:.+}")
-    @Produces({ MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON })
-    public void operationsJsonGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
-        completeOperationsGet(server.operationsGET(operation.apiPath), ar, OperationsGetResult::toJSON);
-    }
-
-    private static void completeOperationsJsonGet(final RestconfFuture<OperationsGetResult> future,
-            final AsyncResponse ar) {
-        completeOperationsGet(future, ar, OperationsGetResult::toJSON);
-    }
-
-    /**
-     * List RPC and action operations in RFC8040 XML format.
-     *
-     * @param ar {@link AsyncResponse} which needs to be completed
-     */
-    @GET
-    @Path("/operations")
-    @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
-    public void operationsXmlGET(@Suspended final AsyncResponse ar) {
-        completeOperationsXmlGet(server.operationsGET(), ar);
+    @Produces({
+        MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
+        MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
+    })
+    public void operationsGET(@Suspended final AsyncResponse ar) {
+        server.operationsGET(emptyRequest).addCallback(new FormattableBodyCallback(ar, prettyPrint));
     }
 
     /**
-     * Retrieve list of operations and actions supported by the server or device in XML format.
+     * Retrieve list of operations and actions supported by the server or device.
      *
      * @param operation path parameter to identify device and/or operation
      * @param ar {@link AsyncResponse} which needs to be completed
      */
     @GET
     @Path("/operations/{operation:.+}")
-    @Produces({ MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML })
-    public void operationsXmlGET(@PathParam("operation") final JaxRsApiPath operation, final AsyncResponse ar) {
-        completeOperationsXmlGet(server.operationsGET(operation.apiPath), ar);
-    }
-
-    private static void completeOperationsXmlGet(final RestconfFuture<OperationsGetResult> future,
-            final AsyncResponse ar) {
-        completeOperationsGet(future, ar, OperationsGetResult::toXML);
-    }
-
-    private static void completeOperationsGet(final RestconfFuture<OperationsGetResult> future, final AsyncResponse ar,
-            final Function<OperationsGetResult, String> toString) {
-        future.addCallback(new JaxRsRestconfCallback<OperationsGetResult>(ar) {
-            @Override
-            Response transform(final OperationsGetResult result) {
-                return Response.ok().entity(toString.apply(result)).build();
-            }
-        });
+    @Produces({
+        MediaTypes.APPLICATION_YANG_DATA_XML, MediaType.APPLICATION_XML, MediaType.TEXT_XML,
+        MediaTypes.APPLICATION_YANG_DATA_JSON, MediaType.APPLICATION_JSON
+    })
+    public void operationsGET(@PathParam("operation") final ApiPath operation, @Suspended final AsyncResponse ar) {
+        server.operationsGET(emptyRequest, operation)
+            .addCallback(new FormattableBodyCallback(ar, prettyPrint));
     }
 
     /**
@@ -643,7 +702,7 @@ public final class JaxRsRestconf {
      * @param identifier module name and rpc identifier string for the desired operation
      * @param body the body of the operation
      * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
+     * @param ar {@link AsyncResponse} which needs to be completed with a {@link FormattableBody} output
      */
     @POST
     // FIXME: identifier is just a *single* QName
@@ -660,10 +719,10 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void operationsXmlPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+    public void operationsXmlPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         try (var xmlBody = new XmlOperationInputBody(body)) {
-            operationsPOST(identifier.apiPath, uriInfo, ar, xmlBody);
+            operationsPOST(identifier, uriInfo, ar, xmlBody);
         }
     }
 
@@ -673,7 +732,7 @@ public final class JaxRsRestconf {
      * @param identifier module name and rpc identifier string for the desired operation
      * @param body the body of the operation
      * @param uriInfo URI info
-     * @param ar {@link AsyncResponse} which needs to be completed with a {@link NormalizedNodePayload} output
+     * @param ar {@link AsyncResponse} which needs to be completed with a {@link FormattableBody} output
      */
     @POST
     // FIXME: identifier is just a *single* QName
@@ -689,22 +748,22 @@ public final class JaxRsRestconf {
         MediaType.APPLICATION_XML,
         MediaType.TEXT_XML
     })
-    public void operationsJsonPOST(@Encoded @PathParam("identifier") final JaxRsApiPath identifier,
-            final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
+    public void operationsJsonPOST(@Encoded @PathParam("identifier") final ApiPath identifier, final InputStream body,
+            @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
         try (var jsonBody = new JsonOperationInputBody(body)) {
-            operationsPOST(identifier.apiPath, uriInfo, ar, jsonBody);
+            operationsPOST(identifier, uriInfo, ar, jsonBody);
         }
     }
 
     private void operationsPOST(final ApiPath identifier, final UriInfo uriInfo, final AsyncResponse ar,
             final OperationInputBody body) {
-        server.operationsPOST(uriInfo.getBaseUri(), identifier, body)
-            .addCallback(new JaxRsRestconfCallback<OperationOutput>(ar) {
+        server.operationsPOST(requestOf(uriInfo), uriInfo.getBaseUri(), identifier, body)
+            .addCallback(new JaxRsRestconfCallback<>(ar) {
                 @Override
-                Response transform(final OperationOutput result) {
+                Response transform(final InvokeResult result) {
                     final var body = result.output();
                     return body == null ? Response.noContent().build()
-                        : Response.ok().entity(new NormalizedNodePayload(result.operation(), body)).build();
+                        : Response.ok().entity(body).build();
                 }
             });
     }
@@ -724,10 +783,99 @@ public final class JaxRsRestconf {
         MediaType.TEXT_XML
     })
     public void yangLibraryVersionGET(@Suspended final AsyncResponse ar) {
-        server.yangLibraryVersionGET().addCallback(new JaxRsRestconfCallback<NormalizedNodePayload>(ar) {
+        server.yangLibraryVersionGET(emptyRequest).addCallback(new FormattableBodyCallback(ar, prettyPrint));
+    }
+
+    // FIXME: References to these resources are generated by our yang-library implementation. That means:
+    //        - We really need to formalize the parameter structure so we get some help from JAX-RS during matching
+    //          of three things:
+    //          - optional yang-ext:mount prefix(es)
+    //          - mandatory module name
+    //          - optional module revision
+    //        - We really should use /yang-library-module/{name}(/{revision})?
+    //        - We seem to be lacking explicit support for submodules in there -- and those locations should then point
+    //          to /yang-library-submodule/{moduleName}(/{moduleRevision})?/{name}(/{revision})? so as to look the
+    //          submodule up efficiently and allow for the weird case where there are two submodules with the same name
+    //          (that is currently not supported by the parser, but it will be in the future)
+    //        - It does not make sense to support yang-ext:mount, unless we also intercept mount points and rewrite
+    //          yang-library locations. We most likely want to do that to ensure users are not tempted to connect to
+    //          wild destinations
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param fileName source file name
+     * @param revision source revision
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
+    @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
+    public void modulesYangGET(@PathParam("fileName") final String fileName,
+            @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYangGET(emptyRequest, fileName, revision), ar);
+    }
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param mountPath mount point path
+     * @param fileName source file name
+     * @param revision source revision
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YANG_MEDIA_TYPE)
+    @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
+    public void modulesYangGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
+            @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
+            @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYangGET(emptyRequest, mountPath, fileName, revision), ar);
+    }
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param fileName source file name
+     * @param revision source revision
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
+    @Path("/" + URLConstants.MODULES_SUBPATH + "/{fileName : [^/]+}")
+    public void modulesYinGET(@PathParam("fileName") final String fileName,
+            @QueryParam("revision") final String revision, @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYinGET(emptyRequest, fileName, revision), ar);
+    }
+
+    /**
+     * Get schema of specific module.
+     *
+     * @param mountPath mount point path
+     * @param fileName source file name
+     * @param revision source revision
+     * @param ar {@link AsyncResponse} which needs to be completed
+     */
+    @GET
+    @Produces(YangConstants.RFC6020_YIN_MEDIA_TYPE)
+    @Path("/" + URLConstants.MODULES_SUBPATH + "/{mountPath:.+}/{fileName : [^/]+}")
+    public void modulesYinGET(@Encoded @PathParam("mountPath") final ApiPath mountPath,
+            @PathParam("fileName") final String fileName, @QueryParam("revision") final String revision,
+            @Suspended final AsyncResponse ar) {
+        completeModulesGET(server.modulesYinGET(emptyRequest, mountPath, fileName, revision), ar);
+    }
+
+    private static void completeModulesGET(final RestconfFuture<ModulesGetResult> future, final AsyncResponse ar) {
+        future.addCallback(new JaxRsRestconfCallback<>(ar) {
             @Override
-            Response transform(final NormalizedNodePayload result) {
-                return Response.ok().entity(result).build();
+            Response transform(final ModulesGetResult result) {
+                final Reader reader;
+                try {
+                    reader = result.source().openStream();
+                } catch (IOException e) {
+                    throw new RestconfDocumentedException("Cannot open source", e);
+                }
+                return Response.ok(reader).build();
             }
         });
     }