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.IdentifierCodec;
82 import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier;
83 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.rev170126.restconf.restconf.Data;
84 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType;
85 import org.opendaylight.yangtools.yang.common.Empty;
86 import org.opendaylight.yangtools.yang.common.ErrorTag;
87 import org.opendaylight.yangtools.yang.common.ErrorType;
88 import org.opendaylight.yangtools.yang.common.QName;
89 import org.opendaylight.yangtools.yang.common.Revision;
90 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
91 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
92 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
93 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
94 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
95 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
96 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
97 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
98 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
99 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
100 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
101 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
102 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
103 import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
104 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute;
105 import org.slf4j.Logger;
106 import org.slf4j.LoggerFactory;
109 * The "{+restconf}/data" subtree represents the datastore resource type, which is a collection of configuration data
110 * and state data nodes.
113 public final class RestconfDataServiceImpl {
114 private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class);
115 private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss");
117 private final RestconfStreamsSubscriptionService delegRestconfSubscrService;
118 private final DatabindProvider databindProvider;
119 private final MdsalRestconfStrategy restconfStrategy;
120 private final DOMMountPointService mountPointService;
121 private final SubscribeToStreamUtil streamUtils;
122 private final DOMActionService actionService;
123 private final DOMDataBroker dataBroker;
125 public RestconfDataServiceImpl(final DatabindProvider databindProvider,
126 final DOMDataBroker dataBroker, final DOMMountPointService mountPointService,
127 final RestconfStreamsSubscriptionService delegRestconfSubscrService,
128 final DOMActionService actionService, final StreamsConfiguration configuration) {
129 this.databindProvider = requireNonNull(databindProvider);
130 this.dataBroker = requireNonNull(dataBroker);
131 restconfStrategy = new MdsalRestconfStrategy(dataBroker);
132 this.mountPointService = requireNonNull(mountPointService);
133 this.delegRestconfSubscrService = requireNonNull(delegRestconfSubscrService);
134 this.actionService = requireNonNull(actionService);
135 streamUtils = configuration.useSSE() ? SubscribeToStreamUtil.serverSentEvents()
136 : SubscribeToStreamUtil.webSockets();
140 * Get target data resource from data root.
142 * @param uriInfo URI info
143 * @return {@link NormalizedNodePayload}
148 MediaTypes.APPLICATION_YANG_DATA_JSON,
149 MediaTypes.APPLICATION_YANG_DATA_XML,
150 MediaType.APPLICATION_JSON,
151 MediaType.APPLICATION_XML,
154 public Response readData(@Context final UriInfo uriInfo) {
155 return readData(null, uriInfo);
159 * Get target data resource.
161 * @param identifier path to target
162 * @param uriInfo URI info
163 * @return {@link NormalizedNodePayload}
166 @Path("/data/{identifier:.+}")
168 MediaTypes.APPLICATION_YANG_DATA_JSON,
169 MediaTypes.APPLICATION_YANG_DATA_XML,
170 MediaType.APPLICATION_JSON,
171 MediaType.APPLICATION_XML,
174 public Response readData(@Encoded @PathParam("identifier") final String identifier,
175 @Context final UriInfo uriInfo) {
176 final ReadDataParams readParams = QueryParams.newReadDataParams(uriInfo);
178 final EffectiveModelContext schemaContextRef = databindProvider.currentContext().modelContext();
180 final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier(
181 identifier, schemaContextRef, mountPointService);
182 final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint();
184 // FIXME: this looks quite crazy, why do we even have it?
185 if (mountPoint == null && identifier != null && identifier.contains(STREAMS_PATH)
186 && !identifier.contains(STREAM_PATH_PART)) {
187 createAllYangNotificationStreams(schemaContextRef, uriInfo);
190 final QueryParameters queryParams = QueryParams.newQueryParameters(readParams, instanceIdentifier);
191 final List<YangInstanceIdentifier> fieldPaths = queryParams.fieldPaths();
192 final RestconfStrategy strategy = getRestconfStrategy(mountPoint);
193 final NormalizedNode node;
194 if (fieldPaths != null && !fieldPaths.isEmpty()) {
195 node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
196 strategy, readParams.withDefaults(), schemaContextRef, fieldPaths);
198 node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(),
199 strategy, readParams.withDefaults(), schemaContextRef);
202 // FIXME: this is utter craziness, refactor it properly!
203 if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART)
204 && identifier.contains(STREAM_LOCATION_PATH_PART)) {
205 final String value = (String) node.body();
206 final String streamName = value.substring(value.indexOf(NOTIFICATION_STREAM + '/'));
207 delegRestconfSubscrService.subscribeToStream(streamName, uriInfo);
210 throw new RestconfDocumentedException(
211 "Request could not be completed because the relevant data model content does not exist",
212 ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
215 return switch (readParams.content()) {
216 case ALL, CONFIG -> {
217 final QName type = node.name().getNodeType();
218 yield Response.status(Status.OK)
219 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
220 .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) + "-"
221 + type.getLocalName() + '"')
222 .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC())))
225 case NONCONFIG -> Response.status(Status.OK)
226 .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams))
231 private void createAllYangNotificationStreams(final EffectiveModelContext schemaContext, final UriInfo uriInfo) {
232 final var transaction = dataBroker.newWriteOnlyTransaction();
234 for (var module : schemaContext.getModuleStatements().values()) {
235 final var moduleName = module.argument().getLocalName();
236 // Note: this handles only RFC6020 notifications
237 module.streamEffectiveSubstatements(NotificationEffectiveStatement.class).forEach(notification -> {
238 final var notifName = notification.argument();
240 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
241 createYangNotifiStream(moduleName, notifName, NotificationOutputType.XML));
242 writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction,
243 createYangNotifiStream(moduleName, notifName, NotificationOutputType.JSON));
248 transaction.commit().get();
249 } catch (final InterruptedException | ExecutionException e) {
250 throw new RestconfDocumentedException("Problem while putting data to DS.", e);
254 private static NotificationListenerAdapter createYangNotifiStream(final String moduleName, final QName notifName,
255 final NotificationOutputType outputType) {
256 final var streamName = createNotificationStreamName(moduleName, notifName.getLocalName(), outputType);
257 final var listenersBroker = ListenersBroker.getInstance();
259 final var existing = listenersBroker.notificationListenerFor(streamName);
260 return existing != null ? existing
261 : listenersBroker.registerNotificationListener(Absolute.of(notifName), streamName, outputType);
264 private static String createNotificationStreamName(final String moduleName, final String notifName,
265 final NotificationOutputType outputType) {
266 final var sb = new StringBuilder()
267 .append(RestconfStreamsConstants.NOTIFICATION_STREAM)
268 .append('/').append(moduleName).append(':').append(notifName);
269 if (outputType != NotificationOutputType.XML) {
270 sb.append('/').append(outputType.getName());
272 return sb.toString();
275 private void writeNotificationStreamToDatastore(final EffectiveModelContext schemaContext,
276 final UriInfo uriInfo, final DOMDataTreeWriteOperations tx, final NotificationListenerAdapter listener) {
277 final URI uri = streamUtils.prepareUriByStreamName(uriInfo, listener.getStreamName());
278 final MapEntryNode mapToStreams = RestconfStateStreams.notificationStreamEntry(schemaContext,
279 listener.getSchemaPath().lastNodeIdentifier(), null, listener.getOutputType(), uri);
281 tx.merge(LogicalDatastoreType.OPERATIONAL,
282 RestconfStateStreams.restconfStateStreamPath(mapToStreams.name()), mapToStreams);
286 * Create or replace the target data resource.
288 * @param identifier path to target
289 * @param payload data node for put to config DS
290 * @return {@link Response}
293 @Path("/data/{identifier:.+}")
295 MediaTypes.APPLICATION_YANG_DATA_JSON,
296 MediaTypes.APPLICATION_YANG_DATA_XML,
297 MediaType.APPLICATION_JSON,
298 MediaType.APPLICATION_XML,
301 public Response putData(@Encoded @PathParam("identifier") final String identifier,
302 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
303 requireNonNull(payload);
305 final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo);
307 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
308 final YangInstanceIdentifier path = iid.getInstanceIdentifier();
310 validInputData(iid.getSchemaNode() != null, payload);
311 validTopLevelNodeName(path, payload);
312 validateListKeysEqualityInPayloadAndUri(payload);
314 final var strategy = getRestconfStrategy(iid.getMountPoint());
315 final var result = PutDataTransactionUtil.putData(path, payload.getData(), iid.getSchemaContext(), strategy,
317 return switch (result) {
318 // Note: no Location header, as it matches the request path
319 case CREATED -> Response.status(Status.CREATED).build();
320 case REPLACED -> Response.noContent().build();
325 * Create a data resource in target.
327 * @param identifier path to target
328 * @param payload new data
329 * @param uriInfo URI info
330 * @return {@link Response}
333 @Path("/data/{identifier:.+}")
335 MediaTypes.APPLICATION_YANG_DATA_JSON,
336 MediaTypes.APPLICATION_YANG_DATA_XML,
337 MediaType.APPLICATION_JSON,
338 MediaType.APPLICATION_XML,
341 public Response postData(@Encoded @PathParam("identifier") final String identifier,
342 final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
343 return postData(payload, uriInfo);
347 * Create a data resource.
349 * @param payload new data
350 * @param uriInfo URI info
351 * @return {@link Response}
356 MediaTypes.APPLICATION_YANG_DATA_JSON,
357 MediaTypes.APPLICATION_YANG_DATA_XML,
358 MediaType.APPLICATION_JSON,
359 MediaType.APPLICATION_XML,
362 public Response postData(final NormalizedNodePayload payload, @Context final UriInfo uriInfo) {
363 requireNonNull(payload);
364 final var iid = payload.getInstanceIdentifierContext();
365 if (iid.getSchemaNode() instanceof ActionDefinition) {
366 return invokeAction(payload);
369 final var params = QueryParams.newWriteDataParams(uriInfo);
370 final var strategy = getRestconfStrategy(iid.getMountPoint());
371 final var path = iid.getInstanceIdentifier();
372 final var context = iid.getSchemaContext();
373 final var data = payload.getData();
375 PostDataTransactionUtil.postData(path, data, strategy, context, params);
376 return Response.created(resolveLocation(uriInfo, path, context, data)).build();
380 * Get location from {@link YangInstanceIdentifier} and {@link UriInfo}.
382 * @param uriInfo uri info
383 * @param initialPath data path
384 * @param schemaContext reference to {@link SchemaContext}
385 * @return {@link URI}
387 private static URI resolveLocation(final UriInfo uriInfo, final YangInstanceIdentifier initialPath,
388 final EffectiveModelContext schemaContext, final NormalizedNode data) {
389 YangInstanceIdentifier path = initialPath;
390 if (data instanceof MapNode mapData) {
391 final var children = mapData.body();
392 if (!children.isEmpty()) {
393 path = path.node(children.iterator().next().name());
397 return uriInfo.getBaseUriBuilder().path("data").path(IdentifierCodec.serialize(path, schemaContext)).build();
401 * Delete the target data resource.
403 * @param identifier path to target
404 * @param ar {@link AsyncResponse} which needs to be completed
407 @Path("/data/{identifier:.+}")
408 public void deleteData(@Encoded @PathParam("identifier") final String identifier,
409 @Suspended final AsyncResponse ar) {
410 final var instanceIdentifier = ParserIdentifier.toInstanceIdentifier(identifier,
411 databindProvider.currentContext().modelContext(), mountPointService);
412 final var strategy = getRestconfStrategy(instanceIdentifier.getMountPoint());
414 Futures.addCallback(strategy.delete(instanceIdentifier.getInstanceIdentifier()), new FutureCallback<>() {
416 public void onSuccess(final Empty result) {
417 ar.resume(Response.noContent().build());
421 public void onFailure(final Throwable failure) {
424 }, MoreExecutors.directExecutor());
428 * Ordered list of edits that are applied to the target datastore by the server.
430 * @param identifier path to target
431 * @param context edits
432 * @param uriInfo URI info
433 * @return {@link PatchStatusContext}
436 @Path("/data/{identifier:.+}")
438 MediaTypes.APPLICATION_YANG_PATCH_JSON,
439 MediaTypes.APPLICATION_YANG_PATCH_XML
442 MediaTypes.APPLICATION_YANG_DATA_JSON,
443 MediaTypes.APPLICATION_YANG_DATA_XML
445 public PatchStatusContext patchData(@Encoded @PathParam("identifier") final String identifier,
446 final PatchContext context, @Context final UriInfo uriInfo) {
447 return patchData(context, uriInfo);
451 * Ordered list of edits that are applied to the datastore by the server.
457 * @return {@link PatchStatusContext}
462 MediaTypes.APPLICATION_YANG_PATCH_JSON,
463 MediaTypes.APPLICATION_YANG_PATCH_XML
466 MediaTypes.APPLICATION_YANG_DATA_JSON,
467 MediaTypes.APPLICATION_YANG_DATA_XML
469 public PatchStatusContext patchData(final PatchContext context, @Context final UriInfo uriInfo) {
470 final InstanceIdentifierContext iid = RestconfDocumentedException.throwIfNull(context,
471 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, "No patch documented provided")
472 .getInstanceIdentifierContext();
473 final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint());
474 return PatchDataTransactionUtil.patchData(context, strategy, iid.getSchemaContext());
478 * Partially modify the target data resource.
480 * @param identifier path to target
481 * @param payload data node for put to config DS
482 * @param ar {@link AsyncResponse} which needs to be completed
485 @Path("/data/{identifier:.+}")
487 MediaTypes.APPLICATION_YANG_DATA_JSON,
488 MediaTypes.APPLICATION_YANG_DATA_XML,
489 MediaType.APPLICATION_JSON,
490 MediaType.APPLICATION_XML,
493 public void patchData(@Encoded @PathParam("identifier") final String identifier,
494 final NormalizedNodePayload payload, @Context final UriInfo uriInfo, @Suspended final AsyncResponse ar) {
495 final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext();
496 final YangInstanceIdentifier path = iid.getInstanceIdentifier();
497 validInputData(iid.getSchemaNode() != null, payload);
498 validTopLevelNodeName(path, payload);
499 validateListKeysEqualityInPayloadAndUri(payload);
500 final var strategy = getRestconfStrategy(iid.getMountPoint());
502 Futures.addCallback(strategy.merge(path, payload.getData(), iid.getSchemaContext()), new FutureCallback<>() {
504 public void onSuccess(final Empty result) {
505 ar.resume(Response.ok().build());
509 public void onFailure(final Throwable failure) {
512 }, MoreExecutors.directExecutor());
516 RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) {
517 if (mountPoint == null) {
518 return restconfStrategy;
521 return RestconfStrategy.forMountPoint(mountPoint).orElseThrow(() -> {
522 LOG.warn("Mount point {} does not expose a suitable access interface", mountPoint.getIdentifier());
523 return new RestconfDocumentedException("Could not find a supported access interface in mount point "
524 + mountPoint.getIdentifier());
529 * Invoke Action operation.
531 * @param payload {@link NormalizedNodePayload} - the body of the operation
532 * @return {@link NormalizedNodePayload} wrapped in {@link Response}
534 public Response invokeAction(final NormalizedNodePayload payload) {
535 final InstanceIdentifierContext context = payload.getInstanceIdentifierContext();
536 final YangInstanceIdentifier yangIIdContext = context.getInstanceIdentifier();
537 final NormalizedNode data = payload.getData();
539 if (yangIIdContext.isEmpty() && !Data.QNAME.equals(data.name().getNodeType())) {
540 throw new RestconfDocumentedException("Instance identifier need to contain at least one path argument",
541 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
544 final DOMMountPoint mountPoint = context.getMountPoint();
545 final Absolute schemaPath = context.inference().toSchemaInferenceStack().toSchemaNodeIdentifier();
546 final DOMActionResult response;
547 if (mountPoint != null) {
548 response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, mountPoint);
550 response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, actionService);
552 final DOMActionResult result = checkActionResponse(response);
554 ContainerNode resultData = null;
555 if (result != null) {
556 resultData = result.getOutput().orElse(null);
559 if (resultData != null && resultData.isEmpty()) {
560 return Response.status(Status.NO_CONTENT).build();
563 return Response.status(Status.OK)
564 .entity(NormalizedNodePayload.ofNullable(context, resultData))
569 * Invoking Action via mount point.
571 * @param mountPoint mount point
572 * @param data input data
573 * @param schemaPath schema path of data
574 * @return {@link DOMActionResult}
576 private static DOMActionResult invokeAction(final ContainerNode data,
577 final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) {
578 return invokeAction(data, schemaPath, yangIId, mountPoint.getService(DOMActionService.class)
579 .orElseThrow(() -> new RestconfDocumentedException("DomAction service is missing.")));
583 * Invoke Action via ActionServiceHandler.
585 * @param data input data
586 * @param yangIId invocation context
587 * @param schemaPath schema path of data
588 * @param actionService action service to invoke action
589 * @return {@link DOMActionResult}
591 // FIXME: NETCONF-718: we should be returning a future here
592 private static DOMActionResult invokeAction(final ContainerNode data, final Absolute schemaPath,
593 final YangInstanceIdentifier yangIId, final DOMActionService actionService) {
594 return RestconfInvokeOperationsServiceImpl.checkedGet(Futures.catching(actionService.invokeAction(
595 schemaPath, new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data),
596 DOMActionException.class,
597 cause -> new SimpleDOMActionResult(List.of(RpcResultBuilder.newError(
598 ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))),
599 MoreExecutors.directExecutor()));
603 * Check the validity of the result.
605 * @param response response of Action
606 * @return {@link DOMActionResult} result
608 private static DOMActionResult checkActionResponse(final DOMActionResult response) {
609 if (response == null) {
614 if (response.getErrors().isEmpty()) {
617 LOG.debug("InvokeAction Error Message {}", response.getErrors());
618 throw new RestconfDocumentedException("InvokeAction Error Message ", null, response.getErrors());
619 } catch (final CancellationException e) {
620 final String errMsg = "The Action Operation was cancelled while executing.";
621 LOG.debug("Cancel Execution: {}", errMsg, e);
622 throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e);
627 * Valid input data based on presence of a schema node.
629 * @param haveSchemaNode true if there is an underlying schema node
630 * @param payload input data
633 static void validInputData(final boolean haveSchemaNode, final NormalizedNodePayload payload) {
634 final boolean haveData = payload.getData() != null;
635 if (haveSchemaNode) {
637 throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL,
638 ErrorTag.MALFORMED_MESSAGE);
640 } else if (haveData) {
641 throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
646 * Valid top level node name.
648 * @param path path of node
649 * @param payload data
652 static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodePayload payload) {
653 final QName dataNodeType = payload.getData().name().getNodeType();
654 if (path.isEmpty()) {
655 if (!Data.QNAME.equals(dataNodeType)) {
656 throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument",
657 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
660 final String identifierName = path.getLastPathArgument().getNodeType().getLocalName();
661 final String payloadName = dataNodeType.getLocalName();
662 if (!payloadName.equals(identifierName)) {
663 throw new RestconfDocumentedException(
664 "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")",
665 ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE);
671 * Validates whether keys in {@code payload} are equal to values of keys in
672 * {@code iiWithData} for list schema node.
674 * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal
677 static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodePayload payload) {
678 final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext();
679 final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument();
680 final SchemaNode schemaNode = iiWithData.getSchemaNode();
681 final NormalizedNode data = payload.getData();
682 if (schemaNode instanceof ListSchemaNode listSchema) {
683 final var keyDefinitions = listSchema.getKeyDefinition();
684 if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) {
685 final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap();
686 isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions);
691 private static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload,
692 final List<QName> keyDefinitions) {
693 final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues);
694 for (final QName keyDefinition : keyDefinitions) {
695 final Object uriKeyValue = RestconfDocumentedException.throwIfNull(
696 mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING,
697 "Missing key %s in URI.", keyDefinition);
699 final Object dataKeyValue = payload.name().getValue(keyDefinition);
701 if (!uriKeyValue.equals(dataKeyValue)) {
702 final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName()
703 + "' specified in the URI doesn't match the value '" + dataKeyValue
704 + "' specified in the message body. ";
705 throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);