Promote FormattableBody to restconf.api
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / jaxrs / JaxRsRestconf.java
index 7b3ae2b4ebe59b7fecc206a3103e2551debfd402..3bbfd675d1073f9a935d6901f67a35ea92d8a04a 100644 (file)
@@ -15,11 +15,8 @@ import java.io.Reader;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
 import java.text.ParseException;
-import java.time.Clock;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
+import java.util.Date;
 import java.util.List;
-import java.util.function.Function;
 import javax.inject.Singleton;
 import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
@@ -34,9 +31,12 @@ 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.ResponseBuilder;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriInfo;
 import javax.ws.rs.ext.ParamConverter;
@@ -44,37 +44,38 @@ import javax.ws.rs.ext.ParamConverterProvider;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.FormattableBody;
 import org.opendaylight.restconf.api.MediaTypes;
 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.URLConstants;
-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.server.api.ConfigurationMetadata;
+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.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.OperationsGetResult;
+import org.opendaylight.restconf.server.api.OperationInputBody;
 import org.opendaylight.restconf.server.api.RestconfServer;
-import org.opendaylight.restconf.server.spi.OperationOutput;
+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.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;
@@ -88,7 +89,7 @@ import org.slf4j.LoggerFactory;
 @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) {
@@ -161,8 +162,7 @@ public final class JaxRsRestconf implements ParamConverterProvider {
         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);
+        completeDataGET(server.dataGET(QueryParams.newDataGetParams(uriInfo)), ar);
     }
 
     /**
@@ -183,34 +183,33 @@ public final class JaxRsRestconf implements ParamConverterProvider {
     })
     public void dataGET(@Encoded @PathParam("identifier") final ApiPath identifier, @Context final UriInfo uriInfo,
             @Suspended final AsyncResponse ar) {
-        final var readParams = QueryParams.newReadDataParams(uriInfo);
-        completeDataGET(server.dataGET(identifier, readParams), readParams, ar);
+        completeDataGET(server.dataGET(identifier, QueryParams.newDataGetParams(uriInfo)), ar);
     }
 
-    private static void completeDataGET(final RestconfFuture<NormalizedNodePayload> future,
-            final ReadDataParams readParams, final AsyncResponse ar) {
+    private static void completeDataGET(final RestconfFuture<DataGetResult> future, 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?
-                            // FIXME: use tag() method instead
-                            .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null)
-                                + "-" + type.getLocalName() + '"')
-                            // FIXME: use lastModified() method instead
-                            .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.status(Status.OK)
+                    .entity(result.payload())
+                    .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>.
@@ -293,11 +292,13 @@ public final class JaxRsRestconf implements ParamConverterProvider {
         }
     }
 
-    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();
             }
         });
     }
@@ -386,11 +387,15 @@ public final class JaxRsRestconf implements ParamConverterProvider {
         }
     }
 
-    private static void completeDataYangPATCH(final RestconfFuture<PatchStatusContext> future, final AsyncResponse ar) {
+    private static 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 status = result.status();
+                final var builder = Response.status(statusOf(status)).entity(status);
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
 
             private static Status statusOf(final PatchStatusContext result) {
@@ -505,16 +510,17 @@ public final class JaxRsRestconf implements ParamConverterProvider {
             @Override
             Response transform(final DataPostResult result) {
                 if (result instanceof CreateResource createResource) {
-                    return Response.created(uriInfo.getBaseUriBuilder()
-                            .path("data")
-                            .path(createResource.createdPath())
-                            .build())
-                        .build();
+                    final var builder = Response.created(uriInfo.getBaseUriBuilder()
+                        .path("data")
+                        .path(createResource.createdPath())
+                        .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();
+                        : Response.ok().entity(output).build();
                 }
                 LOG.error("Unhandled result {}", result);
                 return Response.serverError().build();
@@ -608,81 +614,50 @@ public final class JaxRsRestconf implements ParamConverterProvider {
         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.status(Status.CREATED) : Response.noContent();
+                fillConfigurationMetadata(builder, result);
+                return builder.build();
             }
         });
     }
 
     /**
-     * List RPC and action operations in RFC7951 format.
-     *
-     * @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 ApiPath operation, @Suspended final AsyncResponse ar) {
-        completeOperationsGet(server.operationsGET(operation), 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.
+     * List RPC and action operations.
      *
      * @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) {
+        completeOperationsGet(server.operationsGET(), ar);
     }
 
     /**
-     * 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 ApiPath operation, @Suspended final AsyncResponse ar) {
-        completeOperationsXmlGet(server.operationsGET(operation), ar);
-    }
-
-    private static void completeOperationsXmlGet(final RestconfFuture<OperationsGetResult> future,
-            final AsyncResponse ar) {
-        completeOperationsGet(future, ar, OperationsGetResult::toXML);
+    @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) {
+        completeOperationsGet(server.operationsGET(operation), ar);
     }
 
-    private static void completeOperationsGet(final RestconfFuture<OperationsGetResult> future, final AsyncResponse ar,
-            final Function<OperationsGetResult, String> toString) {
-        future.addCallback(new JaxRsRestconfCallback<OperationsGetResult>(ar) {
+    private static void completeOperationsGet(final RestconfFuture<FormattableBody> future, final AsyncResponse ar) {
+        future.addCallback(new JaxRsRestconfCallback<FormattableBody>(ar) {
             @Override
-            Response transform(final OperationsGetResult result) {
-                return Response.ok().entity(toString.apply(result)).build();
+            Response transform(final FormattableBody result) {
+                return Response.ok().entity(result).build();
             }
         });
     }
@@ -748,13 +723,13 @@ public final class JaxRsRestconf implements ParamConverterProvider {
 
     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(uriInfo.getBaseUri(), identifier, QueryParams.normalize(uriInfo), 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();
                 }
             });
     }