Shift ETag/Last-Modified generation to RestconfStrategy 55/109155/4
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 6 Dec 2023 07:40:27 +0000 (08:40 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 6 Dec 2023 11:09:29 +0000 (12:09 +0100)
We should not be faking headers, but rather let them be controlled by
RestconfStrategy.

This patch introduces ConfigurationMetadata, as the baseline interface
capturing the two headers and DataGetResult as the container allowing
them to be communicated from RestconfServer.dataGET() methods.

JIRA: NETCONF-1207
Change-Id: I2c2b11a60eea0c6c868f2e82f88c002e702fdbc5
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/jaxrs/JaxRsRestconf.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/MdsalRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/NetconfRestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ConfigurationMetadata.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java [new file with mode: 0644]
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/RestconfServer.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/MdsalRestconfServer.java

index 5d39dcfe733884af9fbf9c6a2a78f7e6ff74acb5..28eff8cb63d990a587b20989aae86ab5dbd350b4 100644 (file)
@@ -15,9 +15,7 @@ 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;
@@ -34,7 +32,9 @@ 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;
@@ -64,7 +64,7 @@ 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.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.api.DataPostResult.InvokeOperation;
@@ -74,7 +74,6 @@ import org.opendaylight.restconf.server.api.OperationsGetResult;
 import org.opendaylight.restconf.server.api.OperationsPostResult;
 import org.opendaylight.restconf.server.api.RestconfServer;
 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 +87,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 +160,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.newDataGetParams(uriInfo);
-        completeDataGET(server.dataGET(readParams), readParams, ar);
+        completeDataGET(server.dataGET(QueryParams.newDataGetParams(uriInfo)), ar);
     }
 
     /**
@@ -183,30 +181,25 @@ 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.newDataGetParams(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 DataGetParams 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);
+                final var etag = result.entityTag();
+                if (etag != null) {
+                    builder.tag(new EntityTag(etag.value(), etag.weak()));
+                }
+                final var lastModified = result.lastModified();
+                if (lastModified != null) {
+                    builder.lastModified(Date.from(lastModified));
+                }
+                return builder.build();
             }
         });
     }
index 1198ca47c3cd0b866dac6dd28c910076af60f7c9..286e91f032521db3177dad6affead75f6af4153e 100644 (file)
@@ -28,10 +28,10 @@ import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.WriterFieldsTranslator;
 import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.DataPath;
 import org.opendaylight.restconf.server.spi.RpcImplementation;
@@ -112,13 +112,13 @@ public final class MdsalRestconfStrategy extends RestconfStrategy {
     }
 
     @Override
-    RestconfFuture<NormalizedNodePayload> dataGET(final DataPath path, final DataGetParams params) {
+    RestconfFuture<DataGetResult> dataGET(final DataPath path, final DataGetParams params) {
         final var inference = path.inference();
         final var fields = params.fields();
         final var translatedFields = fields == null ? null
             : WriterFieldsTranslator.translate(inference.getEffectiveModelContext(), path.schema(), fields);
         return completeDataGET(inference, QueryParameters.of(params, translatedFields),
-            readData(params.content(), path.instance(), params.withDefaults()));
+            readData(params.content(), path.instance(), params.withDefaults()), null);
     }
 
     @Override
index d35ade73baf5cb01f49afcc86d82ac1298ecc581..0ac402ac7f7d5837a744bf532bdcae18339fb69d 100644 (file)
@@ -33,10 +33,10 @@ import org.opendaylight.restconf.api.query.WithDefaultsParam;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
-import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.NetconfFieldsTranslator;
 import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.restconf.server.spi.ApiPathNormalizer.DataPath;
 import org.opendaylight.yangtools.yang.common.Empty;
@@ -82,7 +82,7 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
     }
 
     @Override
-    RestconfFuture<NormalizedNodePayload> dataGET(final DataPath path, final DataGetParams params) {
+    RestconfFuture<DataGetResult> dataGET(final DataPath path, final DataGetParams params) {
         final var inference = path.inference();
         final var fields = params.fields();
         final List<YangInstanceIdentifier> fieldPaths;
@@ -104,7 +104,7 @@ public final class NetconfRestconfStrategy extends RestconfStrategy {
         } else {
             node = readData(params.content(), path.instance(), params.withDefaults());
         }
-        return completeDataGET(inference, QueryParameters.of(params), node);
+        return completeDataGET(inference, QueryParameters.of(params), node, null);
     }
 
     @Override
index a07377ee7ffefca1dfc5dae8840efdd4dfe9dbd5..6c2de8a46d7d9dc89fc4d2a0e7e62182aa256f0f 100644 (file)
@@ -73,7 +73,9 @@ import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
 import org.opendaylight.restconf.nb.rfc8040.utils.parser.YangInstanceIdentifierSerializer;
+import org.opendaylight.restconf.server.api.ConfigurationMetadata;
 import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DataPatchPath;
 import org.opendaylight.restconf.server.api.DataPostPath;
 import org.opendaylight.restconf.server.api.DataPostResult;
@@ -785,7 +787,7 @@ public abstract class RestconfStrategy {
 
     abstract void delete(@NonNull SettableRestconfFuture<Empty> future, @NonNull YangInstanceIdentifier path);
 
-    public final @NonNull RestconfFuture<NormalizedNodePayload> dataGET(final ApiPath apiPath,
+    public final @NonNull RestconfFuture<DataGetResult> dataGET(final ApiPath apiPath,
             final DataGetParams params) {
         final DataPath path;
         try {
@@ -796,17 +798,20 @@ public abstract class RestconfStrategy {
         return dataGET(path, params);
     }
 
-    abstract @NonNull RestconfFuture<NormalizedNodePayload> dataGET(DataPath path, DataGetParams params);
+    abstract @NonNull RestconfFuture<DataGetResult> dataGET(DataPath path, DataGetParams params);
 
-    static final @NonNull RestconfFuture<NormalizedNodePayload> completeDataGET(final Inference inference,
-            final QueryParameters queryParams, final NormalizedNode node) {
+    static final @NonNull RestconfFuture<DataGetResult> completeDataGET(final Inference inference,
+            final QueryParameters queryParams, final @Nullable NormalizedNode node,
+            final @Nullable ConfigurationMetadata metadata) {
         if (node == null) {
             return RestconfFuture.failed(new RestconfDocumentedException(
                 "Request could not be completed because the relevant data model content does not exist",
                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING));
         }
 
-        return RestconfFuture.of(new NormalizedNodePayload(inference, node, queryParams));
+        final var payload = new NormalizedNodePayload(inference, node, queryParams);
+        return RestconfFuture.of(metadata == null ? new DataGetResult(payload)
+            : new DataGetResult(payload, metadata.entityTag(), metadata.lastModified()));
     }
 
     /**
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ConfigurationMetadata.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/ConfigurationMetadata.java
new file mode 100644 (file)
index 0000000..897ab7a
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import java.time.Instant;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
+/**
+ * Metadata maintained by a {@link RestconfServer} for configuration resources.
+ */
+public interface ConfigurationMetadata {
+    /**
+     * The value of {@code ETag} HTTP header, as specified by
+     * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.2">RFC8040 Entity-Tag</a>.
+     *
+     * @param value the value, must not be {@link String#isBlank() blank}
+     * @param weak {@code true} if this tag is weak, {@code false} if this tag is strong
+     */
+    @NonNullByDefault
+    record EntityTag(String value, boolean weak) {
+        public EntityTag {
+            if (value.isBlank()) {
+                throw new IllegalArgumentException("Value must not be blank");
+            }
+        }
+    }
+
+    /**
+     * The {@code ETag} HTTP header, if supported by the server.
+     *
+     * @return An {@link EntityTag} or {@code null} if not supported.
+     */
+    @Nullable EntityTag entityTag();
+
+    /**
+     * The {@code Last-Modified} HTTP header, if maintained by the server.
+     *
+     * @return An {@link Instant} or {@code null} if not maintained.
+     */
+    @Nullable Instant lastModified();
+}
diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/api/DataGetResult.java
new file mode 100644 (file)
index 0000000..3a8ed7b
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.server.api;
+
+import static java.util.Objects.requireNonNull;
+
+import java.time.Instant;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
+
+/**
+ * Result of a {@code GET} request as defined in
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.3">RFC8040 section 4.3</a>.
+ *
+ * @param payload Resulting payload
+ * @param entityTag response {@code ETag} header, or {@code null} if not applicable
+ * @param lastModified response {@code Last-Modified} header, or {@code null} if not applicable
+ */
+public record DataGetResult(
+        @NonNull NormalizedNodePayload payload,
+        @Nullable EntityTag entityTag,
+        @Nullable Instant lastModified) implements ConfigurationMetadata {
+    public DataGetResult {
+        requireNonNull(payload);
+    }
+
+    public DataGetResult(final @NonNull NormalizedNodePayload payload) {
+        this(payload, null, null);
+    }
+}
index 4f3c202a6148de85e52507ae48e18344174b212a..3751f21859cf3e17dccc67a02ac065c3e216329d 100644 (file)
@@ -27,9 +27,8 @@ import org.opendaylight.yangtools.yang.common.Empty;
  * An implementation of a RESTCONF server, implementing the
  * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.3">RESTCONF API Resource</a>.
  */
-// FIXME: configuration datastore should maintain ETag and Last-Modified headers, so that these can be returned when
-//        GET/PATCH/POST/PUT modify the data.
-// FIXME: NETCONF-773: as a first step in doing that we should carry those fields in our responses
+// FIXME: NETCONF-1207: configuration datastore should maintain ETag and Last-Modified headers, so that these can be
+//                      returned when PATCH/POST/PUT modify the data.
 @NonNullByDefault
 public interface RestconfServer {
     /**
@@ -45,18 +44,18 @@ public interface RestconfServer {
      * Return the content of the datastore.
      *
      * @param params {@link DataGetParams} for this request
-     * @return A {@link RestconfFuture} of the {@link NormalizedNodePayload} content
+     * @return A {@link RestconfFuture} of the {@link DataGetResult} content
      */
-    RestconfFuture<NormalizedNodePayload> dataGET(DataGetParams params);
+    RestconfFuture<DataGetResult> dataGET(DataGetParams params);
 
     /**
      * Return the content of a data resource.
      *
      * @param identifier resource identifier
      * @param params {@link DataGetParams} for this request
-     * @return A {@link RestconfFuture} of the {@link NormalizedNodePayload} content
+     * @return A {@link RestconfFuture} of the {@link DataGetResult} content
      */
-    RestconfFuture<NormalizedNodePayload> dataGET(ApiPath identifier, DataGetParams params);
+    RestconfFuture<DataGetResult> dataGET(ApiPath identifier, DataGetParams params);
 
     /**
      * Partially modify the target data resource, as defined in
@@ -149,7 +148,7 @@ public interface RestconfServer {
      * @param body RPC operation
      * @return A {@link RestconfFuture} completing with {@link OperationsPostResult}
      */
-    // FIXME: 'operation' should really be an ApiIdentifier with non-null module, but we also support ang-ext:mount,
+    // FIXME: 'operation' should really be an ApiIdentifier with non-null module, but we also support yang-ext:mount,
     //        and hence it is a path right now
     RestconfFuture<OperationsPostResult> operationsPOST(URI restconfURI, ApiPath operation, OperationInputBody body);
 
index 28613cdaa5da501e8228b23ff10a0cb5df6f0eed..1dc8fadc8a7f01915c2838ee9feff132f001c9d7 100644 (file)
@@ -44,6 +44,7 @@ import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStra
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy.StrategyAndTail;
 import org.opendaylight.restconf.server.api.DataGetParams;
+import org.opendaylight.restconf.server.api.DataGetResult;
 import org.opendaylight.restconf.server.api.DataPostResult;
 import org.opendaylight.restconf.server.api.DataPostResult.CreateResource;
 import org.opendaylight.restconf.server.api.DataPutResult;
@@ -176,12 +177,12 @@ public final class MdsalRestconfServer
     }
 
     @Override
-    public RestconfFuture<NormalizedNodePayload> dataGET(final DataGetParams params) {
+    public RestconfFuture<DataGetResult> dataGET(final DataGetParams params) {
         return localStrategy().dataGET(ApiPath.empty(), params);
     }
 
     @Override
-    public RestconfFuture<NormalizedNodePayload> dataGET(final ApiPath identifier, final DataGetParams params) {
+    public RestconfFuture<DataGetResult> dataGET(final ApiPath identifier, final DataGetParams params) {
         final StrategyAndTail stratAndTail;
         try {
             stratAndTail = localStrategy().resolveStrategy(identifier);