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