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;
23 import java.time.Clock;
24 import java.time.LocalDateTime;
25 import java.time.format.DateTimeFormatter;
26 import java.util.HashMap;
27 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.POST;
36 import javax.ws.rs.PUT;
37 import javax.ws.rs.Path;
38 import javax.ws.rs.PathParam;
39 import javax.ws.rs.Produces;
40 import javax.ws.rs.container.AsyncResponse;
41 import javax.ws.rs.container.Suspended;
42 import javax.ws.rs.core.Context;
43 import javax.ws.rs.core.MediaType;
44 import javax.ws.rs.core.Response;
45 import javax.ws.rs.core.Response.Status;
46 import javax.ws.rs.core.UriInfo;
47 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
48 import org.opendaylight.mdsal.dom.api.DOMActionException;
49 import org.opendaylight.mdsal.dom.api.DOMActionResult;
50 import org.opendaylight.mdsal.dom.api.DOMActionService;
51 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
52 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
53 import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteOperations;
54 import org.opendaylight.mdsal.dom.api.DOMMountPoint;
55 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
56 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
57 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
58 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
59 import org.opendaylight.restconf.common.patch.Patch;
60 import org.opendaylight.restconf.common.patch.PatchContext;
61 import org.opendaylight.restconf.common.patch.PatchStatusContext;
62 import org.opendaylight.restconf.nb.rfc8040.MediaTypes;
63 import org.opendaylight.restconf.nb.rfc8040.ReadDataParams;
64 import org.opendaylight.restconf.nb.rfc8040.WriteDataParams;
65 import org.opendaylight.restconf.nb.rfc8040.databind.DatabindProvider;
66 import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams;
67 import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload;
68 import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters;
69 import org.opendaylight.restconf.nb.rfc8040.monitoring.RestconfStateStreams;
70 import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService;
71 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
72 import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy;
73 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil;
74 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil;
75 import org.opendaylight.restconf.nb.rfc8040.rests.utils.PutDataTransactionUtil;
76 import org.opendaylight.restconf.nb.rfc8040.rests.utils.ReadDataTransactionUtil;
77 import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants;
78 import org.opendaylight.restconf.nb.rfc8040.streams.StreamsConfiguration;
79 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.ListenersBroker;
80 import org.opendaylight.restconf.nb.rfc8040.streams.listeners.NotificationListenerAdapter;
81 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
82 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.restconf.restconf.Data;
83 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
84 import org.opendaylight.yangtools.yang.common.Empty;
85 import org.opendaylight.yangtools.yang.common.ErrorTag;
86 import org.opendaylight.yangtools.yang.common.ErrorType;
87 import org.opendaylight.yangtools.yang.common.QName;
88 import org.opendaylight.yangtools.yang.common.Revision;
89 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
90 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
91 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
92 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
93 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
94 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
95 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
96 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
97 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
98 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
99 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
100 import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
101 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
102 import org.slf4j.Logger;
103 import org.slf4j.LoggerFactory;
106 * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
107 * and state data nodes.
110 public final class RestconfDataServiceImpl {
111 private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
112 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
114 private final RestconfStreamsSubscriptionService delegRestconfSubscrService;
115 private final DatabindProvider databindProvider;
116 private final MdsalRestconfStrategy restconfStrategy;
117 private final DOMMountPointService mountPointService;
118 private final SubscribeToStreamUtil streamUtils;
119 private final DOMActionService actionService;
120 private final DOMDataBroker dataBroker;
122 public RestconfDataServiceImpl(final DatabindProvider databindProvider,
123 final DOMDataBroker dataBroker, final DOMMountPointService mountPointService,
124 final RestconfStreamsSubscriptionService delegRestconfSubscrService,
125 final DOMActionService actionService, final StreamsConfiguration configuration) {
126 this.databindProvider = requireNonNull(databindProvider);
127 this.dataBroker = requireNonNull(dataBroker);
128 restconfStrategy = new MdsalRestconfStrategy(dataBroker);
129 this.mountPointService = requireNonNull(mountPointService);
130 this.delegRestconfSubscrService = requireNonNull(delegRestconfSubscrService);
131 this.actionService = requireNonNull(actionService);
132 streamUtils = configuration.useSSE() ? SubscribeToStreamUtil.serverSentEvents()
133 : SubscribeToStreamUtil.webSockets();
137 * Get target data resource from data root.
139 * @param uriInfo URI info
140 * @return {@link NormalizedNodePayload}
145 MediaTypes.APPLICATION_YANG_DATA_JSON,
146 MediaTypes.APPLICATION_YANG_DATA_XML,
147 MediaType.APPLICATION_JSON,
148 MediaType.APPLICATION_XML,
151 public Response readData(@Context final UriInfo uriInfo) {
152 return readData(null, uriInfo);
156 * Get target data resource.
158 * @param identifier path to target
159 * @param uriInfo URI info
160 * @return {@link NormalizedNodePayload}
163 @Path("/data/{identifier:.+}")
165 MediaTypes.APPLICATION_YANG_DATA_JSON,
166 MediaTypes.APPLICATION_YANG_DATA_XML,
167 MediaType.APPLICATION_JSON,
168 MediaType.APPLICATION_XML,
171 public Response readData(@Encoded @PathParam("identifier") final String identifier,
172 @Context final UriInfo uriInfo) {
173 final ReadDataParams readParams = QueryParams.newReadDataParams(uriInfo);
175 final EffectiveModelContext schemaContextRef = databindProvider.currentContext().modelContext();
177 final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
178 identifier, schemaContextRef, mountPointService);
179 final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
181 // FIXME: this looks quite crazy, why do we even have it?
182 if (mountPoint == null && identifier != null && identifier.contains(STREAMS_PATH)
183 && !identifier.contains(STREAM_PATH_PART)) {
184 createAllYangNotificationStreams(schemaContextRef, uriInfo);
187 final QueryParameters queryParams = QueryParams.newQueryParameters(readParams, instanceIdentifier);
188 final List<YangInstanceIdentifier> fieldPaths = queryParams.fieldPaths();
189 final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
190 final NormalizedNode node;
191 if (fieldPaths != null && !fieldPaths.isEmpty()) {
192 node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
193 strategy, readParams.withDefaults(), schemaContextRef, fieldPaths);
195 node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
196 strategy, readParams.withDefaults(), schemaContextRef);
199 // FIXME: this is utter craziness, refactor it properly!
200 if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART)
201 && identifier.contains(STREAM_LOCATION_PATH_PART)) {
202 final String value = (String) node.body();
203 final String streamName = value.substring(value.indexOf(NOTIFICATION_STREAM + '/'));
204 delegRestconfSubscrService.subscribeToStream(streamName, uriInfo);
207 throw new RestconfDocumentedException(
208 "Request could not be completed because the relevant data model content does not exist",
209 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
212 return switch (readParams.content()) {
213 case ALL, CONFIG -> {
214 final QName type = node.name().getNodeType();
215 yield Response.status(Status.OK)
216 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
217 .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) + "-"
218 + type.getLocalName() + '"')
219 .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
222 case NONCONFIG -> Response.status(Status.OK)
223 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
228 private void createAllYangNotificationStreams(final EffectiveModelContext schemaContext, final UriInfo uriInfo) {
229 final var transaction = dataBroker.newWriteOnlyTransaction();
231 for (var module : schemaContext.getModuleStatements().values()) {
232 final var moduleName = module.argument().getLocalName();
233 // Note: this handles only RFC6020 notifications
234 module.streamEffectiveSubstatements(NotificationEffectiveStatement.class).forEach(notification -> {
235 final var notifName = notification.argument();
237 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
238 createYangNotifiStream(moduleName, notifName, NotificationOutputType.XML));
239 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
240 createYangNotifiStream(moduleName, notifName, NotificationOutputType.JSON));
245 transaction.commit().get();
246 } catch (final InterruptedException | ExecutionException e) {
247 throw new RestconfDocumentedException("Problem while putting data to DS.", e);
251 private static NotificationListenerAdapter createYangNotifiStream(final String moduleName, final QName notifName,
252 final NotificationOutputType outputType) {
253 final var streamName = createNotificationStreamName(moduleName, notifName.getLocalName(), outputType);
254 final var listenersBroker = ListenersBroker.getInstance();
256 final var existing = listenersBroker.notificationListenerFor(streamName);
257 return existing != null ? existing
258 : listenersBroker.registerNotificationListener(Absolute.of(notifName), streamName, outputType);
261 private static String createNotificationStreamName(final String moduleName, final String notifName,
262 final NotificationOutputType outputType) {
263 final var sb = new StringBuilder()
264 .append(RestconfStreamsConstants.NOTIFICATION_STREAM)
265 .append('/').append(moduleName).append(':').append(notifName);
266 if (outputType != NotificationOutputType.XML) {
267 sb.append('/').append(outputType.getName());
269 return sb.toString();
272 private void writeNotificationStreamToDatastore(final EffectiveModelContext schemaContext,
273 final UriInfo uriInfo, final DOMDataTreeWriteOperations tx, final NotificationListenerAdapter listener) {
274 final URI uri = streamUtils.prepareUriByStreamName(uriInfo, listener.getStreamName());
275 final MapEntryNode mapToStreams = RestconfStateStreams.notificationStreamEntry(schemaContext,
276 listener.getSchemaPath().lastNodeIdentifier(), null, listener.getOutputType(), uri);
278 tx.merge(LogicalDatastoreType.OPERATIONAL,
279 RestconfStateStreams.restconfStateStreamPath(mapToStreams.name()), mapToStreams);
283 * Create or replace the target data resource.
285 * @param identifier path to target
286 * @param payload data node for put to config DS
287 * @return {@link Response}
290 @Path("/data/{identifier:.+}")
292 MediaTypes.APPLICATION_YANG_DATA_JSON,
293 MediaTypes.APPLICATION_YANG_DATA_XML,
294 MediaType.APPLICATION_JSON,
295 MediaType.APPLICATION_XML,
298 public Response putData(@Encoded @PathParam("identifier") final String identifier,
299 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
300 requireNonNull(payload);
302 final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo);
304 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
305 final YangInstanceIdentifier path = iid.getInstanceIdentifier();
307 validInputData(iid.getSchemaNode() != null, payload);
308 validTopLevelNodeName(path, payload);
309 validateListKeysEqualityInPayloadAndUri(payload);
311 final var strategy = getRestconfStrategy(iid.getMountPoint());
312 final var result = PutDataTransactionUtil.putData(path, payload.getData(), iid.getSchemaContext(), strategy,
314 return switch (result) {
315 // Note: no Location header, as it matches the request path
316 case CREATED -> Response.status(Status.CREATED).build();
317 case REPLACED -> Response.noContent().build();
322 * Create a data resource in target.
324 * @param identifier path to target
325 * @param payload new data
326 * @param uriInfo URI info
327 * @return {@link Response}
330 @Path("/data/{identifier:.+}")
332 MediaTypes.APPLICATION_YANG_DATA_JSON,
333 MediaTypes.APPLICATION_YANG_DATA_XML,
334 MediaType.APPLICATION_JSON,
335 MediaType.APPLICATION_XML,
338 public Response postData(@Encoded @PathParam("identifier") final String identifier,
339 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
340 return postData(payload, uriInfo);
344 * Create a data resource.
346 * @param payload new data
347 * @param uriInfo URI info
348 * @return {@link Response}
353 MediaTypes.APPLICATION_YANG_DATA_JSON,
354 MediaTypes.APPLICATION_YANG_DATA_XML,
355 MediaType.APPLICATION_JSON,
356 MediaType.APPLICATION_XML,
359 public Response postData(final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
360 requireNonNull(payload);
361 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
362 if (iid.getSchemaNode() instanceof ActionDefinition) {
363 return invokeAction(payload);
366 final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo);
367 final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint());
368 return PostDataTransactionUtil.postData(uriInfo, iid.getInstanceIdentifier(), payload.getData(), strategy,
369 iid.getSchemaContext(), params);
373 * Delete the target data resource.
375 * @param identifier path to target
376 * @param ar {@link AsyncResponse} which needs to be completed
379 @Path("/data/{identifier:.+}")
380 public void deleteData(@Encoded @PathParam("identifier") final String identifier,
381 @Suspended final AsyncResponse ar) {
382 final var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
383 databindProvider.currentContext().modelContext(), mountPointService);
384 final var strategy = getRestconfStrategy(instanceIdentifier.getMountPoint());
386 Futures.addCallback(strategy.delete(instanceIdentifier.getInstanceIdentifier()), new FutureCallback<>() {
388 public void onSuccess(final Empty result) {
389 ar.resume(Response.noContent().build());
393 public void onFailure(final Throwable failure) {
396 }, MoreExecutors.directExecutor());
400 * Ordered list of edits that are applied to the target datastore by the server.
402 * @param identifier path to target
403 * @param context edits
404 * @param uriInfo URI info
405 * @return {@link PatchStatusContext}
408 @Path("/data/{identifier:.+}")
410 MediaTypes.APPLICATION_YANG_PATCH_JSON,
411 MediaTypes.APPLICATION_YANG_PATCH_XML
414 MediaTypes.APPLICATION_YANG_DATA_JSON,
415 MediaTypes.APPLICATION_YANG_DATA_XML
417 public PatchStatusContext patchData(@Encoded @PathParam("identifier") final String identifier,
418 final PatchContext context, @Context final UriInfo uriInfo) {
419 return patchData(context, uriInfo);
423 * Ordered list of edits that are applied to the datastore by the server.
429 * @return {@link PatchStatusContext}
434 MediaTypes.APPLICATION_YANG_PATCH_JSON,
435 MediaTypes.APPLICATION_YANG_PATCH_XML
438 MediaTypes.APPLICATION_YANG_DATA_JSON,
439 MediaTypes.APPLICATION_YANG_DATA_XML
441 public PatchStatusContext patchData(final PatchContext context, @Context final UriInfo uriInfo) {
442 final InstanceIdentifierContext iid = RestconfDocumentedException.throwIfNull(context,
443 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, "No patch documented provided")
444 .getInstanceIdentifierContext();
445 final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint());
446 return PatchDataTransactionUtil.patchData(context, strategy, iid.getSchemaContext());
450 * Partially modify the target data resource.
452 * @param identifier path to target
453 * @param payload data node for put to config DS
454 * @param ar {@link AsyncResponse} which needs to be completed
457 @Path("/data/{identifier:.+}")
459 MediaTypes.APPLICATION_YANG_DATA_JSON,
460 MediaTypes.APPLICATION_YANG_DATA_XML,
461 MediaType.APPLICATION_JSON,
462 MediaType.APPLICATION_XML,
465 public void patchData(@Encoded @PathParam("identifier") final String identifier,
466 final NormalizedNodePayload payload, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
467 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
468 final YangInstanceIdentifier path = iid.getInstanceIdentifier();
469 validInputData(iid.getSchemaNode() != null, payload);
470 validTopLevelNodeName(path, payload);
471 validateListKeysEqualityInPayloadAndUri(payload);
472 final var strategy = getRestconfStrategy(iid.getMountPoint());
474 Futures.addCallback(strategy.merge(path, payload.getData(), iid.getSchemaContext()), new FutureCallback<>() {
476 public void onSuccess(final Empty result) {
477 ar.resume(Response.ok().build());
481 public void onFailure(final Throwable failure) {
484 }, MoreExecutors.directExecutor());
488 RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) {
489 if (mountPoint == null) {
490 return restconfStrategy;
493 return RestconfStrategy.forMountPoint(mountPoint).orElseThrow(() -> {
494 LOG.warn("Mount point {} does not expose a suitable access interface", mountPoint.getIdentifier());
495 return new RestconfDocumentedException("Could not find a supported access interface in mount point "
496 + mountPoint.getIdentifier());
501 * Invoke Action operation.
503 * @param payload {@link NormalizedNodePayload} - the body of the operation
504 * @return {@link NormalizedNodePayload} wrapped in {@link Response}
506 public Response invokeAction(final NormalizedNodePayload payload) {
507 final InstanceIdentifierContext context = payload.getInstanceIdentifierContext();
508 final YangInstanceIdentifier yangIIdContext = context.getInstanceIdentifier();
509 final NormalizedNode data = payload.getData();
511 if (yangIIdContext.isEmpty() && !Data.QNAME.equals(data.name().getNodeType())) {
512 throw new RestconfDocumentedException("Instance identifier need to contain at least one path argument",
513 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
516 final DOMMountPoint mountPoint = context.getMountPoint();
517 final Absolute schemaPath = context.inference().toSchemaInferenceStack().toSchemaNodeIdentifier();
518 final DOMActionResult response;
519 if (mountPoint != null) {
520 response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, mountPoint);
522 response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, actionService);
524 final DOMActionResult result = checkActionResponse(response);
526 ContainerNode resultData = null;
527 if (result != null) {
528 resultData = result.getOutput().orElse(null);
531 if (resultData != null && resultData.isEmpty()) {
532 return Response.status(Status.NO_CONTENT).build();
535 return Response.status(Status.OK)
536 .entity(NormalizedNodePayload.ofNullable(context, resultData))
541 * Invoking Action via mount point.
543 * @param mountPoint mount point
544 * @param data input data
545 * @param schemaPath schema path of data
546 * @return {@link DOMActionResult}
548 private static DOMActionResult invokeAction(final ContainerNode data,
549 final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
550 return invokeAction(data, schemaPath, yangIId, mountPoint.getService(DOMActionService.class)
551 .orElseThrow(() -> new RestconfDocumentedException("DomAction service is missing.")));
555 * Invoke Action via ActionServiceHandler.
557 * @param data input data
558 * @param yangIId invocation context
559 * @param schemaPath schema path of data
560 * @param actionService action service to invoke action
561 * @return {@link DOMActionResult}
563 // FIXME: NETCONF-718: we should be returning a future here
564 private static DOMActionResult invokeAction(final ContainerNode data, final Absolute schemaPath,
565 final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
566 return RestconfInvokeOperationsServiceImpl.checkedGet(Futures.catching(actionService.invokeAction(
567 schemaPath, new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
568 DOMActionException.class,
569 cause -> new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
570 ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))),
571 MoreExecutors.directExecutor()));
575 * Check the validity of the result.
577 * @param response response of Action
578 * @return {@link DOMActionResult} result
580 private static DOMActionResult checkActionResponse(final DOMActionResult response) {
581 if (response == null) {
586 if (response.getErrors().isEmpty()) {
589 LOG.debug("InvokeAction Error Message {}", response.getErrors());
590 throw new RestconfDocumentedException("InvokeAction Error Message ", null, response.getErrors());
591 } catch (final CancellationException e) {
592 final String errMsg = "The Action Operation was cancelled while executing.";
593 LOG.debug("Cancel Execution: {}", errMsg, e);
594 throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e);
599 * Valid input data based on presence of a schema node.
601 * @param haveSchemaNode true if there is an underlying schema node
602 * @param payload input data
605 static void validInputData(final boolean haveSchemaNode, final NormalizedNodePayload payload) {
606 final boolean haveData = payload.getData() != null;
607 if (haveSchemaNode) {
609 throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL,
610 ErrorTag.MALFORMED_MESSAGE);
612 } else if (haveData) {
613 throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
618 * Valid top level node name.
620 * @param path path of node
621 * @param payload data
624 static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodePayload payload) {
625 final QName dataNodeType = payload.getData().name().getNodeType();
626 if (path.isEmpty()) {
627 if (!Data.QNAME.equals(dataNodeType)) {
628 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
629 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
632 final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
633 final String payloadName = dataNodeType.getLocalName();
634 if (!payloadName.equals(identifierName)) {
635 throw new RestconfDocumentedException(
636 "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
637 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
643 * Validates whether keys in {@code payload} are equal to values of keys in
644 * {@code iiWithData} for list schema node.
646 * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
649 static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodePayload payload) {
650 final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext();
651 final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
652 final SchemaNode schemaNode = iiWithData.getSchemaNode();
653 final NormalizedNode data = payload.getData();
654 if (schemaNode instanceof ListSchemaNode listSchema) {
655 final var keyDefinitions = listSchema.getKeyDefinition();
656 if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) {
657 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap();
658 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
663 private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
664 final List<QName> keyDefinitions) {
665 final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues);
666 for (final QName keyDefinition : keyDefinitions) {
667 final Object uriKeyValue = RestconfDocumentedException.throwIfNull(
668 mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
669 "Missing key %s in URI.", keyDefinition);
671 final Object dataKeyValue = payload.name().getValue(keyDefinition);
673 if (!uriKeyValue.equals(dataKeyValue)) {
674 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
675 + "' specified in the URI doesn't match the value '" + dataKeyValue
676 + "' specified in the message body. ";
677 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);