Refactor NormalizedNodePayload
[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.monitoring.RestconfStateStreams;
82 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
83 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
84 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
85 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants;
86 import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
87 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.ListenersBroker;
88 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.NotificationListenerAdapter;
89 import org.opendaylight.restconf.nb.rfc8040.utils.parser.IdentifierCodec;
90 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
91 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
92 import org.opendaylight.yangtools.yang.common.Empty;
93 import org.opendaylight.yangtools.yang.common.ErrorTag;
94 import org.opendaylight.yangtools.yang.common.ErrorType;
95 import org.opendaylight.yangtools.yang.common.QName;
96 import org.opendaylight.yangtools.yang.common.Revision;
97 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
98 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
99 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
100 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
101 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
102 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
103 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
104 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
105 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
106 import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
107 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
108 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
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 var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier, schemaContextRef,
186             mountPointService);
187         final var 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 var queryParams = QueryParams.newQueryParameters(readParams, instanceIdentifier);
196         final var fieldPaths = queryParams.fieldPaths();
197         final var 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(new NormalizedNodePayload(instanceIdentifier.inference(), 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(new NormalizedNodePayload(instanceIdentifier.inference(), 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.inference(), instanceIdentifier.getInstanceIdentifier(), jsonBody,
428                 uriInfo, instanceIdentifier.getMountPoint());
429         }
430     }
431
432     /**
433      * Create a top-level data resource.
434      *
435      * @param body data node for put to config DS
436      * @param uriInfo URI info
437      * @return {@link Response}
438      */
439     @POST
440     @Path("/data")
441     @Consumes({
442         MediaTypes.APPLICATION_YANG_DATA_XML,
443         MediaType.APPLICATION_XML,
444         MediaType.TEXT_XML
445     })
446     public Response postDataXML(final InputStream body, @Context final UriInfo uriInfo) {
447         try (var xmlBody = new XmlChildBody(body)) {
448             return postData(xmlBody, uriInfo);
449         }
450     }
451
452     /**
453      * Create a data resource in target.
454      *
455      * @param identifier path to target
456      * @param body data node for put to config DS
457      * @param uriInfo URI info
458      * @return {@link Response}
459      */
460     @POST
461     @Path("/data/{identifier:.+}")
462     @Consumes({
463         MediaTypes.APPLICATION_YANG_DATA_XML,
464         MediaType.APPLICATION_XML,
465         MediaType.TEXT_XML
466     })
467     public Response postDataXML(@Encoded @PathParam("identifier") final String identifier, final InputStream body,
468             @Context final UriInfo uriInfo) {
469         final var iid = ParserIdentifier.toInstanceIdentifier(identifier,
470             databindProvider.currentContext().modelContext(), mountPointService);
471         if (iid.getSchemaNode() instanceof ActionDefinition) {
472             try (var xmlBody = new XmlOperationInputBody(body)) {
473                 return invokeAction(iid, xmlBody);
474             }
475         }
476
477         try (var xmlBody = new XmlChildBody(body)) {
478             return postData(iid.inference(), iid.getInstanceIdentifier(), xmlBody, uriInfo, iid.getMountPoint());
479         }
480     }
481
482     private Response postData(final ChildBody body, final UriInfo uriInfo) {
483         return postData(Inference.ofDataTreePath(databindProvider.currentContext().modelContext()),
484             YangInstanceIdentifier.of(), body, uriInfo, null);
485     }
486
487     private Response postData(final Inference inference, final YangInstanceIdentifier parentPath, final ChildBody body,
488             final UriInfo uriInfo, final @Nullable DOMMountPoint mountPoint) {
489         final var insert = QueryParams.parseInsert(uriInfo);
490         final var strategy = getRestconfStrategy(mountPoint);
491         final var context = inference.getEffectiveModelContext();
492         var path = parentPath;
493         final var payload = body.toPayload(path, 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         final var context = databindProvider.currentContext().modelContext();
757         return yangPatchData(context, parsePatchBody(context, YangInstanceIdentifier.of(), body), null);
758     }
759
760     private PatchStatusContext yangPatchData(final String identifier, final @NonNull PatchBody body) {
761         final var iid = ParserIdentifier.toInstanceIdentifier(requireNonNull(identifier),
762             databindProvider.currentContext().modelContext(), mountPointService);
763         final var context = iid.getSchemaContext();
764         return yangPatchData(context, parsePatchBody(context, iid.getInstanceIdentifier(), body), iid.getMountPoint());
765     }
766
767
768     @VisibleForTesting
769     @NonNull PatchStatusContext yangPatchData(final @NonNull EffectiveModelContext context,
770             final @NonNull PatchContext patch, final @Nullable DOMMountPoint mountPoint) {
771         return getRestconfStrategy(mountPoint).patchData(patch, context);
772     }
773
774     private static @NonNull PatchContext parsePatchBody(final @NonNull EffectiveModelContext context,
775             final @NonNull YangInstanceIdentifier urlPath, final @NonNull PatchBody body) {
776         try {
777             return body.toPatchContext(context, urlPath);
778         } catch (IOException e) {
779             LOG.debug("Error parsing YANG Patch input", e);
780             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
781                     ErrorTag.MALFORMED_MESSAGE, e);
782         }
783     }
784
785     @VisibleForTesting
786     RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) {
787         if (mountPoint == null) {
788             return restconfStrategy;
789         }
790
791         return RestconfStrategy.forMountPoint(mountPoint).orElseThrow(() -> {
792             LOG.warn("Mount point {} does not expose a suitable access interface", mountPoint.getIdentifier());
793             return new RestconfDocumentedException("Could not find a supported access interface in mount point "
794                 + mountPoint.getIdentifier());
795         });
796     }
797
798     /**
799      * Invoke Action operation.
800      *
801      * @param payload {@link NormalizedNodePayload} - the body of the operation
802      * @return {@link NormalizedNodePayload} wrapped in {@link Response}
803      */
804     private Response invokeAction(final InstanceIdentifierContext context, final OperationInputBody body) {
805         final var yangIIdContext = context.getInstanceIdentifier();
806         final ContainerNode input;
807         try {
808             input = body.toContainerNode(context.inference());
809         } catch (IOException e) {
810             LOG.debug("Error reading input", e);
811             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
812                     ErrorTag.MALFORMED_MESSAGE, e);
813         }
814
815         final var mountPoint = context.getMountPoint();
816         final var inference = context.inference();
817         final var schemaPath = inference.toSchemaInferenceStack().toSchemaNodeIdentifier();
818         final var response = mountPoint != null ? invokeAction(input, schemaPath, yangIIdContext, mountPoint)
819             : invokeAction(input, schemaPath, yangIIdContext, actionService);
820         final var result = checkActionResponse(response);
821
822         final var resultData = result != null ? result.getOutput().orElse(null) : null;
823         if (resultData == null || resultData.isEmpty()) {
824             return Response.status(Status.NO_CONTENT).build();
825         }
826         return Response.status(Status.OK).entity(new NormalizedNodePayload(inference, resultData)).build();
827     }
828
829     /**
830      * Invoking Action via mount point.
831      *
832      * @param mountPoint mount point
833      * @param data input data
834      * @param schemaPath schema path of data
835      * @return {@link DOMActionResult}
836      */
837     private static DOMActionResult invokeAction(final ContainerNode data,
838             final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
839         return invokeAction(data, schemaPath, yangIId, mountPoint.getService(DOMActionService.class)
840             .orElseThrow(() -> new RestconfDocumentedException("DomAction service is missing.")));
841     }
842
843     /**
844      * Invoke Action via ActionServiceHandler.
845      *
846      * @param data input data
847      * @param yangIId invocation context
848      * @param schemaPath schema path of data
849      * @param actionService action service to invoke action
850      * @return {@link DOMActionResult}
851      */
852     // FIXME: NETCONF-718: we should be returning a future here
853     private static DOMActionResult invokeAction(final ContainerNode data, final Absolute schemaPath,
854             final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
855         return RestconfInvokeOperationsServiceImpl.checkedGet(Futures.catching(actionService.invokeAction(
856             schemaPath, new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
857             DOMActionException.class,
858             cause -> new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
859                 ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))),
860             MoreExecutors.directExecutor()));
861     }
862
863     /**
864      * Check the validity of the result.
865      *
866      * @param response response of Action
867      * @return {@link DOMActionResult} result
868      */
869     private static DOMActionResult checkActionResponse(final DOMActionResult response) {
870         if (response == null) {
871             return null;
872         }
873
874         try {
875             if (response.getErrors().isEmpty()) {
876                 return response;
877             }
878             LOG.debug("InvokeAction Error Message {}", response.getErrors());
879             throw new RestconfDocumentedException("InvokeAction Error Message ", null, response.getErrors());
880         } catch (final CancellationException e) {
881             final String errMsg = "The Action Operation was cancelled while executing.";
882             LOG.debug("Cancel Execution: {}", errMsg, e);
883             throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e);
884         }
885     }
886 }