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.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 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;
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;
@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) {
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);
}
/**
})
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();
}
});
}
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;
}
@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
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;
}
@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;
} 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
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;
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 {
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()));
}
/**
--- /dev/null
+/*
+ * 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();
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
* 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 {
/**
* 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
* @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);
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;
}
@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);