Introduce restconf.server.{api,spi,mdsal}
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / rests / services / impl / RestconfDataServiceImpl.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. and others.  All rights reserved.
3  *
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
7  */
8 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
9
10 import static java.util.Objects.requireNonNull;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.util.concurrent.FutureCallback;
14 import com.google.common.util.concurrent.Futures;
15 import com.google.common.util.concurrent.MoreExecutors;
16 import java.io.IOException;
17 import java.io.InputStream;
18 import java.net.URI;
19 import java.time.Clock;
20 import java.time.LocalDateTime;
21 import java.time.format.DateTimeFormatter;
22 import java.util.List;
23 import java.util.concurrent.CancellationException;
24 import javax.ws.rs.Consumes;
25 import javax.ws.rs.DELETE;
26 import javax.ws.rs.Encoded;
27 import javax.ws.rs.GET;
28 import javax.ws.rs.PATCH;
29 import javax.ws.rs.POST;
30 import javax.ws.rs.PUT;
31 import javax.ws.rs.Path;
32 import javax.ws.rs.PathParam;
33 import javax.ws.rs.Produces;
34 import javax.ws.rs.container.AsyncResponse;
35 import javax.ws.rs.container.Suspended;
36 import javax.ws.rs.core.Context;
37 import javax.ws.rs.core.MediaType;
38 import javax.ws.rs.core.Response;
39 import javax.ws.rs.core.Response.Status;
40 import javax.ws.rs.core.UriInfo;
41 import org.eclipse.jdt.annotation.NonNull;
42 import org.eclipse.jdt.annotation.Nullable;
43 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
44 import org.opendaylight.mdsal.dom.api.DOMActionException;
45 import org.opendaylight.mdsal.dom.api.DOMActionResult;
46 import org.opendaylight.mdsal.dom.api.DOMActionService;
47 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
48 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
49 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
50 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
51 import org.opendaylight.restconf.common.errors.RestconfFuture;
52 import org.opendaylight.restconf.common.errors.SettableRestconfFuture;
53 import org.opendaylight.restconf.common.patch.PatchContext;
54 import org.opendaylight.restconf.common.patch.PatchStatusContext;
55 import org.opendaylight.restconf.common.patch.PatchStatusEntity;
56 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
57 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
58 import org.opendaylight.restconf.nb.rfc8040.databind.ChildBody;
59 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
60 import org.opendaylight.restconf.nb.rfc8040.databind.JsonChildBody;
61 import org.opendaylight.restconf.nb.rfc8040.databind.JsonOperationInputBody;
62 import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
63 import org.opendaylight.restconf.nb.rfc8040.databind.JsonResourceBody;
64 import org.opendaylight.restconf.nb.rfc8040.databind.OperationInputBody;
65 import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
66 import org.opendaylight.restconf.nb.rfc8040.databind.ResourceBody;
67 import org.opendaylight.restconf.nb.rfc8040.databind.XmlChildBody;
68 import org.opendaylight.restconf.nb.rfc8040.databind.XmlOperationInputBody;
69 import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
70 import org.opendaylight.restconf.nb.rfc8040.databind.XmlResourceBody;
71 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
72 import org.opendaylight.restconf.nb.rfc8040.legacy.ErrorTags;
73 import org.opendaylight.restconf.nb.rfc8040.legacy.InstanceIdentifierContext;
74 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
75 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy.CreateOrReplaceResult;
76 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
77 import org.opendaylight.yangtools.yang.common.Empty;
78 import org.opendaylight.yangtools.yang.common.ErrorTag;
79 import org.opendaylight.yangtools.yang.common.ErrorType;
80 import org.opendaylight.yangtools.yang.common.QName;
81 import org.opendaylight.yangtools.yang.common.Revision;
82 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
83 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
84 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
85 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
86 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
87 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
88 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
89 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
90 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
91 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
92 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
93 import org.slf4j.Logger;
94 import org.slf4j.LoggerFactory;
95
96 /**
97  * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
98  * and state data nodes.
99  */
100 @Path("/")
101 public final class RestconfDataServiceImpl {
102     private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
103     private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
104
105     private final DatabindProvider databindProvider;
106     private final DOMActionService actionService;
107     private final MdsalRestconfServer server;
108
109     public RestconfDataServiceImpl(final DatabindProvider databindProvider, final MdsalRestconfServer server,
110             final DOMActionService actionService) {
111         this.databindProvider = requireNonNull(databindProvider);
112         this.server = requireNonNull(server);
113         this.actionService = requireNonNull(actionService);
114     }
115
116     /**
117      * Get target data resource from data root.
118      *
119      * @param uriInfo URI info
120      * @return {@link NormalizedNodePayload}
121      */
122     @GET
123     @Path("/data")
124     @Produces({
125         MediaTypes.APPLICATION_YANG_DATA_JSON,
126         MediaTypes.APPLICATION_YANG_DATA_XML,
127         MediaType.APPLICATION_JSON,
128         MediaType.APPLICATION_XML,
129         MediaType.TEXT_XML
130     })
131     public Response readData(@Context final UriInfo uriInfo) {
132         final var readParams = QueryParams.newReadDataParams(uriInfo);
133         return readData(server.bindRequestRoot(), readParams);
134     }
135
136     /**
137      * Get target data resource.
138      *
139      * @param identifier path to target
140      * @param uriInfo URI info
141      * @return {@link NormalizedNodePayload}
142      */
143     @GET
144     @Path("/data/{identifier:.+}")
145     @Produces({
146         MediaTypes.APPLICATION_YANG_DATA_JSON,
147         MediaTypes.APPLICATION_YANG_DATA_XML,
148         MediaType.APPLICATION_JSON,
149         MediaType.APPLICATION_XML,
150         MediaType.TEXT_XML
151     })
152     public Response readData(@Encoded @PathParam("identifier") final String identifier,
153             @Context final UriInfo uriInfo) {
154         final var readParams = QueryParams.newReadDataParams(uriInfo);
155         return readData(server.bindRequestPath(identifier), readParams);
156     }
157
158     private Response readData(final InstanceIdentifierContext reqPath, final ReadDataParams readParams) {
159         final var queryParams = QueryParams.newQueryParameters(readParams, reqPath);
160         final var fieldPaths = queryParams.fieldPaths();
161         final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
162         final NormalizedNode node;
163         if (fieldPaths != null && !fieldPaths.isEmpty()) {
164             node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
165                 readParams.withDefaults(), fieldPaths);
166         } else {
167             node = strategy.readData(readParams.content(), reqPath.getInstanceIdentifier(),
168                 readParams.withDefaults());
169         }
170         if (node == null) {
171             throw new RestconfDocumentedException(
172                 "Request could not be completed because the relevant data model content does not exist",
173                 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
174         }
175
176         return switch (readParams.content()) {
177             case ALL, CONFIG -> {
178                 final QName type = node.name().getNodeType();
179                 yield Response.status(Status.OK)
180                     .entity(new NormalizedNodePayload(reqPath.inference(), node, queryParams))
181                     .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) + "-"
182                         + type.getLocalName() + '"')
183                     .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
184                     .build();
185             }
186             case NONCONFIG -> Response.status(Status.OK)
187                 .entity(new NormalizedNodePayload(reqPath.inference(), node, queryParams))
188                 .build();
189         };
190     }
191
192     /**
193      * Replace the data store.
194      *
195      * @param uriInfo request URI information
196      * @param body data node for put to config DS
197      * @param ar {@link AsyncResponse} which needs to be completed
198      */
199     @PUT
200     @Path("/data")
201     @Consumes({
202         MediaTypes.APPLICATION_YANG_DATA_JSON,
203         MediaType.APPLICATION_JSON,
204     })
205     public void putDataJSON(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
206         try (var jsonBody = new JsonResourceBody(body)) {
207             putData(null, uriInfo, jsonBody, ar);
208         }
209     }
210
211     /**
212      * Create or replace the target data resource.
213      *
214      * @param identifier path to target
215      * @param uriInfo request URI information
216      * @param body data node for put to config DS
217      * @param ar {@link AsyncResponse} which needs to be completed
218      */
219     @PUT
220     @Path("/data/{identifier:.+}")
221     @Consumes({
222         MediaTypes.APPLICATION_YANG_DATA_JSON,
223         MediaType.APPLICATION_JSON,
224     })
225     public void putDataJSON(@Encoded @PathParam("identifier") final String identifier,
226             @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
227         try (var jsonBody = new JsonResourceBody(body)) {
228             putData(identifier, uriInfo, jsonBody, ar);
229         }
230     }
231
232     /**
233      * Replace the data store.
234      *
235      * @param uriInfo request URI information
236      * @param body data node for put to config DS
237      * @param ar {@link AsyncResponse} which needs to be completed
238      */
239     @PUT
240     @Path("/data")
241     @Consumes({
242         MediaTypes.APPLICATION_YANG_DATA_XML,
243         MediaType.APPLICATION_XML,
244         MediaType.TEXT_XML
245     })
246     public void putDataXML(@Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
247         try (var xmlBody = new XmlResourceBody(body)) {
248             putData(null, uriInfo, xmlBody, ar);
249         }
250     }
251
252     /**
253      * Create or replace the target data resource.
254      *
255      * @param identifier path to target
256      * @param uriInfo request URI information
257      * @param body data node for put to config DS
258      * @param ar {@link AsyncResponse} which needs to be completed
259      */
260     @PUT
261     @Path("/data/{identifier:.+}")
262     @Consumes({
263         MediaTypes.APPLICATION_YANG_DATA_XML,
264         MediaType.APPLICATION_XML,
265         MediaType.TEXT_XML
266     })
267     public void putDataXML(@Encoded @PathParam("identifier") final String identifier,
268             @Context final UriInfo uriInfo, final InputStream body, @Suspended final AsyncResponse ar) {
269         try (var xmlBody = new XmlResourceBody(body)) {
270             putData(identifier, uriInfo, xmlBody, ar);
271         }
272     }
273
274     private void putData(final @Nullable String identifier, final UriInfo uriInfo, final ResourceBody body,
275             final AsyncResponse ar) {
276         final var reqPath = server.bindRequestPath(identifier);
277         final var insert = QueryParams.parseInsert(reqPath.getSchemaContext(), uriInfo);
278         final var req = bindResourceRequest(reqPath, body);
279
280         req.strategy().putData(req.path(), req.data(), insert).addCallback(new JaxRsRestconfCallback<>(ar) {
281             @Override
282             Response transform(final CreateOrReplaceResult result) {
283                 return switch (result) {
284                     // Note: no Location header, as it matches the request path
285                     case CREATED -> Response.status(Status.CREATED).build();
286                     case REPLACED -> Response.noContent().build();
287                 };
288             }
289         });
290     }
291
292     /**
293      * Create a top-level data resource.
294      *
295      * @param body data node for put to config DS
296      * @param uriInfo URI info
297      * @param ar {@link AsyncResponse} which needs to be completed
298      */
299     @POST
300     @Path("/data")
301     @Consumes({
302         MediaTypes.APPLICATION_YANG_DATA_JSON,
303         MediaType.APPLICATION_JSON,
304     })
305     public void postDataJSON(final InputStream body, @Context final UriInfo uriInfo,
306             @Suspended final AsyncResponse ar) {
307         try (var jsonBody = new JsonChildBody(body)) {
308             postData(jsonBody, uriInfo, ar);
309         }
310     }
311
312     /**
313      * Create a data resource in target.
314      *
315      * @param identifier path to target
316      * @param body data node for put to config DS
317      * @param uriInfo URI info
318      * @param ar {@link AsyncResponse} which needs to be completed
319      */
320     @POST
321     @Path("/data/{identifier:.+}")
322     @Consumes({
323         MediaTypes.APPLICATION_YANG_DATA_JSON,
324         MediaType.APPLICATION_JSON,
325     })
326     public void postDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
327             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
328         final var reqPath = server.bindRequestPath(identifier);
329         if (reqPath.getSchemaNode() instanceof ActionDefinition) {
330             try (var jsonBody = new JsonOperationInputBody(body)) {
331                 invokeAction(reqPath, jsonBody, ar);
332             }
333         } else {
334             try (var jsonBody = new JsonChildBody(body)) {
335                 postData(reqPath.inference(), reqPath.getInstanceIdentifier(), jsonBody, uriInfo,
336                     reqPath.getMountPoint(), ar);
337             }
338         }
339     }
340
341     /**
342      * Create a top-level data resource.
343      *
344      * @param body data node for put to config DS
345      * @param uriInfo URI info
346      * @param ar {@link AsyncResponse} which needs to be completed
347      */
348     @POST
349     @Path("/data")
350     @Consumes({
351         MediaTypes.APPLICATION_YANG_DATA_XML,
352         MediaType.APPLICATION_XML,
353         MediaType.TEXT_XML
354     })
355     public void postDataXML(final InputStream body, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
356         try (var xmlBody = new XmlChildBody(body)) {
357             postData(xmlBody, uriInfo, ar);
358         }
359     }
360
361     /**
362      * Create a data resource in target.
363      *
364      * @param identifier path to target
365      * @param body data node for put to config DS
366      * @param uriInfo URI info
367      * @param ar {@link AsyncResponse} which needs to be completed
368      */
369     @POST
370     @Path("/data/{identifier:.+}")
371     @Consumes({
372         MediaTypes.APPLICATION_YANG_DATA_XML,
373         MediaType.APPLICATION_XML,
374         MediaType.TEXT_XML
375     })
376     public void postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
377             @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
378         final var reqPath = server.bindRequestPath(identifier);
379         if (reqPath.getSchemaNode() instanceof ActionDefinition) {
380             try (var xmlBody = new XmlOperationInputBody(body)) {
381                 invokeAction(reqPath, xmlBody, ar);
382             }
383         } else {
384             try (var xmlBody = new XmlChildBody(body)) {
385                 postData(reqPath.inference(), reqPath.getInstanceIdentifier(), xmlBody, uriInfo,
386                     reqPath.getMountPoint(), ar);
387             }
388         }
389     }
390
391     private void postData(final ChildBody body, final UriInfo uriInfo, final AsyncResponse ar) {
392         postData(Inference.ofDataTreePath(databindProvider.currentContext().modelContext()),
393             YangInstanceIdentifier.of(), body, uriInfo, null, ar);
394     }
395
396     private void postData(final Inference inference, final YangInstanceIdentifier parentPath, final ChildBody body,
397             final UriInfo uriInfo, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
398         final var modelContext = inference.getEffectiveModelContext();
399         final var insert = QueryParams.parseInsert(modelContext, uriInfo);
400         final var strategy = server.getRestconfStrategy(modelContext, mountPoint);
401         final var payload = body.toPayload(parentPath, inference);
402         final var data = payload.body();
403         final var path = concat(parentPath, payload.prefix());
404
405         strategy.postData(path, data, insert).addCallback(new JaxRsRestconfCallback<>(ar) {
406             @Override
407             Response transform(final Empty result) {
408                 return Response.created(resolveLocation(uriInfo, path, modelContext, data)).build();
409             }
410         });
411     }
412
413     private static YangInstanceIdentifier concat(final YangInstanceIdentifier parent, final List<PathArgument> args) {
414         var ret = parent;
415         for (var arg : args) {
416             ret = ret.node(arg);
417         }
418         return ret;
419     }
420
421     /**
422      * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
423      *
424      * @param uriInfo       uri info
425      * @param initialPath   data path
426      * @param schemaContext reference to {@link SchemaContext}
427      * @return {@link URI}
428      */
429     private static URI resolveLocation(final UriInfo uriInfo, final YangInstanceIdentifier initialPath,
430                                        final EffectiveModelContext schemaContext, final NormalizedNode data) {
431         YangInstanceIdentifier path = initialPath;
432         if (data instanceof MapNode mapData) {
433             final var children = mapData.body();
434             if (!children.isEmpty()) {
435                 path = path.node(children.iterator().next().name());
436             }
437         }
438
439         return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
440     }
441
442     /**
443      * Delete the target data resource.
444      *
445      * @param identifier path to target
446      * @param ar {@link AsyncResponse} which needs to be completed
447      */
448     @DELETE
449     @Path("/data/{identifier:.+}")
450     public void deleteData(@Encoded @PathParam("identifier") final String identifier,
451             @Suspended final AsyncResponse ar) {
452         final var reqPath = server.bindRequestPath(identifier);
453         final var strategy = server.getRestconfStrategy(reqPath.getSchemaContext(), reqPath.getMountPoint());
454
455         strategy.delete(reqPath.getInstanceIdentifier()).addCallback(new JaxRsRestconfCallback<>(ar) {
456             @Override
457             Response transform(final Empty result) {
458                 return Response.noContent().build();
459             }
460         });
461     }
462
463     /**
464      * Partially modify the target data store, as defined in
465      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
466      *
467      * @param body data node for put to config DS
468      * @param ar {@link AsyncResponse} which needs to be completed
469      */
470     @PATCH
471     @Path("/data")
472     @Consumes({
473         MediaTypes.APPLICATION_YANG_DATA_XML,
474         MediaType.APPLICATION_XML,
475         MediaType.TEXT_XML
476     })
477     public void plainPatchDataXML(final InputStream body, @Suspended final AsyncResponse ar) {
478         try (var xmlBody = new XmlResourceBody(body)) {
479             plainPatchData(xmlBody, ar);
480         }
481     }
482
483     /**
484      * Partially modify the target data resource, as defined in
485      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
486      *
487      * @param identifier path to target
488      * @param body data node for put to config DS
489      * @param ar {@link AsyncResponse} which needs to be completed
490      */
491     @PATCH
492     @Path("/data/{identifier:.+}")
493     @Consumes({
494         MediaTypes.APPLICATION_YANG_DATA_XML,
495         MediaType.APPLICATION_XML,
496         MediaType.TEXT_XML
497     })
498     public void plainPatchDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
499             @Suspended final AsyncResponse ar) {
500         try (var xmlBody = new XmlResourceBody(body)) {
501             plainPatchData(identifier, xmlBody, ar);
502         }
503     }
504
505     /**
506      * Partially modify the target data store, as defined in
507      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
508      *
509      * @param body data node for put to config DS
510      * @param ar {@link AsyncResponse} which needs to be completed
511      */
512     @PATCH
513     @Path("/data")
514     @Consumes({
515         MediaTypes.APPLICATION_YANG_DATA_JSON,
516         MediaType.APPLICATION_JSON,
517     })
518     public void plainPatchDataJSON(final InputStream body, @Suspended final AsyncResponse ar) {
519         try (var jsonBody = new JsonResourceBody(body)) {
520             plainPatchData(jsonBody, ar);
521         }
522     }
523
524     /**
525      * Partially modify the target data resource, as defined in
526      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
527      *
528      * @param identifier path to target
529      * @param body data node for put to config DS
530      * @param ar {@link AsyncResponse} which needs to be completed
531      */
532     @PATCH
533     @Path("/data/{identifier:.+}")
534     @Consumes({
535         MediaTypes.APPLICATION_YANG_DATA_JSON,
536         MediaType.APPLICATION_JSON,
537     })
538     public void plainPatchDataJSON(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
539             @Suspended final AsyncResponse ar) {
540         try (var jsonBody = new JsonResourceBody(body)) {
541             plainPatchData(identifier, jsonBody, ar);
542         }
543     }
544
545     /**
546      * Partially modify the target data resource, as defined in
547      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
548      *
549      * @param body data node for put to config DS
550      * @param ar {@link AsyncResponse} which needs to be completed
551      */
552     private void plainPatchData(final ResourceBody body, final AsyncResponse ar) {
553         plainPatchData(server.bindRequestRoot(), body, ar);
554     }
555
556     /**
557      * Partially modify the target data resource, as defined in
558      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
559      *
560      * @param identifier path to target
561      * @param body data node for put to config DS
562      * @param ar {@link AsyncResponse} which needs to be completed
563      */
564     private void plainPatchData(final String identifier, final ResourceBody body, final AsyncResponse ar) {
565         plainPatchData(server.bindRequestPath(identifier), body, ar);
566     }
567
568     /**
569      * Partially modify the target data resource, as defined in
570      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
571      *
572      * @param reqPath path to target
573      * @param body data node for put to config DS
574      * @param ar {@link AsyncResponse} which needs to be completed
575      */
576     private void plainPatchData(final InstanceIdentifierContext reqPath, final ResourceBody body,
577             final AsyncResponse ar) {
578         final var req = bindResourceRequest(reqPath, body);
579         req.strategy().merge(req.path(), req.data()).addCallback(new JaxRsRestconfCallback<>(ar) {
580             @Override
581             Response transform(final Empty result) {
582                 return Response.ok().build();
583             }
584         });
585     }
586
587     private @NonNull ResourceRequest bindResourceRequest(final InstanceIdentifierContext reqPath,
588             final ResourceBody body) {
589         final var inference = reqPath.inference();
590         final var path = reqPath.getInstanceIdentifier();
591         final var data = body.toNormalizedNode(path, inference, reqPath.getSchemaNode());
592
593         return new ResourceRequest(
594             server.getRestconfStrategy(inference.getEffectiveModelContext(), reqPath.getMountPoint()),
595             path, data);
596     }
597
598     /**
599      * Ordered list of edits that are applied to the target datastore by the server, as defined in
600      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
601      *
602      * @param identifier path to target
603      * @param body YANG Patch body
604      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
605      */
606     @PATCH
607     @Path("/data/{identifier:.+}")
608     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
609     @Produces({
610         MediaTypes.APPLICATION_YANG_DATA_JSON,
611         MediaTypes.APPLICATION_YANG_DATA_XML
612     })
613     public void yangPatchDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
614             @Suspended final AsyncResponse ar) {
615         try (var xmlBody = new XmlPatchBody(body)) {
616             yangPatchData(identifier, xmlBody, ar);
617         }
618     }
619
620     /**
621      * Ordered list of edits that are applied to the datastore by the server, as defined in
622      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
623      *
624      * @param body YANG Patch body
625      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
626      */
627     @PATCH
628     @Path("/data")
629     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
630     @Produces({
631         MediaTypes.APPLICATION_YANG_DATA_JSON,
632         MediaTypes.APPLICATION_YANG_DATA_XML
633     })
634     public void yangPatchDataXML(final InputStream body, @Suspended final AsyncResponse ar) {
635         try (var xmlBody = new XmlPatchBody(body)) {
636             yangPatchData(xmlBody, ar);
637         }
638     }
639
640     /**
641      * Ordered list of edits that are applied to the target datastore by the server, as defined in
642      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
643      *
644      * @param identifier path to target
645      * @param body YANG Patch body
646      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
647      */
648     @PATCH
649     @Path("/data/{identifier:.+}")
650     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
651     @Produces({
652         MediaTypes.APPLICATION_YANG_DATA_JSON,
653         MediaTypes.APPLICATION_YANG_DATA_XML
654     })
655     public void yangPatchDataJSON(@Encoded @PathParam("identifier") final String identifier,
656             final InputStream body, @Suspended final AsyncResponse ar) {
657         try (var jsonBody = new JsonPatchBody(body)) {
658             yangPatchData(identifier, jsonBody, ar);
659         }
660     }
661
662     /**
663      * Ordered list of edits that are applied to the datastore by the server, as defined in
664      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
665      *
666      * @param body YANG Patch body
667      * @param ar {@link AsyncResponse} which needs to be completed with a {@link PatchStatusContext}
668      */
669     @PATCH
670     @Path("/data")
671     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
672     @Produces({
673         MediaTypes.APPLICATION_YANG_DATA_JSON,
674         MediaTypes.APPLICATION_YANG_DATA_XML
675     })
676     public void yangPatchDataJSON(final InputStream body, @Suspended final AsyncResponse ar) {
677         try (var jsonBody = new JsonPatchBody(body)) {
678             yangPatchData(jsonBody, ar);
679         }
680     }
681
682     private void yangPatchData(final @NonNull PatchBody body, final AsyncResponse ar) {
683         final var context = server.bindRequestRoot().getSchemaContext();
684         yangPatchData(context, parsePatchBody(context, YangInstanceIdentifier.of(), body), null, ar);
685     }
686
687     private void yangPatchData(final String identifier, final @NonNull PatchBody body,
688             final AsyncResponse ar) {
689         final var reqPath = server.bindRequestPath(identifier);
690         final var modelContext = reqPath.getSchemaContext();
691         yangPatchData(modelContext, parsePatchBody(modelContext, reqPath.getInstanceIdentifier(), body),
692             reqPath.getMountPoint(), ar);
693     }
694
695     @VisibleForTesting
696     void yangPatchData(final @NonNull EffectiveModelContext modelContext,
697             final @NonNull PatchContext patch, final @Nullable DOMMountPoint mountPoint, final AsyncResponse ar) {
698         server.getRestconfStrategy(modelContext, mountPoint).patchData(patch)
699             .addCallback(new JaxRsRestconfCallback<>(ar) {
700                 @Override
701                 Response transform(final PatchStatusContext result) {
702                     return Response.status(getStatusCode(result)).entity(result).build();
703                 }
704             });
705     }
706
707     private static Status getStatusCode(final PatchStatusContext result) {
708         if (result.ok()) {
709             return Status.OK;
710         } else if (result.globalErrors() == null || result.globalErrors().isEmpty()) {
711             return result.editCollection().stream()
712                 .filter(patchStatus -> !patchStatus.isOk() && !patchStatus.getEditErrors().isEmpty())
713                 .findFirst()
714                 .map(PatchStatusEntity::getEditErrors)
715                 .flatMap(errors -> errors.stream().findFirst())
716                 .map(error -> ErrorTags.statusOf(error.getErrorTag()))
717                 .orElse(Status.INTERNAL_SERVER_ERROR);
718         } else {
719             final var error = result.globalErrors().iterator().next();
720             return ErrorTags.statusOf(error.getErrorTag());
721         }
722     }
723
724     private static @NonNull PatchContext parsePatchBody(final @NonNull EffectiveModelContext context,
725             final @NonNull YangInstanceIdentifier urlPath, final @NonNull PatchBody body) {
726         try {
727             return body.toPatchContext(context, urlPath);
728         } catch (IOException e) {
729             LOG.debug("Error parsing YANG Patch input", e);
730             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
731                     ErrorTag.MALFORMED_MESSAGE, e);
732         }
733     }
734
735     /**
736      * Invoke Action operation.
737      *
738      * @param payload {@link NormalizedNodePayload} - the body of the operation
739      * @param ar {@link AsyncResponse} which needs to be completed with a NormalizedNodePayload
740      */
741     private void invokeAction(final InstanceIdentifierContext reqPath, final OperationInputBody body,
742             final AsyncResponse ar) {
743         final var yangIIdContext = reqPath.getInstanceIdentifier();
744         final ContainerNode input;
745         try {
746             input = body.toContainerNode(reqPath.inference());
747         } catch (IOException e) {
748             LOG.debug("Error reading input", e);
749             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
750                     ErrorTag.MALFORMED_MESSAGE, e);
751         }
752
753         final var mountPoint = reqPath.getMountPoint();
754         final var inference = reqPath.inference();
755         final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
756         final var response = mountPoint != null ? invokeAction(input, schemaPath, yangIIdContext, mountPoint)
757             : invokeAction(input, schemaPath, yangIIdContext, actionService);
758
759         response.addCallback(new JaxRsRestconfCallback<>(ar) {
760             @Override
761             Response transform(final DOMActionResult result) {
762                 final var output = result.getOutput().orElse(null);
763                 return output == null || output.isEmpty() ? Response.status(Status.NO_CONTENT).build()
764                     : Response.status(Status.OK).entity(new NormalizedNodePayload(inference, output)).build();
765             }
766         });
767     }
768
769     /**
770      * Invoking Action via mount point.
771      *
772      * @param mountPoint mount point
773      * @param data input data
774      * @param schemaPath schema path of data
775      * @return {@link DOMActionResult}
776      */
777     private static RestconfFuture<DOMActionResult> invokeAction(final ContainerNode data,
778             final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
779         final var actionService = mountPoint.getService(DOMActionService.class);
780         return actionService.isPresent() ? invokeAction(data, schemaPath, yangIId, actionService.orElseThrow())
781             : RestconfFuture.failed(new RestconfDocumentedException("DOMActionService is missing."));
782     }
783
784     /**
785      * Invoke Action via ActionServiceHandler.
786      *
787      * @param data input data
788      * @param yangIId invocation context
789      * @param schemaPath schema path of data
790      * @param actionService action service to invoke action
791      * @return {@link DOMActionResult}
792      */
793     private static RestconfFuture<DOMActionResult> invokeAction(final ContainerNode data, final Absolute schemaPath,
794             final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
795         final var ret = new SettableRestconfFuture<DOMActionResult>();
796
797         Futures.addCallback(actionService.invokeAction(schemaPath,
798             new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
799             new FutureCallback<DOMActionResult>() {
800                 @Override
801                 public void onSuccess(final DOMActionResult result) {
802                     final var errors = result.getErrors();
803                     LOG.debug("InvokeAction Error Message {}", errors);
804                     if (errors.isEmpty()) {
805                         ret.set(result);
806                     } else {
807                         ret.setFailure(new RestconfDocumentedException("InvokeAction Error Message ", null, errors));
808                     }
809                 }
810
811                 @Override
812                 public void onFailure(final Throwable cause) {
813                     if (cause instanceof DOMActionException) {
814                         ret.set(new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
815                             ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))));
816                     } else if (cause instanceof RestconfDocumentedException e) {
817                         ret.setFailure(e);
818                     } else if (cause instanceof CancellationException) {
819                         ret.setFailure(new RestconfDocumentedException("Action cancelled while executing",
820                             ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, cause));
821                     } else {
822                         ret.setFailure(new RestconfDocumentedException("Invocation failed", cause));
823                     }
824                 }
825             }, MoreExecutors.directExecutor());
826
827         return ret;
828     }
829 }