2 * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved.
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
8 package org.opendaylight.restconf.nb.rfc8040.rests.services.impl;
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;
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;
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;
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;
115 * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
116 * and state data nodes.
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");
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;
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();
146 * Get target data resource from data root.
148 * @param uriInfo URI info
149 * @return {@link NormalizedNodePayload}
154 MediaTypes.APPLICATION_YANG_DATA_JSON,
155 MediaTypes.APPLICATION_YANG_DATA_XML,
156 MediaType.APPLICATION_JSON,
157 MediaType.APPLICATION_XML,
160 public Response readData(@Context final UriInfo uriInfo) {
161 return readData(null, uriInfo);
165 * Get target data resource.
167 * @param identifier path to target
168 * @param uriInfo URI info
169 * @return {@link NormalizedNodePayload}
172 @Path("/data/{identifier:.+}")
174 MediaTypes.APPLICATION_YANG_DATA_JSON,
175 MediaTypes.APPLICATION_YANG_DATA_XML,
176 MediaType.APPLICATION_JSON,
177 MediaType.APPLICATION_XML,
180 public Response readData(@Encoded @PathParam("identifier") final String identifier,
181 @Context final UriInfo uriInfo) {
182 final ReadDataParams readParams = QueryParams.newReadDataParams(uriInfo);
184 final EffectiveModelContext schemaContextRef = databindProvider.currentContext().modelContext();
186 final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
187 identifier, schemaContextRef, mountPointService);
188 final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
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);
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);
204 node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
205 strategy, readParams.withDefaults(), schemaContextRef);
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);
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);
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())))
231 case NONCONFIG -> Response.status(Status.OK)
232 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
237 private void createAllYangNotificationStreams(final EffectiveModelContext schemaContext, final UriInfo uriInfo) {
238 final var transaction = dataBroker.newWriteOnlyTransaction();
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();
246 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
247 createYangNotifiStream(moduleName, notifName, NotificationOutputType.XML));
248 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
249 createYangNotifiStream(moduleName, notifName, NotificationOutputType.JSON));
254 transaction.commit().get();
255 } catch (final InterruptedException | ExecutionException e) {
256 throw new RestconfDocumentedException("Problem while putting data to DS.", e);
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();
265 final var existing = listenersBroker.notificationListenerFor(streamName);
266 return existing != null ? existing
267 : listenersBroker.registerNotificationListener(Absolute.of(notifName), streamName, outputType);
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());
278 return sb.toString();
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);
287 tx.merge(LogicalDatastoreType.OPERATIONAL,
288 RestconfStateStreams.restconfStateStreamPath(mapToStreams.name()), mapToStreams);
292 * Create or replace the target data resource.
294 * @param identifier path to target
295 * @param payload data node for put to config DS
296 * @return {@link Response}
299 @Path("/data/{identifier:.+}")
301 MediaTypes.APPLICATION_YANG_DATA_JSON,
302 MediaTypes.APPLICATION_YANG_DATA_XML,
303 MediaType.APPLICATION_JSON,
304 MediaType.APPLICATION_XML,
307 public Response putData(@Encoded @PathParam("identifier") final String identifier,
308 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
309 requireNonNull(payload);
311 final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo);
313 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
314 final YangInstanceIdentifier path = iid.getInstanceIdentifier();
316 validInputData(iid.getSchemaNode() != null, payload);
317 validTopLevelNodeName(path, payload);
318 validateListKeysEqualityInPayloadAndUri(payload);
320 final var strategy = getRestconfStrategy(iid.getMountPoint());
321 final var result = PutDataTransactionUtil.putData(path, payload.getData(), iid.getSchemaContext(), strategy,
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();
331 * Create a data resource in target.
333 * @param identifier path to target
334 * @param payload new data
335 * @param uriInfo URI info
336 * @return {@link Response}
339 @Path("/data/{identifier:.+}")
341 MediaTypes.APPLICATION_YANG_DATA_JSON,
342 MediaTypes.APPLICATION_YANG_DATA_XML,
343 MediaType.APPLICATION_JSON,
344 MediaType.APPLICATION_XML,
347 public Response postData(@Encoded @PathParam("identifier") final String identifier,
348 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
349 return postData(payload, uriInfo);
353 * Create a data resource.
355 * @param payload new data
356 * @param uriInfo URI info
357 * @return {@link Response}
362 MediaTypes.APPLICATION_YANG_DATA_JSON,
363 MediaTypes.APPLICATION_YANG_DATA_XML,
364 MediaType.APPLICATION_JSON,
365 MediaType.APPLICATION_XML,
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);
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();
381 PostDataTransactionUtil.postData(path, data, strategy, context, params);
382 return Response.created(resolveLocation(uriInfo, path, context, data)).build();
386 * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
388 * @param uriInfo uri info
389 * @param initialPath data path
390 * @param schemaContext reference to {@link SchemaContext}
391 * @return {@link URI}
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());
403 return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
407 * Delete the target data resource.
409 * @param identifier path to target
410 * @param ar {@link AsyncResponse} which needs to be completed
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());
420 Futures.addCallback(strategy.delete(instanceIdentifier.getInstanceIdentifier()), new FutureCallback<>() {
422 public void onSuccess(final Empty result) {
423 ar.resume(Response.noContent().build());
427 public void onFailure(final Throwable failure) {
430 }, MoreExecutors.directExecutor());
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>.
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
443 @Path("/data/{identifier:.+}")
445 MediaTypes.APPLICATION_YANG_DATA_JSON,
446 MediaTypes.APPLICATION_YANG_DATA_XML,
447 MediaType.APPLICATION_JSON,
448 MediaType.APPLICATION_XML,
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());
460 Futures.addCallback(strategy.merge(path, payload.getData(), iid.getSchemaContext()), new FutureCallback<>() {
462 public void onSuccess(final Empty result) {
463 ar.resume(Response.ok().build());
467 public void onFailure(final Throwable failure) {
470 }, MoreExecutors.directExecutor());
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>.
477 * @param identifier path to target
478 * @param body YANG Patch body
479 * @return {@link PatchStatusContext}
482 @Path("/data/{identifier:.+}")
483 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
485 MediaTypes.APPLICATION_YANG_DATA_JSON,
486 MediaTypes.APPLICATION_YANG_DATA_XML
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);
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>.
499 * @param body YANG Patch body
500 * @return {@link PatchStatusContext}
504 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_XML)
506 MediaTypes.APPLICATION_YANG_DATA_JSON,
507 MediaTypes.APPLICATION_YANG_DATA_XML
509 public PatchStatusContext yangPatchDataXML(final InputStream body) {
510 try (var xmlBody = new XmlPatchBody(body)) {
511 return yangPatchData(xmlBody);
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>.
519 * @param identifier path to target
520 * @param body YANG Patch body
521 * @return {@link PatchStatusContext}
524 @Path("/data/{identifier:.+}")
525 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
527 MediaTypes.APPLICATION_YANG_DATA_JSON,
528 MediaTypes.APPLICATION_YANG_DATA_XML
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);
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>.
541 * @param body YANG Patch body
542 * @return {@link PatchStatusContext}
546 @Consumes(MediaTypes.APPLICATION_YANG_PATCH_JSON)
548 MediaTypes.APPLICATION_YANG_DATA_JSON,
549 MediaTypes.APPLICATION_YANG_DATA_XML
551 public PatchStatusContext yangPatchDataJSON(final InputStream body) {
552 try (var jsonBody = new JsonPatchBody(body)) {
553 return yangPatchData(jsonBody);
557 private PatchStatusContext yangPatchData(final @NonNull PatchBody body) {
558 return yangPatchData(InstanceIdentifierContext.ofLocalRoot(databindProvider.currentContext().modelContext()),
562 private PatchStatusContext yangPatchData(final String identifier, final @NonNull PatchBody body) {
563 return yangPatchData(ParserIdentifier.toInstanceIdentifier(identifier,
564 databindProvider.currentContext().modelContext(), mountPointService), body);
567 private PatchStatusContext yangPatchData(final @NonNull InstanceIdentifierContext targetResource,
568 final @NonNull PatchBody body) {
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);
579 PatchStatusContext yangPatchData(final InstanceIdentifierContext targetResource, final PatchContext context) {
580 return PatchDataTransactionUtil.patchData(context, getRestconfStrategy(targetResource.getMountPoint()),
581 targetResource.getSchemaContext());
585 RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) {
586 if (mountPoint == null) {
587 return restconfStrategy;
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());
598 * Invoke Action operation.
600 * @param payload {@link NormalizedNodePayload} - the body of the operation
601 * @return {@link NormalizedNodePayload} wrapped in {@link Response}
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();
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);
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);
619 response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, actionService);
621 final DOMActionResult result = checkActionResponse(response);
623 ContainerNode resultData = null;
624 if (result != null) {
625 resultData = result.getOutput().orElse(null);
628 if (resultData != null && resultData.isEmpty()) {
629 return Response.status(Status.NO_CONTENT).build();
632 return Response.status(Status.OK)
633 .entity(NormalizedNodePayload.ofNullable(context, resultData))
638 * Invoking Action via mount point.
640 * @param mountPoint mount point
641 * @param data input data
642 * @param schemaPath schema path of data
643 * @return {@link DOMActionResult}
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.")));
652 * Invoke Action via ActionServiceHandler.
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}
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()));
672 * Check the validity of the result.
674 * @param response response of Action
675 * @return {@link DOMActionResult} result
677 private static DOMActionResult checkActionResponse(final DOMActionResult response) {
678 if (response == null) {
683 if (response.getErrors().isEmpty()) {
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);
696 * Valid input data based on presence of a schema node.
698 * @param haveSchemaNode true if there is an underlying schema node
699 * @param payload input data
702 static void validInputData(final boolean haveSchemaNode, final NormalizedNodePayload payload) {
703 final boolean haveData = payload.getData() != null;
704 if (haveSchemaNode) {
706 throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL,
707 ErrorTag.MALFORMED_MESSAGE);
709 } else if (haveData) {
710 throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
715 * Valid top level node name.
717 * @param path path of node
718 * @param payload data
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);
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);
740 * Validates whether keys in {@code payload} are equal to values of keys in
741 * {@code iiWithData} for list schema node.
743 * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
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);
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);
768 final Object dataKeyValue = payload.name().getValue(keyDefinition);
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);