2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
4 * This program and the accompanying materials are made available under the
5 * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6 * and is available at http://www.eclipse.org/legal/epl-v10.html
8 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
10 import static java.util.Objects.requireNonNull;
12 import com.google.common.annotations.VisibleForTesting;
13 import java.io.IOException;
14 import java.io.InputStream;
16 import java.util.List;
17 import javax.ws.rs.Consumes;
18 import javax.ws.rs.DELETE;
19 import javax.ws.rs.Encoded;
20 import javax.ws.rs.PATCH;
21 import javax.ws.rs.POST;
22 import javax.ws.rs.PUT;
23 import javax.ws.rs.Path;
24 import javax.ws.rs.PathParam;
25 import javax.ws.rs.Produces;
26 import javax.ws.rs.container.AsyncResponse;
27 import javax.ws.rs.container.Suspended;
28 import javax.ws.rs.core.Context;
29 import javax.ws.rs.core.MediaType;
30 import javax.ws.rs.core.Response;
31 import javax.ws.rs.core.Response.Status;
32 import javax.ws.rs.core.UriInfo;
33 import org.eclipse.jdt.annotation.NonNull;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.opendaylight.mdsal.dom.api.DOMActionResult;
36 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
37 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
38 import org.opendaylight.restconf.common.patch.PatchContext;
39 import org.opendaylight.restconf.common.patch.PatchStatusContext;
40 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
41 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
42 import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
43 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
44 import org.opendaylight.restconf.nb.rfc8040.databind.JsonChildBody;
45 import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
46 import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
47 import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
48 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
49 import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
50 import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
51 import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
52 import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
53 import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
54 import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
55 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
56 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
57 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
58 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
59 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy.CreateOrReplaceResult;
60 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
61 import org.opendaylight.yangtools.yang.common.Empty;
62 import org.opendaylight.yangtools.yang.common.ErrorTag;
63 import org.opendaylight.yangtools.yang.common.ErrorType;
64 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
65 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
66 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
67 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
68 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
69 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
70 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
71 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
76 * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
77 * and state data nodes.
80 public final class RestconfDataServiceImpl {
81 private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
83 private final DatabindProvider databindProvider;
84 private final MdsalRestconfServer server;
86 public RestconfDataServiceImpl(final DatabindProvider databindProvider, final MdsalRestconfServer server) {
87 this.databindProvider = requireNonNull(databindProvider);
88 this.server = requireNonNull(server);
92 * Replace the data store.
94 * @param uriInfo request URI information
95 * @param body data node for put to config DS
96 * @param ar {@link AsyncResponse} which needs to be completed
101 MediaTypes.APPLICATION_YANG_DATA_JSON,
102 MediaType.APPLICATION_JSON,
104 public void putDataJSON(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
105 try (var jsonBody = new JsonResourceBody(body)) {
106 putData(null, uriInfo, jsonBody, ar);
111 * Create or replace the target data resource.
113 * @param identifier path to target
114 * @param uriInfo request URI information
115 * @param body data node for put to config DS
116 * @param ar {@link AsyncResponse} which needs to be completed
119 @Path("/data/{identifier:.+}")
121 MediaTypes.APPLICATION_YANG_DATA_JSON,
122 MediaType.APPLICATION_JSON,
124 public void putDataJSON(@Encoded @PathParam("identifier") final String identifier,
125 @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
126 try (var jsonBody = new JsonResourceBody(body)) {
127 putData(identifier, uriInfo, jsonBody, ar);
132 * Replace the data store.
134 * @param uriInfo request URI information
135 * @param body data node for put to config DS
136 * @param ar {@link AsyncResponse} which needs to be completed
141 MediaTypes.APPLICATION_YANG_DATA_XML,
142 MediaType.APPLICATION_XML,
145 public void putDataXML(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
146 try (var xmlBody = new XmlResourceBody(body)) {
147 putData(null, uriInfo, xmlBody, ar);
152 * Create or replace the target data resource.
154 * @param identifier path to target
155 * @param uriInfo request URI information
156 * @param body data node for put to config DS
157 * @param ar {@link AsyncResponse} which needs to be completed
160 @Path("/data/{identifier:.+}")
162 MediaTypes.APPLICATION_YANG_DATA_XML,
163 MediaType.APPLICATION_XML,
166 public void putDataXML(@Encoded @PathParam("identifier") final String identifier,
167 @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
168 try (var xmlBody = new XmlResourceBody(body)) {
169 putData(identifier, uriInfo, xmlBody, ar);
173 private void putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body,
174 final AsyncResponse ar) {
175 final var reqPath = server.bindRequestPath(identifier);
176 final var insert = QueryParams.parseInsert(reqPath.getSchemaContext(), uriInfo);
177 final var req = server.bindResourceRequest(reqPath, body);
179 req.strategy().putData(req.path(), req.data(), insert).addCallback(new JaxRsRestconfCallback<>(ar) {
181 Response transform(final CreateOrReplaceResult result) {
182 return switch (result) {
183 // Note: no Location header, as it matches the request path
184 case CREATED -> Response.status(Status.CREATED).build();
185 case REPLACED -> Response.noContent().build();
192 * Create a top-level data resource.
194 * @param body data node for put to config DS
195 * @param uriInfo URI info
196 * @param ar {@link AsyncResponse} which needs to be completed
201 MediaTypes.APPLICATION_YANG_DATA_JSON,
202 MediaType.APPLICATION_JSON,
204 public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
205 @Suspended final AsyncResponse ar) {
206 try (var jsonBody = new JsonChildBody(body)) {
207 postData(jsonBody, uriInfo, ar);
212 * Create a data resource in target.
214 * @param identifier path to target
215 * @param body data node for put to config DS
216 * @param uriInfo URI info
217 * @param ar {@link AsyncResponse} which needs to be completed
220 @Path("/data/{identifier:.+}")
222 MediaTypes.APPLICATION_YANG_DATA_JSON,
223 MediaType.APPLICATION_JSON,
225 public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
226 @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
227 final var reqPath = server.bindRequestPath(identifier);
228 if (reqPath.getSchemaNode() instanceof ActionDefinition) {
229 try (var jsonBody = new JsonOperationInputBody(body)) {
230 invokeAction(reqPath, jsonBody, ar);
233 try (var jsonBody = new JsonChildBody(body)) {
234 postData(reqPath.inference(), reqPath.getInstanceIdentifier(), jsonBody, uriInfo,
235 reqPath.getMountPoint(), ar);
241 * Create a top-level data resource.
243 * @param body data node for put to config DS
244 * @param uriInfo URI info
245 * @param ar {@link AsyncResponse} which needs to be completed
250 MediaTypes.APPLICATION_YANG_DATA_XML,
251 MediaType.APPLICATION_XML,
254 public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
255 try (var xmlBody = new XmlChildBody(body)) {
256 postData(xmlBody, uriInfo, ar);
261 * Create a data resource in target.
263 * @param identifier path to target
264 * @param body data node for put to config DS
265 * @param uriInfo URI info
266 * @param ar {@link AsyncResponse} which needs to be completed
269 @Path("/data/{identifier:.+}")
271 MediaTypes.APPLICATION_YANG_DATA_XML,
272 MediaType.APPLICATION_XML,
275 public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
276 @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
277 final var reqPath = server.bindRequestPath(identifier);
278 if (reqPath.getSchemaNode() instanceof ActionDefinition) {
279 try (var xmlBody = new XmlOperationInputBody(body)) {
280 invokeAction(reqPath, xmlBody, ar);
283 try (var xmlBody = new XmlChildBody(body)) {
284 postData(reqPath.inference(), reqPath.getInstanceIdentifier(), xmlBody, uriInfo,
285 reqPath.getMountPoint(), ar);
290 private void postData(final ChildBody body, final UriInfo uriInfo, final AsyncResponse ar) {
291 postData(Inference.ofDataTreePath(databindProvider.currentContext().modelContext()),
292 YangInstanceIdentifier.of(), body, uriInfo, null, ar);
295 private void postData(final Inference inference, final YangInstanceIdentifier parentPath, final ChildBody body,
296 final UriInfo uriInfo, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
297 final var modelContext = inference.getEffectiveModelContext();
298 final var insert = QueryParams.parseInsert(modelContext, uriInfo);
299 final var strategy = server.getRestconfStrategy(modelContext, mountPoint);
300 final var payload = body.toPayload(parentPath, inference);
301 final var data = payload.body();
302 final var path = concat(parentPath, payload.prefix());
304 strategy.postData(path, data, insert).addCallback(new JaxRsRestconfCallback<>(ar) {
306 Response transform(final Empty result) {
307 return Response.created(resolveLocation(uriInfo, path, modelContext, data)).build();
312 private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
314 for (var arg : args) {
321 * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
323 * @param uriInfo uri info
324 * @param initialPath data path
325 * @param schemaContext reference to {@link SchemaContext}
326 * @return {@link URI}
328 private static URI resolveLocation(final UriInfo uriInfo, final YangInstanceIdentifier initialPath,
329 final EffectiveModelContext schemaContext, final NormalizedNode data) {
330 YangInstanceIdentifier path = initialPath;
331 if (data instanceof MapNode mapData) {
332 final var children = mapData.body();
333 if (!children.isEmpty()) {
334 path = path.node(children.iterator().next().name());
338 return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
342 * Delete the target data resource.
344 * @param identifier path to target
345 * @param ar {@link AsyncResponse} which needs to be completed
348 @Path("/data/{identifier:.+}")
349 public void deleteData(@Encoded @PathParam("identifier") final String identifier,
350 @Suspended final AsyncResponse ar) {
351 final var reqPath = server.bindRequestPath(identifier);
352 final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
354 strategy.delete(reqPath.getInstanceIdentifier()).addCallback(new JaxRsRestconfCallback<>(ar) {
356 Response transform(final Empty result) {
357 return Response.noContent().build();
363 * Partially modify the target data store, as defined in
364 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
366 * @param body data node for put to config DS
367 * @param ar {@link AsyncResponse} which needs to be completed
372 MediaTypes.APPLICATION_YANG_DATA_XML,
373 MediaType.APPLICATION_XML,
376 public void plainPatchDataXML(final InputStream body, @Suspended final AsyncResponse ar) {
377 try (var xmlBody = new XmlResourceBody(body)) {
378 plainPatchData(xmlBody, ar);
383 * Partially modify the target data resource, as defined in
384 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
386 * @param identifier path to target
387 * @param body data node for put to config DS
388 * @param ar {@link AsyncResponse} which needs to be completed
391 @Path("/data/{identifier:.+}")
393 MediaTypes.APPLICATION_YANG_DATA_XML,
394 MediaType.APPLICATION_XML,
397 public void plainPatchDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
398 @Suspended final AsyncResponse ar) {
399 try (var xmlBody = new XmlResourceBody(body)) {
400 plainPatchData(identifier, xmlBody, ar);
405 * Partially modify the target data store, as defined in
406 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
408 * @param body data node for put to config DS
409 * @param ar {@link AsyncResponse} which needs to be completed
414 MediaTypes.APPLICATION_YANG_DATA_JSON,
415 MediaType.APPLICATION_JSON,
417 public void plainPatchDataJSON(final InputStream body, @Suspended final AsyncResponse ar) {
418 try (var jsonBody = new JsonResourceBody(body)) {
419 plainPatchData(jsonBody, ar);
424 * Partially modify the target data resource, as defined in
425 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
427 * @param identifier path to target
428 * @param body data node for put to config DS
429 * @param ar {@link AsyncResponse} which needs to be completed
432 @Path("/data/{identifier:.+}")
434 MediaTypes.APPLICATION_YANG_DATA_JSON,
435 MediaType.APPLICATION_JSON,
437 public void plainPatchDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
438 @Suspended final AsyncResponse ar) {
439 try (var jsonBody = new JsonResourceBody(body)) {
440 plainPatchData(identifier, jsonBody, ar);
445 * Partially modify the target data resource, as defined in
446 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
448 * @param body data node for put to config DS
449 * @param ar {@link AsyncResponse} which needs to be completed
451 private void plainPatchData(final ResourceBody body, final AsyncResponse ar) {
452 plainPatchData(server.bindRequestRoot(), body, ar);
456 * Partially modify the target data resource, as defined in
457 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
459 * @param identifier path to target
460 * @param body data node for put to config DS
461 * @param ar {@link AsyncResponse} which needs to be completed
463 private void plainPatchData(final String identifier, final ResourceBody body, final AsyncResponse ar) {
464 plainPatchData(server.bindRequestPath(identifier), body, ar);
468 * Partially modify the target data resource, as defined in
469 * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
471 * @param reqPath path to target
472 * @param body data node for put to config DS
473 * @param ar {@link AsyncResponse} which needs to be completed
475 private void plainPatchData(final InstanceIdentifierContext reqPath, final ResourceBody body,
476 final AsyncResponse ar) {
477 final var req = server.bindResourceRequest(reqPath, body);
478 req.strategy().merge(req.path(), req.data()).addCallback(new JaxRsRestconfCallback<>(ar) {
480 Response transform(final Empty result) {
481 return Response.ok().build();
487 * Ordered list of edits that are applied to the target datastore by the server, as defined in
488 * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
490 * @param identifier path to target
491 * @param body YANG Patch body
492 * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
495 @Path("/data/{identifier:.+}")
496 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
498 MediaTypes.APPLICATION_YANG_DATA_JSON,
499 MediaTypes.APPLICATION_YANG_DATA_XML
501 public void yangPatchDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
502 @Suspended final AsyncResponse ar) {
503 try (var xmlBody = new XmlPatchBody(body)) {
504 yangPatchData(identifier, xmlBody, ar);
509 * Ordered list of edits that are applied to the datastore by the server, as defined in
510 * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
512 * @param body YANG Patch body
513 * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
517 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
519 MediaTypes.APPLICATION_YANG_DATA_JSON,
520 MediaTypes.APPLICATION_YANG_DATA_XML
522 public void yangPatchDataXML(final InputStream body, @Suspended final AsyncResponse ar) {
523 try (var xmlBody = new XmlPatchBody(body)) {
524 yangPatchData(xmlBody, ar);
529 * Ordered list of edits that are applied to the target datastore by the server, as defined in
530 * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
532 * @param identifier path to target
533 * @param body YANG Patch body
534 * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
537 @Path("/data/{identifier:.+}")
538 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
540 MediaTypes.APPLICATION_YANG_DATA_JSON,
541 MediaTypes.APPLICATION_YANG_DATA_XML
543 public void yangPatchDataJSON(@Encoded @PathParam("identifier") final String identifier,
544 final InputStream body, @Suspended final AsyncResponse ar) {
545 try (var jsonBody = new JsonPatchBody(body)) {
546 yangPatchData(identifier, jsonBody, ar);
551 * Ordered list of edits that are applied to the datastore by the server, as defined in
552 * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
554 * @param body YANG Patch body
555 * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
559 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
561 MediaTypes.APPLICATION_YANG_DATA_JSON,
562 MediaTypes.APPLICATION_YANG_DATA_XML
564 public void yangPatchDataJSON(final InputStream body, @Suspended final AsyncResponse ar) {
565 try (var jsonBody = new JsonPatchBody(body)) {
566 yangPatchData(jsonBody, ar);
570 private void yangPatchData(final @NonNull PatchBody body, final AsyncResponse ar) {
571 final var context = server.bindRequestRoot().getSchemaContext();
572 yangPatchData(context, parsePatchBody(context, YangInstanceIdentifier.of(), body), null, ar);
575 private void yangPatchData(final String identifier, final @NonNull PatchBody body,
576 final AsyncResponse ar) {
577 final var reqPath = server.bindRequestPath(identifier);
578 final var modelContext = reqPath.getSchemaContext();
579 yangPatchData(modelContext, parsePatchBody(modelContext, reqPath.getInstanceIdentifier(), body),
580 reqPath.getMountPoint(), ar);
584 void yangPatchData(final @NonNull EffectiveModelContext modelContext,
585 final @NonNull PatchContext patch, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
586 server.getRestconfStrategy(modelContext, mountPoint).patchData(patch)
587 .addCallback(new JaxRsRestconfCallback<>(ar) {
589 Response transform(final PatchStatusContext result) {
590 return Response.status(getStatusCode(result)).entity(result).build();
595 private static Status getStatusCode(final PatchStatusContext result) {
598 } else if (result.globalErrors() == null || result.globalErrors().isEmpty()) {
599 return result.editCollection().stream()
600 .filter(patchStatus -> !patchStatus.isOk() && !patchStatus.getEditErrors().isEmpty())
602 .map(PatchStatusEntity::getEditErrors)
603 .flatMap(errors -> errors.stream().findFirst())
604 .map(error -> ErrorTags.statusOf(error.getErrorTag()))
605 .orElse(Status.INTERNAL_SERVER_ERROR);
607 final var error = result.globalErrors().iterator().next();
608 return ErrorTags.statusOf(error.getErrorTag());
612 private static @NonNull PatchContext parsePatchBody(final @NonNull EffectiveModelContext context,
613 final @NonNull YangInstanceIdentifier urlPath, final @NonNull PatchBody body) {
615 return body.toPatchContext(context, urlPath);
616 } catch (IOException e) {
617 LOG.debug("Error parsing YANG Patch input", e);
618 throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
619 ErrorTag.MALFORMED_MESSAGE, e);
624 * Invoke Action operation.
626 * @param payload {@link NormalizedNodePayload} - the body of the operation
627 * @param ar {@link AsyncResponse} which needs to be completed with a NormalizedNodePayload
629 private void invokeAction(final InstanceIdentifierContext reqPath, final OperationInputBody body,
630 final AsyncResponse ar) {
631 server.dataInvokePOST(reqPath, body).addCallback(new JaxRsRestconfCallback<>(ar) {
633 Response transform(final DOMActionResult result) {
634 final var output = result.getOutput().orElse(null);
635 return output == null || output.isEmpty() ? Response.status(Status.NO_CONTENT).build()
636 : Response.status(Status.OK).entity(new NormalizedNodePayload(reqPath.inference(), output)).build();