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;
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;
}
/**
@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
- protected Response transform(final Empty result) {
+ Response transform(final Empty result) {
return Response.noContent().build();
}
});
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);
}
/**
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
- protected 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>.
})
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);
}
}
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);
}
}
})
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);
}
}
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
- protected 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();
}
});
}
* <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
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);
}
}
*
* @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
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);
}
}
* <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
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);
}
}
* <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}
*/
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
- protected 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()) {
}
}
}
- 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());
}
});
}
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);
}
}
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);
}
/**
})
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);
}
}
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
- protected Response transform(final DataPostResult result) {
- if (result instanceof CreateResource createResource) {
- return Response.created(uriInfo.getBaseUriBuilder()
- .path("data")
- .path(createResource.createdPath())
- .build())
- .build();
+ Response transform(final DataPostResult result) {
+ 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();
})
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);
}
}
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);
}
}
})
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);
}
}
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);
}
}
private static void completeDataPUT(final RestconfFuture<DataPutResult> future, final AsyncResponse ar) {
future.addCallback(new JaxRsRestconfCallback<>(ar) {
@Override
- protected 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();
- };
+ Response transform(final DataPutResult result) {
+ // 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.
- *
- * @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,
- @Suspended 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.
+ * 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) {
+ 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,
- @Suspended 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
- protected 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));
}
/**
* @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
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);
}
}
* @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
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
- protected 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();
}
});
}
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
- protected 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();
}
});
}