X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=blobdiff_plain;f=restconf%2Frestconf-nb-rfc8040%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Frestconf%2Fnb%2Frfc8040%2Frests%2Fservices%2Fimpl%2FRestconfDataServiceImpl.java;h=8257b6394e0cc986552805cadb9b8211ff8e2582;hb=365a4ee24f4760a4e09b72d4959f618479dccf2a;hp=08f129478a3e7e45117da886e4ff6150b897997c;hpb=125d61fea0c349f61a3f41287bb464396deb3380;p=netconf.git diff --git a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java index 08f129478a..8257b6394e 100644 --- a/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java +++ b/restconf/restconf-nb-rfc8040/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/services/impl/RestconfDataServiceImpl.java @@ -7,52 +7,86 @@ */ package org.opendaylight.restconf.nb.rfc8040.rests.services.impl; -import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.CREATE_NOTIFICATION_STREAM; +import static java.util.Objects.requireNonNull; +import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.NOTIFICATION_STREAM; +import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAMS_PATH; import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_ACCESS_PATH_PART; import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_LOCATION_PATH_PART; import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_PATH; +import static org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfStreamsConstants.STREAM_PATH_PART; -import com.google.common.base.Optional; -import com.google.common.base.Preconditions; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.MoreExecutors; +import java.net.URI; import java.time.Clock; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; +import java.util.HashMap; import java.util.List; -import java.util.Map.Entry; -import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import javax.ws.rs.Path; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import javax.ws.rs.core.UriInfo; -import org.opendaylight.controller.md.sal.dom.api.DOMDataBroker; -import org.opendaylight.controller.md.sal.dom.api.DOMMountPoint; -import org.opendaylight.controller.md.sal.dom.api.DOMTransactionChain; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.dom.api.DOMActionException; +import org.opendaylight.mdsal.dom.api.DOMActionResult; +import org.opendaylight.mdsal.dom.api.DOMActionService; +import org.opendaylight.mdsal.dom.api.DOMDataBroker; +import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier; +import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteOperations; +import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction; +import org.opendaylight.mdsal.dom.api.DOMMountPoint; +import org.opendaylight.mdsal.dom.api.DOMMountPointService; +import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult; import org.opendaylight.restconf.common.context.InstanceIdentifierContext; -import org.opendaylight.restconf.common.context.NormalizedNodeContext; -import org.opendaylight.restconf.common.context.WriterParameters; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; -import org.opendaylight.restconf.common.errors.RestconfError; import org.opendaylight.restconf.common.patch.PatchContext; import org.opendaylight.restconf.common.patch.PatchStatusContext; -import org.opendaylight.restconf.nb.rfc8040.RestConnectorProvider; -import org.opendaylight.restconf.nb.rfc8040.handlers.DOMMountPointServiceHandler; +import org.opendaylight.restconf.nb.rfc8040.ReadDataParams; +import org.opendaylight.restconf.nb.rfc8040.Rfc8040; +import org.opendaylight.restconf.nb.rfc8040.WriteDataParams; +import org.opendaylight.restconf.nb.rfc8040.databind.jaxrs.QueryParams; import org.opendaylight.restconf.nb.rfc8040.handlers.SchemaContextHandler; -import org.opendaylight.restconf.nb.rfc8040.handlers.TransactionChainHandler; -import org.opendaylight.restconf.nb.rfc8040.references.SchemaContextRef; +import org.opendaylight.restconf.nb.rfc8040.legacy.NormalizedNodePayload; +import org.opendaylight.restconf.nb.rfc8040.legacy.QueryParameters; import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfDataService; import org.opendaylight.restconf.nb.rfc8040.rests.services.api.RestconfStreamsSubscriptionService; -import org.opendaylight.restconf.nb.rfc8040.rests.transactions.TransactionVarsWrapper; +import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy; +import org.opendaylight.restconf.nb.rfc8040.rests.transactions.RestconfStrategy; import org.opendaylight.restconf.nb.rfc8040.rests.utils.DeleteDataTransactionUtil; import org.opendaylight.restconf.nb.rfc8040.rests.utils.PatchDataTransactionUtil; +import org.opendaylight.restconf.nb.rfc8040.rests.utils.PlainPatchDataTransactionUtil; import org.opendaylight.restconf.nb.rfc8040.rests.utils.PostDataTransactionUtil; import org.opendaylight.restconf.nb.rfc8040.rests.utils.PutDataTransactionUtil; import org.opendaylight.restconf.nb.rfc8040.rests.utils.ReadDataTransactionUtil; -import org.opendaylight.restconf.nb.rfc8040.rests.utils.RestconfDataServiceConstant; -import org.opendaylight.restconf.nb.rfc8040.utils.RestconfConstants; +import org.opendaylight.restconf.nb.rfc8040.streams.Configuration; +import org.opendaylight.restconf.nb.rfc8040.streams.listeners.NotificationListenerAdapter; +import org.opendaylight.restconf.nb.rfc8040.utils.mapping.RestconfMappingNodeUtil; import org.opendaylight.restconf.nb.rfc8040.utils.parser.ParserIdentifier; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.common.Revision; +import org.opendaylight.yangtools.yang.common.RpcResultBuilder; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.model.api.ActionDefinition; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.NotificationDefinition; +import org.opendaylight.yangtools.yang.model.api.SchemaContext; import org.opendaylight.yangtools.yang.model.api.SchemaNode; +import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,37 +95,30 @@ import org.slf4j.LoggerFactory; */ @Path("/") public class RestconfDataServiceImpl implements RestconfDataService { - private static final Logger LOG = LoggerFactory.getLogger(RestconfDataServiceImpl.class); private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MMM-dd HH:mm:ss"); - - private SchemaContextHandler schemaContextHandler; - private TransactionChainHandler transactionChainHandler; - private DOMMountPointServiceHandler mountPointServiceHandler; + private static final QName NETCONF_BASE_QNAME = SchemaContext.NAME; private final RestconfStreamsSubscriptionService delegRestconfSubscrService; + private final SchemaContextHandler schemaContextHandler; + private final MdsalRestconfStrategy restconfStrategy; + private final DOMMountPointService mountPointService; + private final SubscribeToStreamUtil streamUtils; + private final DOMActionService actionService; + private final DOMDataBroker dataBroker; public RestconfDataServiceImpl(final SchemaContextHandler schemaContextHandler, - final TransactionChainHandler transactionChainHandler, - final DOMMountPointServiceHandler mountPointServiceHandler, - final RestconfStreamsSubscriptionService delegRestconfSubscrService) { - this.schemaContextHandler = schemaContextHandler; - this.transactionChainHandler = transactionChainHandler; - this.mountPointServiceHandler = mountPointServiceHandler; - this.delegRestconfSubscrService = delegRestconfSubscrService; - } - - @Override - public synchronized void updateHandlers(final Object... handlers) { - for (final Object object : handlers) { - if (object instanceof SchemaContextHandler) { - schemaContextHandler = (SchemaContextHandler) object; - } else if (object instanceof DOMMountPointServiceHandler) { - mountPointServiceHandler = (DOMMountPointServiceHandler) object; - } else if (object instanceof TransactionChainHandler) { - transactionChainHandler = (TransactionChainHandler) object; - } - } + final DOMDataBroker dataBroker, final DOMMountPointService mountPointService, + final RestconfStreamsSubscriptionService delegRestconfSubscrService, + final DOMActionService actionService, final Configuration configuration) { + this.schemaContextHandler = requireNonNull(schemaContextHandler); + this.dataBroker = requireNonNull(dataBroker); + restconfStrategy = new MdsalRestconfStrategy(dataBroker); + this.mountPointService = requireNonNull(mountPointService); + this.delegRestconfSubscrService = requireNonNull(delegRestconfSubscrService); + this.actionService = requireNonNull(actionService); + streamUtils = configuration.isUseSSE() ? SubscribeToStreamUtil.serverSentEvents() + : SubscribeToStreamUtil.webSockets(); } @Override @@ -101,222 +128,131 @@ public class RestconfDataServiceImpl implements RestconfDataService { @Override public Response readData(final String identifier, final UriInfo uriInfo) { - final SchemaContextRef schemaContextRef = new SchemaContextRef(this.schemaContextHandler.get()); - final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier( - identifier, schemaContextRef.get(), Optional.of(this.mountPointServiceHandler.get())); - - boolean withDefaUsed = false; - String withDefa = null; - - for (final Entry> entry : uriInfo.getQueryParameters().entrySet()) { - switch (entry.getKey()) { - case "with-defaults": - if (!withDefaUsed) { - withDefaUsed = true; - withDefa = entry.getValue().iterator().next(); - } else { - throw new RestconfDocumentedException("With-defaults parameter can be used only once."); - } - break; - default: - LOG.info("Unknown key : {}.", entry.getKey()); - break; - } - } - boolean tagged = false; - if (withDefaUsed) { - if ("report-all-tagged".equals(withDefa)) { - tagged = true; - withDefa = null; - } - if ("report-all".equals(withDefa)) { - withDefa = null; - } - } - - final WriterParameters parameters = ReadDataTransactionUtil.parseUriParameters( - instanceIdentifier, uriInfo, tagged); + final ReadDataParams readParams = QueryParams.newReadDataParams(uriInfo); + final EffectiveModelContext schemaContextRef = schemaContextHandler.get(); + final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier( + identifier, schemaContextRef, Optional.of(mountPointService)); final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint(); - final DOMTransactionChain transactionChain; - if (mountPoint == null) { - transactionChain = this.transactionChainHandler.get(); + + // FIXME: this looks quite crazy, why do we even have it? + if (mountPoint == null && identifier != null && identifier.contains(STREAMS_PATH) + && !identifier.contains(STREAM_PATH_PART)) { + createAllYangNotificationStreams(schemaContextRef, uriInfo); + } + + final QueryParameters queryParams = QueryParams.newQueryParameters(readParams, instanceIdentifier); + final List fieldPaths = queryParams.fieldPaths(); + final RestconfStrategy strategy = getRestconfStrategy(mountPoint); + final NormalizedNode node; + if (fieldPaths != null && !fieldPaths.isEmpty()) { + node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(), + strategy, readParams.withDefaults(), schemaContextRef, fieldPaths); } else { - transactionChain = transactionChainOfMountPoint(mountPoint); + node = ReadDataTransactionUtil.readData(readParams.content(), instanceIdentifier.getInstanceIdentifier(), + strategy, readParams.withDefaults(), schemaContextRef); } - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper( - instanceIdentifier, mountPoint, transactionChain); - final NormalizedNode node = - ReadDataTransactionUtil.readData(identifier, parameters.getContent(), transactionNode, withDefa, - schemaContextRef, uriInfo); - if (identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART) + // FIXME: this is utter craziness, refactor it properly! + if (identifier != null && identifier.contains(STREAM_PATH) && identifier.contains(STREAM_ACCESS_PATH_PART) && identifier.contains(STREAM_LOCATION_PATH_PART)) { - final String value = (String) node.getValue(); - final String streamName = value.substring( - value.indexOf(CREATE_NOTIFICATION_STREAM.toString() + RestconfConstants.SLASH), - value.length()); - this.delegRestconfSubscrService.subscribeToStream(streamName, uriInfo); + final String value = (String) node.body(); + final String streamName = value.substring(value.indexOf(NOTIFICATION_STREAM + '/')); + delegRestconfSubscrService.subscribeToStream(streamName, uriInfo); } if (node == null) { throw new RestconfDocumentedException( "Request could not be completed because the relevant data model content does not exist", - RestconfError.ErrorType.PROTOCOL, - RestconfError.ErrorTag.DATA_MISSING); + ErrorType.PROTOCOL, ErrorTag.DATA_MISSING); } - if (parameters.getContent().equals(RestconfDataServiceConstant.ReadData.ALL) - || parameters.getContent().equals(RestconfDataServiceConstant.ReadData.CONFIG)) { - final QName type = node.getNodeType(); - return Response.status(200) - .entity(new NormalizedNodeContext(instanceIdentifier, node, parameters)) + switch (readParams.content()) { + case ALL: + case CONFIG: + final QName type = node.getIdentifier().getNodeType(); + return Response.status(Status.OK) + .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams)) .header("ETag", '"' + type.getModule().getRevision().map(Revision::toString).orElse(null) - + type.getLocalName() + '"') + + "-" + type.getLocalName() + '"') .header("Last-Modified", FORMATTER.format(LocalDateTime.now(Clock.systemUTC()))) .build(); + default: + return Response.status(Status.OK) + .entity(NormalizedNodePayload.ofReadData(instanceIdentifier, node, queryParams)) + .build(); } - - return Response.status(200).entity(new NormalizedNodeContext(instanceIdentifier, node, parameters)).build(); } - @Override - public Response putData(final String identifier, final NormalizedNodeContext payload, final UriInfo uriInfo) { - Preconditions.checkNotNull(payload); - - boolean insertUsed = false; - boolean pointUsed = false; - String insert = null; - String point = null; - - for (final Entry> entry : uriInfo.getQueryParameters().entrySet()) { - switch (entry.getKey()) { - case "insert": - if (!insertUsed) { - insertUsed = true; - insert = entry.getValue().iterator().next(); - } else { - throw new RestconfDocumentedException("Insert parameter can be used only once."); - } - break; - case "point": - if (!pointUsed) { - pointUsed = true; - point = entry.getValue().iterator().next(); - } else { - throw new RestconfDocumentedException("Point parameter can be used only once."); - } - break; - default: - throw new RestconfDocumentedException("Bad parameter for post: " + entry.getKey()); - } + private void createAllYangNotificationStreams(final EffectiveModelContext schemaContext, final UriInfo uriInfo) { + final DOMDataTreeWriteTransaction transaction = dataBroker.newWriteOnlyTransaction(); + for (final NotificationDefinition notificationDefinition : schemaContext.getNotifications()) { + writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction, + CreateStreamUtil.createYangNotifiStream(notificationDefinition, schemaContext, + NotificationOutputType.XML)); + writeNotificationStreamToDatastore(schemaContext, uriInfo, transaction, + CreateStreamUtil.createYangNotifiStream(notificationDefinition, schemaContext, + NotificationOutputType.JSON)); } + try { + transaction.commit().get(); + } catch (final InterruptedException | ExecutionException e) { + throw new RestconfDocumentedException("Problem while putting data to DS.", e); + } + } - checkQueryParams(insertUsed, pointUsed, insert); + private void writeNotificationStreamToDatastore(final EffectiveModelContext schemaContext, + final UriInfo uriInfo, final DOMDataTreeWriteOperations tx, final NotificationListenerAdapter listener) { + final URI uri = streamUtils.prepareUriByStreamName(uriInfo, listener.getStreamName()); + final MapEntryNode mapToStreams = RestconfMappingNodeUtil.mapYangNotificationStreamByIetfRestconfMonitoring( + listener.getSchemaPath().lastNodeIdentifier(), schemaContext.getNotifications(), null, + listener.getOutputType(), uri); - final InstanceIdentifierContext iid = payload - .getInstanceIdentifierContext(); + tx.merge(LogicalDatastoreType.OPERATIONAL, + Rfc8040.restconfStateStreamPath(mapToStreams.getIdentifier()), mapToStreams); + } - PutDataTransactionUtil.validInputData(iid.getSchemaNode(), payload); - PutDataTransactionUtil.validTopLevelNodeName(iid.getInstanceIdentifier(), payload); - PutDataTransactionUtil.validateListKeysEqualityInPayloadAndUri(payload); + @Override + public Response putData(final String identifier, final NormalizedNodePayload payload, final UriInfo uriInfo) { + requireNonNull(payload); - final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint(); - final DOMTransactionChain transactionChain; - final SchemaContextRef ref; - if (mountPoint == null) { - transactionChain = this.transactionChainHandler.get(); - ref = new SchemaContextRef(this.schemaContextHandler.get()); - } else { - transactionChain = transactionChainOfMountPoint(mountPoint); - ref = new SchemaContextRef(mountPoint.getSchemaContext()); - } + final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo); - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper( - payload.getInstanceIdentifierContext(), mountPoint, transactionChain); - return PutDataTransactionUtil.putData(payload, ref, transactionNode, insert, point); - } + final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext(); - private static void checkQueryParams(final boolean insertUsed, final boolean pointUsed, final String insert) { - if (pointUsed && !insertUsed) { - throw new RestconfDocumentedException("Point parameter can't be used without Insert parameter."); - } - if (pointUsed && (insert.equals("first") || insert.equals("last"))) { - throw new RestconfDocumentedException( - "Point parameter can be used only with 'after' or 'before' values of Insert parameter."); - } + validInputData(iid.getSchemaNode() != null, payload); + validTopLevelNodeName(iid.getInstanceIdentifier(), payload); + validateListKeysEqualityInPayloadAndUri(payload); + + final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint()); + return PutDataTransactionUtil.putData(payload, iid.getSchemaContext(), strategy, params); } @Override - public Response postData(final String identifier, final NormalizedNodeContext payload, final UriInfo uriInfo) { + public Response postData(final String identifier, final NormalizedNodePayload payload, final UriInfo uriInfo) { return postData(payload, uriInfo); } @Override - public Response postData(final NormalizedNodeContext payload, final UriInfo uriInfo) { - Preconditions.checkNotNull(payload); - - boolean insertUsed = false; - boolean pointUsed = false; - String insert = null; - String point = null; - - for (final Entry> entry : uriInfo.getQueryParameters().entrySet()) { - switch (entry.getKey()) { - case "insert": - if (!insertUsed) { - insertUsed = true; - insert = entry.getValue().iterator().next(); - } else { - throw new RestconfDocumentedException("Insert parameter can be used only once."); - } - break; - case "point": - if (!pointUsed) { - pointUsed = true; - point = entry.getValue().iterator().next(); - } else { - throw new RestconfDocumentedException("Point parameter can be used only once."); - } - break; - default: - throw new RestconfDocumentedException("Bad parameter for post: " + entry.getKey()); - } + public Response postData(final NormalizedNodePayload payload, final UriInfo uriInfo) { + requireNonNull(payload); + final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext(); + if (iid.getSchemaNode() instanceof ActionDefinition) { + return invokeAction(payload); } - checkQueryParams(insertUsed, pointUsed, insert); - - final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint(); - final DOMTransactionChain transactionChain; - final SchemaContextRef ref; - if (mountPoint == null) { - transactionChain = this.transactionChainHandler.get(); - ref = new SchemaContextRef(this.schemaContextHandler.get()); - } else { - transactionChain = transactionChainOfMountPoint(mountPoint); - ref = new SchemaContextRef(mountPoint.getSchemaContext()); - } - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper( - payload.getInstanceIdentifierContext(), mountPoint, transactionChain); - return PostDataTransactionUtil.postData(uriInfo, payload, transactionNode, ref, insert, point); + final WriteDataParams params = QueryParams.newWriteDataParams(uriInfo); + final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint()); + return PostDataTransactionUtil.postData(uriInfo, payload, strategy, iid.getSchemaContext(), params); } @Override public Response deleteData(final String identifier) { - final SchemaContextRef schemaContextRef = new SchemaContextRef(this.schemaContextHandler.get()); - final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier( - identifier, schemaContextRef.get(), Optional.of(this.mountPointServiceHandler.get())); + final InstanceIdentifierContext instanceIdentifier = ParserIdentifier.toInstanceIdentifier( + identifier, schemaContextHandler.get(), Optional.of(mountPointService)); final DOMMountPoint mountPoint = instanceIdentifier.getMountPoint(); - final DOMTransactionChain transactionChain; - if (mountPoint == null) { - transactionChain = this.transactionChainHandler.get(); - } else { - transactionChain = transactionChainOfMountPoint(mountPoint); - } - - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper(instanceIdentifier, mountPoint, - transactionChain); - return DeleteDataTransactionUtil.deleteData(transactionNode); + final RestconfStrategy strategy = getRestconfStrategy(mountPoint); + return DeleteDataTransactionUtil.deleteData(strategy, instanceIdentifier.getInstanceIdentifier()); } @Override @@ -326,39 +262,219 @@ public class RestconfDataServiceImpl implements RestconfDataService { @Override public PatchStatusContext patchData(final PatchContext context, final UriInfo uriInfo) { - Preconditions.checkNotNull(context); - final DOMMountPoint mountPoint = context.getInstanceIdentifierContext().getMountPoint(); + final InstanceIdentifierContext iid = RestconfDocumentedException.throwIfNull(context, + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE, "No patch documented provided") + .getInstanceIdentifierContext(); + final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint()); + return PatchDataTransactionUtil.patchData(context, strategy, iid.getSchemaContext()); + } + + @Override + public Response patchData(final String identifier, final NormalizedNodePayload payload, final UriInfo uriInfo) { + requireNonNull(payload); + + final InstanceIdentifierContext iid = payload.getInstanceIdentifierContext(); + validInputData(iid.getSchemaNode() != null, payload); + validTopLevelNodeName(iid.getInstanceIdentifier(), payload); + validateListKeysEqualityInPayloadAndUri(payload); + + final RestconfStrategy strategy = getRestconfStrategy(iid.getMountPoint()); + return PlainPatchDataTransactionUtil.patchData(payload, strategy, iid.getSchemaContext()); + } - final DOMTransactionChain transactionChain; - final SchemaContextRef ref; + // FIXME: why is this synchronized? + public synchronized RestconfStrategy getRestconfStrategy(final DOMMountPoint mountPoint) { if (mountPoint == null) { - transactionChain = this.transactionChainHandler.get(); - ref = new SchemaContextRef(this.schemaContextHandler.get()); + return restconfStrategy; + } + + return RestconfStrategy.forMountPoint(mountPoint).orElseThrow(() -> { + LOG.warn("Mount point {} does not expose a suitable access interface", mountPoint.getIdentifier()); + return new RestconfDocumentedException("Could not find a supported access interface in mount point " + + mountPoint.getIdentifier()); + }); + } + + /** + * Invoke Action operation. + * + * @param payload {@link NormalizedNodePayload} - the body of the operation + * @return {@link NormalizedNodePayload} wrapped in {@link Response} + */ + public Response invokeAction(final NormalizedNodePayload payload) { + final InstanceIdentifierContext context = payload.getInstanceIdentifierContext(); + final YangInstanceIdentifier yangIIdContext = context.getInstanceIdentifier(); + final NormalizedNode data = payload.getData(); + + if (yangIIdContext.isEmpty() && !NETCONF_BASE_QNAME.equals(data.getIdentifier().getNodeType())) { + throw new RestconfDocumentedException("Instance identifier need to contain at least one path argument", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + final DOMMountPoint mountPoint = context.getMountPoint(); + final Absolute schemaPath = context.inference().toSchemaInferenceStack().toSchemaNodeIdentifier(); + final DOMActionResult response; + if (mountPoint != null) { + response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, mountPoint); } else { - transactionChain = transactionChainOfMountPoint(mountPoint); - ref = new SchemaContextRef(mountPoint.getSchemaContext()); + response = invokeAction((ContainerNode) data, schemaPath, yangIIdContext, actionService); + } + final DOMActionResult result = checkActionResponse(response); + + ContainerNode resultData = null; + if (result != null) { + resultData = result.getOutput().orElse(null); + } + + if (resultData != null && resultData.isEmpty()) { + return Response.status(Status.NO_CONTENT).build(); } - final TransactionVarsWrapper transactionNode = new TransactionVarsWrapper( - context.getInstanceIdentifierContext(), mountPoint, transactionChain); + return Response.status(Status.OK) + .entity(NormalizedNodePayload.ofNullable(context, resultData)) + .build(); + } + + /** + * Invoking Action via mount point. + * + * @param mountPoint mount point + * @param data input data + * @param schemaPath schema path of data + * @return {@link DOMActionResult} + */ + private static DOMActionResult invokeAction(final ContainerNode data, + final Absolute schemaPath, final YangInstanceIdentifier yangIId, final DOMMountPoint mountPoint) { + return invokeAction(data, schemaPath, yangIId, mountPoint.getService(DOMActionService.class) + .orElseThrow(() -> new RestconfDocumentedException("DomAction service is missing."))); + } - return PatchDataTransactionUtil.patchData(context, transactionNode, ref); + /** + * Invoke Action via ActionServiceHandler. + * + * @param data input data + * @param yangIId invocation context + * @param schemaPath schema path of data + * @param actionService action service to invoke action + * @return {@link DOMActionResult} + */ + // FIXME: NETCONF-718: we should be returning a future here + private static DOMActionResult invokeAction(final ContainerNode data, final Absolute schemaPath, + final YangInstanceIdentifier yangIId, final DOMActionService actionService) { + return RestconfInvokeOperationsServiceImpl.checkedGet(Futures.catching(actionService.invokeAction( + schemaPath, new DOMDataTreeIdentifier(LogicalDatastoreType.OPERATIONAL, yangIId.getParent()), data), + DOMActionException.class, + cause -> new SimpleDOMActionResult(List.of(RpcResultBuilder.newError( + ErrorType.RPC, ErrorTag.OPERATION_FAILED, cause.getMessage()))), + MoreExecutors.directExecutor())); } /** - * Prepare transaction chain to access data of mount point. - * @param mountPoint - * mount point reference - * @return {@link DOMTransactionChain} + * Check the validity of the result. + * + * @param response response of Action + * @return {@link DOMActionResult} result */ - private static DOMTransactionChain transactionChainOfMountPoint(@Nonnull final DOMMountPoint mountPoint) { - final Optional domDataBrokerService = mountPoint.getService(DOMDataBroker.class); - if (domDataBrokerService.isPresent()) { - return domDataBrokerService.get().createTransactionChain(RestConnectorProvider.TRANSACTION_CHAIN_LISTENER); + private static DOMActionResult checkActionResponse(final DOMActionResult response) { + if (response == null) { + return null; + } + + try { + if (response.getErrors().isEmpty()) { + return response; + } + LOG.debug("InvokeAction Error Message {}", response.getErrors()); + throw new RestconfDocumentedException("InvokeAction Error Message ", null, response.getErrors()); + } catch (final CancellationException e) { + final String errMsg = "The Action Operation was cancelled while executing."; + LOG.debug("Cancel Execution: {}", errMsg, e); + throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e); } + } - final String errMsg = "DOM data broker service isn't available for mount point " + mountPoint.getIdentifier(); - LOG.warn(errMsg); - throw new RestconfDocumentedException(errMsg); + /** + * Valid input data based on presence of a schema node. + * + * @param haveSchemaNode true if there is an underlying schema node + * @param payload input data + */ + @VisibleForTesting + public static void validInputData(final boolean haveSchemaNode, final NormalizedNodePayload payload) { + final boolean haveData = payload.getData() != null; + if (haveSchemaNode) { + if (!haveData) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, + ErrorTag.MALFORMED_MESSAGE); + } + } else if (haveData) { + throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + } + + /** + * Valid top level node name. + * + * @param path path of node + * @param payload data + */ + @VisibleForTesting + public static void validTopLevelNodeName(final YangInstanceIdentifier path, final NormalizedNodePayload payload) { + final QName dataNodeType = payload.getData().getIdentifier().getNodeType(); + if (path.isEmpty()) { + if (!NETCONF_BASE_QNAME.equals(dataNodeType)) { + throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + } else { + final String identifierName = path.getLastPathArgument().getNodeType().getLocalName(); + final String payloadName = dataNodeType.getLocalName(); + if (!payloadName.equals(identifierName)) { + throw new RestconfDocumentedException( + "Payload name (" + payloadName + ") is different from identifier name (" + identifierName + ")", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + } + } + + + /** + * Validates whether keys in {@code payload} are equal to values of keys in + * {@code iiWithData} for list schema node. + * + * @throws RestconfDocumentedException if key values or key count in payload and URI isn't equal + */ + @VisibleForTesting + public static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodePayload payload) { + final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext(); + final PathArgument lastPathArgument = iiWithData.getInstanceIdentifier().getLastPathArgument(); + final SchemaNode schemaNode = iiWithData.getSchemaNode(); + final NormalizedNode data = payload.getData(); + if (schemaNode instanceof ListSchemaNode) { + final List keyDefinitions = ((ListSchemaNode) schemaNode).getKeyDefinition(); + if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) { + final Map uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap(); + isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions); + } + } + } + + private static void isEqualUriAndPayloadKeyValues(final Map uriKeyValues, final MapEntryNode payload, + final List keyDefinitions) { + final Map mutableCopyUriKeyValues = new HashMap<>(uriKeyValues); + for (final QName keyDefinition : keyDefinitions) { + final Object uriKeyValue = RestconfDocumentedException.throwIfNull( + mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, + "Missing key %s in URI.", keyDefinition); + + final Object dataKeyValue = payload.getIdentifier().getValue(keyDefinition); + + if (!uriKeyValue.equals(dataKeyValue)) { + final String errMsg = "The value '" + uriKeyValue + "' for key '" + keyDefinition.getLocalName() + + "' specified in the URI doesn't match the value '" + dataKeyValue + + "' specified in the message body. "; + throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + } } }