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