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