2b0258d62e7e09997617033d11e2724bf4f6760c
[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.HashMap;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.concurrent.CancellationException;
32 import java.util.concurrent.ExecutionException;
33 import javax.ws.rs.Consumes;
34 import javax.ws.rs.DELETE;
35 import javax.ws.rs.Encoded;
36 import javax.ws.rs.GET;
37 import javax.ws.rs.PATCH;
38 import javax.ws.rs.POST;
39 import javax.ws.rs.PUT;
40 import javax.ws.rs.Path;
41 import javax.ws.rs.PathParam;
42 import javax.ws.rs.Produces;
43 import javax.ws.rs.container.AsyncResponse;
44 import javax.ws.rs.container.Suspended;
45 import javax.ws.rs.core.Context;
46 import javax.ws.rs.core.MediaType;
47 import javax.ws.rs.core.Response;
48 import javax.ws.rs.core.Response.Status;
49 import javax.ws.rs.core.UriInfo;
50 import org.eclipse.jdt.annotation.NonNull;
51 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
52 import org.opendaylight.mdsal.dom.api.DOMActionException;
53 import org.opendaylight.mdsal.dom.api.DOMActionResult;
54 import org.opendaylight.mdsal.dom.api.DOMActionService;
55 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
56 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
57 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteOperations;
58 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
59 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
60 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
61 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
62 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
63 import org.opendaylight.restconf.common.patch.PatchContext;
64 import org.opendaylight.restconf.common.patch.PatchStatusContext;
65 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
66 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
67 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
68 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
69 import org.opendaylight.restconf.nb.rfc8040.databind.JsonPatchBody;
70 import org.opendaylight.restconf.nb.rfc8040.databind.PatchBody;
71 import org.opendaylight.restconf.nb.rfc8040.databind.XmlPatchBody;
72 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
73 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
74 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
75 import org.opendaylight.restconf.nb.rfc8040.monitoring.RestconfStateStreams;
76 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
77 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
78 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
79 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
80 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
81 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PutDataTransactionUtil;
82 import org.opendaylight.restconf.nb.rfc8040.rests.utils.ReadDataTransactionUtil;
83 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants;
84 import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
85 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.ListenersBroker;
86 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.NotificationListenerAdapter;
87 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
88 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
89 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.restconf.restconf.Data;
90 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
91 import org.opendaylight.yangtools.yang.common.Empty;
92 import org.opendaylight.yangtools.yang.common.ErrorTag;
93 import org.opendaylight.yangtools.yang.common.ErrorType;
94 import org.opendaylight.yangtools.yang.common.QName;
95 import org.opendaylight.yangtools.yang.common.Revision;
96 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
97 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
98 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
99 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
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.ListSchemaNode;
107 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
108 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
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
131     public RestconfDataServiceImpl(final DatabindProvider databindProvider,
132             final DOMDataBroker dataBroker, final DOMMountPointService  mountPointService,
133             final RestconfStreamsSubscriptionService delegRestconfSubscrService,
134             final DOMActionService actionService, final StreamsConfiguration configuration) {
135         this.databindProvider = requireNonNull(databindProvider);
136         this.dataBroker = requireNonNull(dataBroker);
137         restconfStrategy = new MdsalRestconfStrategy(dataBroker);
138         this.mountPointService = requireNonNull(mountPointService);
139         this.delegRestconfSubscrService = requireNonNull(delegRestconfSubscrService);
140         this.actionService = requireNonNull(actionService);
141         streamUtils = configuration.useSSE() ? SubscribeToStreamUtil.serverSentEvents()
142                 : SubscribeToStreamUtil.webSockets();
143     }
144
145     /**
146      * Get target data resource from data root.
147      *
148      * @param uriInfo URI info
149      * @return {@link NormalizedNodePayload}
150      */
151     @GET
152     @Path("/data")
153     @Produces({
154         MediaTypes.APPLICATION_YANG_DATA_JSON,
155         MediaTypes.APPLICATION_YANG_DATA_XML,
156         MediaType.APPLICATION_JSON,
157         MediaType.APPLICATION_XML,
158         MediaType.TEXT_XML
159     })
160     public Response readData(@Context final UriInfo uriInfo) {
161         return readData(null, uriInfo);
162     }
163
164     /**
165      * Get target data resource.
166      *
167      * @param identifier path to target
168      * @param uriInfo URI info
169      * @return {@link NormalizedNodePayload}
170      */
171     @GET
172     @Path("/data/{identifier:.+}")
173     @Produces({
174         MediaTypes.APPLICATION_YANG_DATA_JSON,
175         MediaTypes.APPLICATION_YANG_DATA_XML,
176         MediaType.APPLICATION_JSON,
177         MediaType.APPLICATION_XML,
178         MediaType.TEXT_XML
179     })
180     public Response readData(@Encoded @PathParam("identifier") final String identifier,
181             @Context final UriInfo uriInfo) {
182         final ReadDataParams readParams = QueryParams.newReadDataParams(uriInfo);
183
184         final EffectiveModelContext schemaContextRef = databindProvider.currentContext().modelContext();
185         // FIXME: go through
186         final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
187                 identifier, schemaContextRef, mountPointService);
188         final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
189
190         // FIXME: this looks quite crazy, why do we even have it?
191         if (mountPoint == null && identifier != null && identifier.contains(STREAMS_PATH)
192             && !identifier.contains(STREAM_PATH_PART)) {
193             createAllYangNotificationStreams(schemaContextRef, uriInfo);
194         }
195
196         final QueryParameters queryParams = QueryParams.newQueryParameters(readParams, instanceIdentifier);
197         final List<YangInstanceIdentifier> fieldPaths = queryParams.fieldPaths();
198         final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
199         final NormalizedNode node;
200         if (fieldPaths != null && !fieldPaths.isEmpty()) {
201             node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
202                     strategy, readParams.withDefaults(), schemaContextRef, fieldPaths);
203         } else {
204             node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
205                     strategy, readParams.withDefaults(), schemaContextRef);
206         }
207
208         // FIXME: this is utter craziness, refactor it properly!
209         if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART)
210                 && identifier.contains(STREAM_LOCATION_PATH_PART)) {
211             final String value = (String) node.body();
212             final String streamName = value.substring(value.indexOf(NOTIFICATION_STREAM + '/'));
213             delegRestconfSubscrService.subscribeToStream(streamName, uriInfo);
214         }
215         if (node == null) {
216             throw new RestconfDocumentedException(
217                     "Request could not be completed because the relevant data model content does not exist",
218                     ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
219         }
220
221         return switch (readParams.content()) {
222             case ALL, CONFIG -> {
223                 final QName type = node.name().getNodeType();
224                 yield Response.status(Status.OK)
225                     .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
226                     .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) + "-"
227                         + type.getLocalName() + '"')
228                     .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
229                     .build();
230             }
231             case NONCONFIG -> Response.status(Status.OK)
232                 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
233                 .build();
234         };
235     }
236
237     private void createAllYangNotificationStreams(final EffectiveModelContext schemaContext, final UriInfo uriInfo) {
238         final var transaction = dataBroker.newWriteOnlyTransaction();
239
240         for (var module : schemaContext.getModuleStatements().values()) {
241             final var moduleName = module.argument().getLocalName();
242             // Note: this handles only RFC6020 notifications
243             module.streamEffectiveSubstatements(NotificationEffectiveStatement.class).forEach(notification -> {
244                 final var notifName = notification.argument();
245
246                 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
247                     createYangNotifiStream(moduleName, notifName, NotificationOutputType.XML));
248                 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
249                     createYangNotifiStream(moduleName, notifName, NotificationOutputType.JSON));
250             });
251         }
252
253         try {
254             transaction.commit().get();
255         } catch (final InterruptedException | ExecutionException e) {
256             throw new RestconfDocumentedException("Problem while putting data to DS.", e);
257         }
258     }
259
260     private static NotificationListenerAdapter createYangNotifiStream(final String moduleName, final QName notifName,
261             final NotificationOutputType outputType) {
262         final var streamName = createNotificationStreamName(moduleName, notifName.getLocalName(), outputType);
263         final var listenersBroker = ListenersBroker.getInstance();
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      * Create or replace the target data resource.
293      *
294      * @param identifier path to target
295      * @param payload data node for put to config DS
296      * @return {@link Response}
297      */
298     @PUT
299     @Path("/data/{identifier:.+}")
300     @Consumes({
301         MediaTypes.APPLICATION_YANG_DATA_JSON,
302         MediaTypes.APPLICATION_YANG_DATA_XML,
303         MediaType.APPLICATION_JSON,
304         MediaType.APPLICATION_XML,
305         MediaType.TEXT_XML
306     })
307     public Response putData(@Encoded @PathParam("identifier") final String identifier,
308             final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
309         requireNonNull(payload);
310
311         final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo);
312
313         final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
314         final YangInstanceIdentifier path = iid.getInstanceIdentifier();
315
316         validInputData(iid.getSchemaNode() != null, payload);
317         validTopLevelNodeName(path, payload);
318         validateListKeysEqualityInPayloadAndUri(payload);
319
320         final var strategy = getRestconfStrategy(iid.getMountPoint());
321         final var result = PutDataTransactionUtil.putData(path, payload.getData(), iid.getSchemaContext(), strategy,
322             params);
323         return switch (result) {
324             // Note: no Location header, as it matches the request path
325             case CREATED -> Response.status(Status.CREATED).build();
326             case REPLACED -> Response.noContent().build();
327         };
328     }
329
330     /**
331      * Create a data resource in target.
332      *
333      * @param identifier path to target
334      * @param payload new data
335      * @param uriInfo URI info
336      * @return {@link Response}
337      */
338     @POST
339     @Path("/data/{identifier:.+}")
340     @Consumes({
341         MediaTypes.APPLICATION_YANG_DATA_JSON,
342         MediaTypes.APPLICATION_YANG_DATA_XML,
343         MediaType.APPLICATION_JSON,
344         MediaType.APPLICATION_XML,
345         MediaType.TEXT_XML
346     })
347     public Response postData(@Encoded @PathParam("identifier") final String identifier,
348             final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
349         return postData(payload, uriInfo);
350     }
351
352     /**
353      * Create a data resource.
354      *
355      * @param payload new data
356      * @param uriInfo URI info
357      * @return {@link Response}
358      */
359     @POST
360     @Path("/data")
361     @Consumes({
362         MediaTypes.APPLICATION_YANG_DATA_JSON,
363         MediaTypes.APPLICATION_YANG_DATA_XML,
364         MediaType.APPLICATION_JSON,
365         MediaType.APPLICATION_XML,
366         MediaType.TEXT_XML
367     })
368     public Response postData(final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
369         requireNonNull(payload);
370         final var iid = payload.getInstanceIdentifierContext();
371         if (iid.getSchemaNode() instanceof ActionDefinition) {
372             return invokeAction(payload);
373         }
374
375         final var params = QueryParams.newWriteDataParams(uriInfo);
376         final var strategy = getRestconfStrategy(iid.getMountPoint());
377         final var path = iid.getInstanceIdentifier();
378         final var context = iid.getSchemaContext();
379         final var data = payload.getData();
380
381         PostDataTransactionUtil.postData(path, data, strategy, context, params);
382         return Response.created(resolveLocation(uriInfo, path, context, data)).build();
383     }
384
385     /**
386      * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
387      *
388      * @param uriInfo       uri info
389      * @param initialPath   data path
390      * @param schemaContext reference to {@link SchemaContext}
391      * @return {@link URI}
392      */
393     private static URI resolveLocation(final UriInfo uriInfo, final YangInstanceIdentifier initialPath,
394                                        final EffectiveModelContext schemaContext, final NormalizedNode data) {
395         YangInstanceIdentifier path = initialPath;
396         if (data instanceof MapNode mapData) {
397             final var children = mapData.body();
398             if (!children.isEmpty()) {
399                 path = path.node(children.iterator().next().name());
400             }
401         }
402
403         return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
404     }
405
406     /**
407      * Delete the target data resource.
408      *
409      * @param identifier path to target
410      * @param ar {@link AsyncResponse} which needs to be completed
411      */
412     @DELETE
413     @Path("/data/{identifier:.+}")
414     public void deleteData(@Encoded @PathParam("identifier") final String identifier,
415             @Suspended final AsyncResponse ar) {
416         final var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
417             databindProvider.currentContext().modelContext(), mountPointService);
418         final var strategy = getRestconfStrategy(instanceIdentifier.getMountPoint());
419
420         Futures.addCallback(strategy.delete(instanceIdentifier.getInstanceIdentifier()), new FutureCallback<>() {
421             @Override
422             public void onSuccess(final Empty result) {
423                 ar.resume(Response.noContent().build());
424             }
425
426             @Override
427             public void onFailure(final Throwable failure) {
428                 ar.resume(failure);
429             }
430         }, MoreExecutors.directExecutor());
431     }
432
433
434     /**
435      * Partially modify the target data resource, as defined in
436      * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-4.6.1">RFC8040, section 4.6.1</a>.
437      *
438      * @param identifier path to target
439      * @param payload data node for put to config DS
440      * @param ar {@link AsyncResponse} which needs to be completed
441      */
442     @PATCH
443     @Path("/data/{identifier:.+}")
444     @Consumes({
445         MediaTypes.APPLICATION_YANG_DATA_JSON,
446         MediaTypes.APPLICATION_YANG_DATA_XML,
447         MediaType.APPLICATION_JSON,
448         MediaType.APPLICATION_XML,
449         MediaType.TEXT_XML
450     })
451     public void plainPatchData(@Encoded @PathParam("identifier") final String identifier,
452             final NormalizedNodePayload payload, @Suspended final AsyncResponse ar) {
453         final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
454         final YangInstanceIdentifier path = iid.getInstanceIdentifier();
455         validInputData(iid.getSchemaNode() != null, payload);
456         validTopLevelNodeName(path, payload);
457         validateListKeysEqualityInPayloadAndUri(payload);
458         final var strategy = getRestconfStrategy(iid.getMountPoint());
459
460         Futures.addCallback(strategy.merge(path, payload.getData(), iid.getSchemaContext()), new FutureCallback<>() {
461             @Override
462             public void onSuccess(final Empty result) {
463                 ar.resume(Response.ok().build());
464             }
465
466             @Override
467             public void onFailure(final Throwable failure) {
468                 ar.resume(failure);
469             }
470         }, MoreExecutors.directExecutor());
471     }
472
473     /**
474      * Ordered list of edits that are applied to the target datastore by the server, as defined in
475      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
476      *
477      * @param identifier path to target
478      * @param body YANG Patch body
479      * @return {@link PatchStatusContext}
480      */
481     @PATCH
482     @Path("/data/{identifier:.+}")
483     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
484     @Produces({
485         MediaTypes.APPLICATION_YANG_DATA_JSON,
486         MediaTypes.APPLICATION_YANG_DATA_XML
487     })
488     public PatchStatusContext yangPatchDataXML(@Encoded @PathParam("identifier") final String identifier,
489             final InputStream body) {
490         try (var xmlBody = new XmlPatchBody(body)) {
491             return yangPatchData(identifier, xmlBody);
492         }
493     }
494
495     /**
496      * Ordered list of edits that are applied to the datastore by the server, as defined in
497      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
498      *
499      * @param body YANG Patch body
500      * @return {@link PatchStatusContext}
501      */
502     @PATCH
503     @Path("/data")
504     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
505     @Produces({
506         MediaTypes.APPLICATION_YANG_DATA_JSON,
507         MediaTypes.APPLICATION_YANG_DATA_XML
508     })
509     public PatchStatusContext yangPatchDataXML(final InputStream body) {
510         try (var xmlBody = new XmlPatchBody(body)) {
511             return yangPatchData(xmlBody);
512         }
513     }
514
515     /**
516      * Ordered list of edits that are applied to the target datastore by the server, as defined in
517      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
518      *
519      * @param identifier path to target
520      * @param body YANG Patch body
521      * @return {@link PatchStatusContext}
522      */
523     @PATCH
524     @Path("/data/{identifier:.+}")
525     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
526     @Produces({
527         MediaTypes.APPLICATION_YANG_DATA_JSON,
528         MediaTypes.APPLICATION_YANG_DATA_XML
529     })
530     public PatchStatusContext yangPatchDataJSON(@Encoded @PathParam("identifier") final String identifier,
531             final InputStream body) {
532         try (var jsonBody = new JsonPatchBody(body)) {
533             return yangPatchData(identifier, jsonBody);
534         }
535     }
536
537     /**
538      * Ordered list of edits that are applied to the datastore by the server, as defined in
539      * <a href="https://www.rfc-editor.org/rfc/rfc8072#section-2">RFC8072, section 2</a>.
540      *
541      * @param body YANG Patch body
542      * @return {@link PatchStatusContext}
543      */
544     @PATCH
545     @Path("/data")
546     @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
547     @Produces({
548         MediaTypes.APPLICATION_YANG_DATA_JSON,
549         MediaTypes.APPLICATION_YANG_DATA_XML
550     })
551     public PatchStatusContext yangPatchDataJSON(final InputStream body) {
552         try (var jsonBody = new JsonPatchBody(body)) {
553             return yangPatchData(jsonBody);
554         }
555     }
556
557     private PatchStatusContext yangPatchData(final @NonNull PatchBody body) {
558         return yangPatchData(InstanceIdentifierContext.ofLocalRoot(databindProvider.currentContext().modelContext()),
559             body);
560     }
561
562     private PatchStatusContext yangPatchData(final String identifier, final @NonNull PatchBody body) {
563         return yangPatchData(ParserIdentifier.toInstanceIdentifier(identifier,
564                 databindProvider.currentContext().modelContext(), mountPointService), body);
565     }
566
567     private PatchStatusContext yangPatchData(final @NonNull InstanceIdentifierContext targetResource,
568             final @NonNull PatchBody body) {
569         try {
570             return yangPatchData(targetResource, body.toPatchContext(targetResource));
571         } catch (IOException e) {
572             LOG.debug("Error parsing YANG Patch input", e);
573             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
574                     ErrorTag.MALFORMED_MESSAGE, e);
575         }
576     }
577
578     @VisibleForTesting
579     PatchStatusContext yangPatchData(final InstanceIdentifierContext targetResource, final PatchContext context) {
580         return PatchDataTransactionUtil.patchData(context, getRestconfStrategy(targetResource.getMountPoint()),
581             targetResource.getSchemaContext());
582     }
583
584     @VisibleForTesting
585     RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) {
586         if (mountPoint == null) {
587             return restconfStrategy;
588         }
589
590         return RestconfStrategy.forMountPoint(mountPoint).orElseThrow(() -> {
591             LOG.warn("Mount point {} does not expose a suitable access interface", mountPoint.getIdentifier());
592             return new RestconfDocumentedException("Could not find a supported access interface in mount point "
593                 + mountPoint.getIdentifier());
594         });
595     }
596
597     /**
598      * Invoke Action operation.
599      *
600      * @param payload {@link NormalizedNodePayload} - the body of the operation
601      * @return {@link NormalizedNodePayload} wrapped in {@link Response}
602      */
603     public Response invokeAction(final NormalizedNodePayload payload) {
604         final InstanceIdentifierContext context = payload.getInstanceIdentifierContext();
605         final YangInstanceIdentifier yangIIdContext = context.getInstanceIdentifier();
606         final NormalizedNode data = payload.getData();
607
608         if (yangIIdContext.isEmpty() && !Data.QNAME.equals(data.name().getNodeType())) {
609             throw new RestconfDocumentedException("Instance identifier need to contain at least one path argument",
610                 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
611         }
612
613         final DOMMountPoint mountPoint = context.getMountPoint();
614         final Absolute schemaPath = context.inference().toSchemaInferenceStack().toSchemaNodeIdentifier();
615         final DOMActionResult response;
616         if (mountPoint != null) {
617             response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, mountPoint);
618         } else {
619             response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, actionService);
620         }
621         final DOMActionResult result = checkActionResponse(response);
622
623         ContainerNode resultData = null;
624         if (result != null) {
625             resultData = result.getOutput().orElse(null);
626         }
627
628         if (resultData != null && resultData.isEmpty()) {
629             return Response.status(Status.NO_CONTENT).build();
630         }
631
632         return Response.status(Status.OK)
633             .entity(NormalizedNodePayload.ofNullable(context, resultData))
634             .build();
635     }
636
637     /**
638      * Invoking Action via mount point.
639      *
640      * @param mountPoint mount point
641      * @param data input data
642      * @param schemaPath schema path of data
643      * @return {@link DOMActionResult}
644      */
645     private static DOMActionResult invokeAction(final ContainerNode data,
646             final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
647         return invokeAction(data, schemaPath, yangIId, mountPoint.getService(DOMActionService.class)
648             .orElseThrow(() -> new RestconfDocumentedException("DomAction service is missing.")));
649     }
650
651     /**
652      * Invoke Action via ActionServiceHandler.
653      *
654      * @param data input data
655      * @param yangIId invocation context
656      * @param schemaPath schema path of data
657      * @param actionService action service to invoke action
658      * @return {@link DOMActionResult}
659      */
660     // FIXME: NETCONF-718: we should be returning a future here
661     private static DOMActionResult invokeAction(final ContainerNode data, final Absolute schemaPath,
662             final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
663         return RestconfInvokeOperationsServiceImpl.checkedGet(Futures.catching(actionService.invokeAction(
664             schemaPath, new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
665             DOMActionException.class,
666             cause -> new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
667                 ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))),
668             MoreExecutors.directExecutor()));
669     }
670
671     /**
672      * Check the validity of the result.
673      *
674      * @param response response of Action
675      * @return {@link DOMActionResult} result
676      */
677     private static DOMActionResult checkActionResponse(final DOMActionResult response) {
678         if (response == null) {
679             return null;
680         }
681
682         try {
683             if (response.getErrors().isEmpty()) {
684                 return response;
685             }
686             LOG.debug("InvokeAction Error Message {}", response.getErrors());
687             throw new RestconfDocumentedException("InvokeAction Error Message ", null, response.getErrors());
688         } catch (final CancellationException e) {
689             final String errMsg = "The Action Operation was cancelled while executing.";
690             LOG.debug("Cancel Execution: {}", errMsg, e);
691             throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e);
692         }
693     }
694
695     /**
696      * Valid input data based on presence of a schema node.
697      *
698      * @param haveSchemaNode true if there is an underlying schema node
699      * @param payload    input data
700      */
701     @VisibleForTesting
702     static void validInputData(final boolean haveSchemaNode, final NormalizedNodePayload payload) {
703         final boolean haveData = payload.getData() != null;
704         if (haveSchemaNode) {
705             if (!haveData) {
706                 throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL,
707                     ErrorTag.MALFORMED_MESSAGE);
708             }
709         } else if (haveData) {
710             throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
711         }
712     }
713
714     /**
715      * Valid top level node name.
716      *
717      * @param path    path of node
718      * @param payload data
719      */
720     @VisibleForTesting
721     static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodePayload payload) {
722         final QName dataNodeType = payload.getData().name().getNodeType();
723         if (path.isEmpty()) {
724             if (!Data.QNAME.equals(dataNodeType)) {
725                 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
726                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
727             }
728         } else {
729             final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
730             final String payloadName = dataNodeType.getLocalName();
731             if (!payloadName.equals(identifierName)) {
732                 throw new RestconfDocumentedException(
733                         "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
734                         ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
735             }
736         }
737     }
738
739     /**
740      * Validates whether keys in {@code payload} are equal to values of keys in
741      * {@code iiWithData} for list schema node.
742      *
743      * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
744      */
745     @VisibleForTesting
746     static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodePayload payload) {
747         final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext();
748         final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
749         final SchemaNode schemaNode = iiWithData.getSchemaNode();
750         final NormalizedNode data = payload.getData();
751         if (schemaNode instanceof ListSchemaNode listSchema) {
752             final var keyDefinitions = listSchema.getKeyDefinition();
753             if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) {
754                 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap();
755                 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
756             }
757         }
758     }
759
760     private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
761             final List<QName> keyDefinitions) {
762         final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues);
763         for (final QName keyDefinition : keyDefinitions) {
764             final Object uriKeyValue = RestconfDocumentedException.throwIfNull(
765                     mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
766                     "Missing key %s in URI.", keyDefinition);
767
768             final Object dataKeyValue = payload.name().getValue(keyDefinition);
769
770             if (!uriKeyValue.equals(dataKeyValue)) {
771                 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
772                         + "' specified in the URI doesn't match the value '" + dataKeyValue
773                         + "' specified in the message body. ";
774                 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
775             }
776         }
777     }
778 }