diff options
Diffstat (limited to 'netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl')
25 files changed, 6458 insertions, 0 deletions
diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BatchedExistenceCheck.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BatchedExistenceCheck.java new file mode 100644 index 0000000..4efc6a8 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BatchedExistenceCheck.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2017 Pantheon Technologies, s.r.o. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.SettableFuture; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.Collection; +import java.util.Map.Entry; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.common.api.ReadFailedException; +import org.opendaylight.mdsal.dom.api.DOMDataTreeReadOperations; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; + +final class BatchedExistenceCheck { + private static final AtomicIntegerFieldUpdater<BatchedExistenceCheck> UPDATER = + AtomicIntegerFieldUpdater.newUpdater(BatchedExistenceCheck.class, "outstanding"); + + private final SettableFuture<Entry<YangInstanceIdentifier, ReadFailedException>> future = SettableFuture.create(); + + @SuppressWarnings("unused") + private volatile int outstanding; + + private BatchedExistenceCheck(final int total) { + this.outstanding = total; + } + + static BatchedExistenceCheck start(final DOMDataTreeReadOperations readTx, + final LogicalDatastoreType datastore, final YangInstanceIdentifier parentPath, + final Collection<? extends NormalizedNode> children) { + final BatchedExistenceCheck ret = new BatchedExistenceCheck(children.size()); + for (NormalizedNode child : children) { + final YangInstanceIdentifier path = parentPath.node(child.getIdentifier()); + readTx.exists(datastore, path).addCallback(new FutureCallback<Boolean>() { + @Override + public void onSuccess(final Boolean result) { + ret.complete(path, result); + } + + @Override + @SuppressFBWarnings("BC_UNCONFIRMED_CAST_OF_RETURN_VALUE") + public void onFailure(final Throwable throwable) { + final Exception e; + if (throwable instanceof Exception) { + e = (Exception) throwable; + } else { + e = new ExecutionException(throwable); + } + + ret.complete(path, ReadFailedException.MAPPER.apply(e)); + } + }, MoreExecutors.directExecutor()); + } + + return ret; + } + + Entry<YangInstanceIdentifier, ReadFailedException> getFailure() throws InterruptedException { + try { + return future.get(); + } catch (ExecutionException e) { + // This should never happen + throw new IllegalStateException(e); + } + } + + private void complete(final YangInstanceIdentifier childPath, final boolean present) { + final int count = UPDATER.decrementAndGet(this); + if (present) { + future.set(new SimpleImmutableEntry<>(childPath, null)); + } else if (count == 0) { + future.set(null); + } + } + + private void complete(final YangInstanceIdentifier childPath, final ReadFailedException cause) { + UPDATER.decrementAndGet(this); + future.set(new SimpleImmutableEntry<>(childPath, cause)); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/Bierman02RestConfWiring.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/Bierman02RestConfWiring.java new file mode 100644 index 0000000..18d19e4 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/Bierman02RestConfWiring.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019 Red Hat, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.servlet.ServletException; +import org.opendaylight.mdsal.dom.api.DOMDataBroker; +import org.opendaylight.mdsal.dom.api.DOMMountPointService; +import org.opendaylight.mdsal.dom.api.DOMNotificationService; +import org.opendaylight.mdsal.dom.api.DOMRpcService; +import org.opendaylight.mdsal.dom.api.DOMSchemaService; +import org.opendaylight.netconf.sal.rest.impl.RestconfApplication; +import org.opendaylight.netconf.sal.restconf.api.RestConfConfig; +import org.opendaylight.netconf.sal.restconf.web.WebInitializer; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddressBuilder; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber; +import org.opendaylight.yangtools.yang.common.Uint16; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Standalone wiring for RESTCONF. + * + * <p>ACK: Some lines here were originally inspired by the RestConfWiring class in + * opendaylight-simple which in turn was inspired by the CommunityRestConf class + * from lighty.io. The differences include (1) that this class is "pure Java" + * without depending on any binding framework utility classes; (2) we do not mix + * bierman02 and rfc8040 for proper modularity; (3) we simply use {@literal @}Inject + * instead of manual object wiring, where possible. + * + * @author Michael Vorburger.ch (see ACK note for history) + */ +@SuppressWarnings("deprecation") +// NOT @Singleton, to avoid that the blueprint-maven-plugin generates <bean>, which we don't want for this +public class Bierman02RestConfWiring { + + private static final Logger LOG = LoggerFactory.getLogger(Bierman02RestConfWiring.class); + + private final RestconfProviderImpl webSocketServer; + + @Inject + // The point of all the arguments here is simply to make your chosen Dependency Injection (DI) framework init. them + public Bierman02RestConfWiring(final RestConfConfig config, + final DOMSchemaService domSchemaService, final DOMMountPointService domMountPointService, + final DOMRpcService domRpcService, final DOMDataBroker domDataBroker, + final DOMNotificationService domNotificationService, final ControllerContext controllerContext, + final RestconfApplication application, final BrokerFacade broker, final RestconfImpl restconf, + final StatisticsRestconfServiceWrapper stats, final JSONRestconfServiceImpl jsonRestconfServiceImpl, + final WebInitializer webInitializer) { + + // WebSocket + LOG.info("webSocketAddress = {}, webSocketPort = {}", config.webSocketAddress(), config.webSocketPort()); + IpAddress wsIpAddress = IpAddressBuilder.getDefaultInstance(config.webSocketAddress().getHostAddress()); + this.webSocketServer = new RestconfProviderImpl(stats, wsIpAddress, + new PortNumber(Uint16.valueOf(config.webSocketPort()))); + } + + @PostConstruct + public void start() throws ServletException { + this.webSocketServer.start(); + } + + @PreDestroy + public void stop() { + this.webSocketServer.close(); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java new file mode 100644 index 0000000..ca867cb --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/BrokerFacade.java @@ -0,0 +1,1277 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; +import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.CONFIGURATION; +import static org.opendaylight.mdsal.common.api.LogicalDatastoreType.OPERATIONAL; + +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableList; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; +import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Response.Status; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.mdsal.common.api.CommitInfo; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.common.api.ReadFailedException; +import org.opendaylight.mdsal.dom.api.DOMDataBroker; +import org.opendaylight.mdsal.dom.api.DOMDataTreeChangeService; +import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier; +import org.opendaylight.mdsal.dom.api.DOMDataTreeReadOperations; +import org.opendaylight.mdsal.dom.api.DOMDataTreeReadTransaction; +import org.opendaylight.mdsal.dom.api.DOMDataTreeReadWriteTransaction; +import org.opendaylight.mdsal.dom.api.DOMDataTreeWriteTransaction; +import org.opendaylight.mdsal.dom.api.DOMMountPoint; +import org.opendaylight.mdsal.dom.api.DOMNotificationListener; +import org.opendaylight.mdsal.dom.api.DOMNotificationService; +import org.opendaylight.mdsal.dom.api.DOMRpcResult; +import org.opendaylight.mdsal.dom.api.DOMRpcService; +import org.opendaylight.mdsal.dom.api.DOMSchemaService; +import org.opendaylight.netconf.sal.streams.listeners.ListenerAdapter; +import org.opendaylight.netconf.sal.streams.listeners.NotificationListenerAdapter; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +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.PatchEditOperation; +import org.opendaylight.restconf.common.patch.PatchEntity; +import org.opendaylight.restconf.common.patch.PatchStatusContext; +import org.opendaylight.restconf.common.patch.PatchStatusEntity; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.CreateDataChangeEventSubscriptionInput1.Scope; +import org.opendaylight.yangtools.concepts.ListenerRegistration; +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.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +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.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.UserLeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.NormalizedNodeBuilder; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextTree; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public class BrokerFacade implements Closeable { + private static final Logger LOG = LoggerFactory.getLogger(BrokerFacade.class); + + private final ThreadLocal<Boolean> isMounted = new ThreadLocal<>(); + private final DOMNotificationService domNotification; + private final ControllerContext controllerContext; + private final DOMDataBroker domDataBroker; + + private volatile DOMRpcService rpcService; + + @Inject + public BrokerFacade(final DOMRpcService rpcService, final DOMDataBroker domDataBroker, + final DOMNotificationService domNotification, final ControllerContext controllerContext) { + this.rpcService = requireNonNull(rpcService); + this.domDataBroker = requireNonNull(domDataBroker); + this.domNotification = requireNonNull(domNotification); + this.controllerContext = requireNonNull(controllerContext); + } + + /** + * Factory method. + * + * @deprecated Just use + * {@link #BrokerFacade(DOMRpcService, DOMDataBroker, DOMNotificationService, ControllerContext)} + * constructor instead. + */ + @Deprecated + public static BrokerFacade newInstance(final DOMRpcService rpcService, final DOMDataBroker domDataBroker, + final DOMNotificationService domNotification, final ControllerContext controllerContext) { + return new BrokerFacade(rpcService, domDataBroker, domNotification, controllerContext); + } + + @Override + @PreDestroy + public void close() { + } + + /** + * Read config data by path. + * + * @param path + * path of data + * @return read date + */ + public NormalizedNode readConfigurationData(final YangInstanceIdentifier path) { + return readConfigurationData(path, null); + } + + /** + * Read config data by path. + * + * @param path + * path of data + * @param withDefa + * value of with-defaults parameter + * @return read date + */ + public NormalizedNode readConfigurationData(final YangInstanceIdentifier path, final String withDefa) { + try (DOMDataTreeReadTransaction tx = domDataBroker.newReadOnlyTransaction()) { + return readDataViaTransaction(tx, CONFIGURATION, path, withDefa); + } + } + + /** + * Read config data from mount point by path. + * + * @param mountPoint + * mount point for reading data + * @param path + * path of data + * @return read data + */ + public NormalizedNode readConfigurationData(final DOMMountPoint mountPoint, final YangInstanceIdentifier path) { + return readConfigurationData(mountPoint, path, null); + } + + /** + * Read config data from mount point by path. + * + * @param mountPoint + * mount point for reading data + * @param path + * path of data + * @param withDefa + * value of with-defaults parameter + * @return read data + */ + public NormalizedNode readConfigurationData(final DOMMountPoint mountPoint, final YangInstanceIdentifier path, + final String withDefa) { + final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class); + if (domDataBrokerService.isPresent()) { + try (DOMDataTreeReadTransaction tx = domDataBrokerService.get().newReadOnlyTransaction()) { + return readDataViaTransaction(tx, CONFIGURATION, path, withDefa); + } + } + throw dataBrokerUnavailable(path); + } + + /** + * Read operational data by path. + * + * @param path + * path of data + * @return read data + */ + public NormalizedNode readOperationalData(final YangInstanceIdentifier path) { + try (DOMDataTreeReadTransaction tx = domDataBroker.newReadOnlyTransaction()) { + return readDataViaTransaction(tx, OPERATIONAL, path); + } + } + + /** + * Read operational data from mount point by path. + * + * @param mountPoint + * mount point for reading data + * @param path + * path of data + * @return read data + */ + public NormalizedNode readOperationalData(final DOMMountPoint mountPoint, final YangInstanceIdentifier path) { + final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class); + if (domDataBrokerService.isPresent()) { + try (DOMDataTreeReadTransaction tx = domDataBrokerService.get().newReadOnlyTransaction()) { + return readDataViaTransaction(tx, OPERATIONAL, path); + } + } + throw dataBrokerUnavailable(path); + } + + /** + * <b>PUT configuration data</b> + * + * <p> + * Prepare result(status) for PUT operation and PUT data via transaction. + * Return wrapped status and future from PUT. + * + * @param globalSchema + * used by merge parents (if contains list) + * @param path + * path of node + * @param payload + * input data + * @param point + * point + * @param insert + * insert + * @return wrapper of status and future of PUT + */ + public PutResult commitConfigurationDataPut(final EffectiveModelContext globalSchema, + final YangInstanceIdentifier path, final NormalizedNode payload, final String insert, final String point) { + requireNonNull(globalSchema); + requireNonNull(path); + requireNonNull(payload); + + isMounted.set(false); + final DOMDataTreeReadWriteTransaction newReadWriteTransaction = domDataBroker.newReadWriteTransaction(); + final Status status = readDataViaTransaction(newReadWriteTransaction, CONFIGURATION, path) != null ? Status.OK + : Status.CREATED; + final FluentFuture<? extends CommitInfo> future = putDataViaTransaction( + newReadWriteTransaction, CONFIGURATION, path, payload, globalSchema, insert, point); + isMounted.remove(); + return new PutResult(status, future); + } + + /** + * <b>PUT configuration data (Mount point)</b> + * + * <p> + * Prepare result(status) for PUT operation and PUT data via transaction. + * Return wrapped status and future from PUT. + * + * @param mountPoint + * mount point for getting transaction for operation and schema + * context for merging parents(if contains list) + * @param path + * path of node + * @param payload + * input data + * @param point + * point + * @param insert + * insert + * @return wrapper of status and future of PUT + */ + public PutResult commitMountPointDataPut(final DOMMountPoint mountPoint, final YangInstanceIdentifier path, + final NormalizedNode payload, final String insert, final String point) { + requireNonNull(mountPoint); + requireNonNull(path); + requireNonNull(payload); + + isMounted.set(true); + final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class); + if (domDataBrokerService.isPresent()) { + final DOMDataTreeReadWriteTransaction newReadWriteTransaction = + domDataBrokerService.get().newReadWriteTransaction(); + final Status status = readDataViaTransaction(newReadWriteTransaction, CONFIGURATION, path) != null + ? Status.OK : Status.CREATED; + final FluentFuture<? extends CommitInfo> future = putDataViaTransaction( + newReadWriteTransaction, CONFIGURATION, path, payload, modelContext(mountPoint), insert, point); + isMounted.remove(); + return new PutResult(status, future); + } + isMounted.remove(); + throw dataBrokerUnavailable(path); + } + + public PatchStatusContext patchConfigurationDataWithinTransaction(final PatchContext patchContext) + throws Exception { + final DOMMountPoint mountPoint = patchContext.getInstanceIdentifierContext().getMountPoint(); + + // get new transaction and schema context on server or on mounted device + final EffectiveModelContext schemaContext; + final DOMDataTreeReadWriteTransaction patchTransaction; + if (mountPoint == null) { + schemaContext = patchContext.getInstanceIdentifierContext().getSchemaContext(); + patchTransaction = domDataBroker.newReadWriteTransaction(); + } else { + schemaContext = modelContext(mountPoint); + + final Optional<DOMDataBroker> optional = mountPoint.getService(DOMDataBroker.class); + + if (optional.isPresent()) { + patchTransaction = optional.get().newReadWriteTransaction(); + } else { + // if mount point does not have broker it is not possible to continue and global error is reported + LOG.error("Http Patch {} has failed - device {} does not support broker service", + patchContext.getPatchId(), mountPoint.getIdentifier()); + return new PatchStatusContext( + patchContext.getPatchId(), + null, + false, + ImmutableList.of(new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, + "DOM data broker service isn't available for mount point " + mountPoint.getIdentifier())) + ); + } + } + + final List<PatchStatusEntity> editCollection = new ArrayList<>(); + List<RestconfError> editErrors; + boolean withoutError = true; + + for (final PatchEntity patchEntity : patchContext.getData()) { + final PatchEditOperation operation = patchEntity.getOperation(); + switch (operation) { + case CREATE: + if (withoutError) { + try { + postDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(), + patchEntity.getNode(), schemaContext); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), true, null)); + } catch (final RestconfDocumentedException e) { + LOG.error("Error call http Patch operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), false, editErrors)); + withoutError = false; + } + } + break; + case REPLACE: + if (withoutError) { + try { + putDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity + .getTargetNode(), patchEntity.getNode(), schemaContext); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), true, null)); + } catch (final RestconfDocumentedException e) { + LOG.error("Error call http Patch operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), false, editErrors)); + withoutError = false; + } + } + break; + case DELETE: + case REMOVE: + if (withoutError) { + try { + deleteDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity + .getTargetNode()); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), true, null)); + } catch (final RestconfDocumentedException e) { + LOG.error("Error call http Patch operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), false, editErrors)); + withoutError = false; + } + } + break; + case MERGE: + if (withoutError) { + try { + mergeDataWithinTransaction(patchTransaction, CONFIGURATION, patchEntity.getTargetNode(), + patchEntity.getNode(), schemaContext); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), true, null)); + } catch (final RestconfDocumentedException e) { + LOG.error("Error call http Patch operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + + editErrors = new ArrayList<>(); + editErrors.addAll(e.getErrors()); + editCollection.add(new PatchStatusEntity(patchEntity.getEditId(), false, editErrors)); + withoutError = false; + } + } + break; + default: + LOG.error("Unsupported http Patch operation {} on target {}", + operation, + patchEntity.getTargetNode().toString()); + break; + } + } + + // if errors then cancel transaction and return error status + if (!withoutError) { + patchTransaction.cancel(); + return new PatchStatusContext(patchContext.getPatchId(), ImmutableList.copyOf(editCollection), false, null); + } + + // if no errors commit transaction + final CountDownLatch waiter = new CountDownLatch(1); + final FluentFuture<? extends CommitInfo> future = patchTransaction.commit(); + final PatchStatusContextHelper status = new PatchStatusContextHelper(); + + future.addCallback(new FutureCallback<CommitInfo>() { + @Override + public void onSuccess(final CommitInfo result) { + status.setStatus(new PatchStatusContext(patchContext.getPatchId(), ImmutableList.copyOf(editCollection), + true, null)); + waiter.countDown(); + } + + @Override + public void onFailure(final Throwable throwable) { + // if commit failed it is global error + LOG.error("Http Patch {} transaction commit has failed", patchContext.getPatchId()); + status.setStatus(new PatchStatusContext(patchContext.getPatchId(), ImmutableList.copyOf(editCollection), + false, ImmutableList.of( + new RestconfError(ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED, throwable.getMessage())))); + waiter.countDown(); + } + }, MoreExecutors.directExecutor()); + + waiter.await(); + return status.getStatus(); + } + + // POST configuration + public FluentFuture<? extends CommitInfo> commitConfigurationDataPost( + final EffectiveModelContext globalSchema, final YangInstanceIdentifier path, + final NormalizedNode payload, final String insert, final String point) { + isMounted.set(false); + FluentFuture<? extends CommitInfo> future = + postDataViaTransaction(domDataBroker.newReadWriteTransaction(), CONFIGURATION, path, payload, + globalSchema, insert, point); + isMounted.remove(); + return future; + } + + public FluentFuture<? extends CommitInfo> commitConfigurationDataPost( + final DOMMountPoint mountPoint, final YangInstanceIdentifier path, final NormalizedNode payload, + final String insert, final String point) { + isMounted.set(true); + final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class); + if (domDataBrokerService.isPresent()) { + FluentFuture<? extends CommitInfo> future = + postDataViaTransaction(domDataBrokerService.get().newReadWriteTransaction(), CONFIGURATION, path, + payload, modelContext(mountPoint), insert, point); + isMounted.remove(); + return future; + } + isMounted.remove(); + throw dataBrokerUnavailable(path); + } + + // DELETE configuration + public FluentFuture<? extends CommitInfo> commitConfigurationDataDelete(final YangInstanceIdentifier path) { + return deleteDataViaTransaction(domDataBroker.newReadWriteTransaction(), CONFIGURATION, path); + } + + public FluentFuture<? extends CommitInfo> commitConfigurationDataDelete( + final DOMMountPoint mountPoint, final YangInstanceIdentifier path) { + final Optional<DOMDataBroker> domDataBrokerService = mountPoint.getService(DOMDataBroker.class); + if (domDataBrokerService.isPresent()) { + return deleteDataViaTransaction(domDataBrokerService.get().newReadWriteTransaction(), CONFIGURATION, path); + } + throw dataBrokerUnavailable(path); + } + + // RPC + public ListenableFuture<? extends DOMRpcResult> invokeRpc(final @NonNull QName type, + final @NonNull NormalizedNode input) { + if (rpcService == null) { + throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE); + } + LOG.trace("Invoke RPC {} with input: {}", type, input); + return rpcService.invokeRpc(type, input); + } + + public void registerToListenDataChanges(final LogicalDatastoreType datastore, final Scope scope, + final ListenerAdapter listener) { + if (listener.isListening()) { + return; + } + + final YangInstanceIdentifier path = listener.getPath(); + DOMDataTreeChangeService changeService = domDataBroker.getExtensions() + .getInstance(DOMDataTreeChangeService.class); + if (changeService == null) { + throw new UnsupportedOperationException("DOMDataBroker does not support the DOMDataTreeChangeService" + + domDataBroker); + } + DOMDataTreeIdentifier root = new DOMDataTreeIdentifier(datastore, path); + ListenerRegistration<ListenerAdapter> registration = + changeService.registerDataTreeChangeListener(root, listener); + listener.setRegistration(registration); + } + + private NormalizedNode readDataViaTransaction(final DOMDataTreeReadOperations transaction, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path) { + return readDataViaTransaction(transaction, datastore, path, null); + } + + private NormalizedNode readDataViaTransaction(final DOMDataTreeReadOperations transaction, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final String withDefa) { + LOG.trace("Read {} via Restconf: {}", datastore.name(), path); + + try { + final Optional<NormalizedNode> optional = transaction.read(datastore, path).get(); + return optional.map(normalizedNode -> withDefa == null ? normalizedNode : + prepareDataByParamWithDef(normalizedNode, path, withDefa)).orElse(null); + } catch (InterruptedException e) { + LOG.warn("Error reading {} from datastore {}", path, datastore.name(), e); + throw new RestconfDocumentedException("Error reading data.", e); + } catch (ExecutionException e) { + LOG.warn("Error reading {} from datastore {}", path, datastore.name(), e); + throw RestconfDocumentedException.decodeAndThrow("Error reading data.", Throwables.getCauseAs(e, + ReadFailedException.class)); + } + } + + private NormalizedNode prepareDataByParamWithDef(final NormalizedNode result, + final YangInstanceIdentifier path, final String withDefa) { + boolean trim; + switch (withDefa) { + case "trim": + trim = true; + break; + case "explicit": + trim = false; + break; + default: + throw new RestconfDocumentedException("Bad value used with with-defaults parameter : " + withDefa); + } + + final EffectiveModelContext ctx = controllerContext.getGlobalSchema(); + final DataSchemaContextTree baseSchemaCtxTree = DataSchemaContextTree.from(ctx); + final DataSchemaNode baseSchemaNode = baseSchemaCtxTree.findChild(path).orElseThrow().getDataSchemaNode(); + if (result instanceof ContainerNode) { + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> builder = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) baseSchemaNode); + buildCont(builder, (ContainerNode) result, baseSchemaCtxTree, path, trim); + return builder.build(); + } + + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> builder = + SchemaAwareBuilders.mapEntryBuilder((ListSchemaNode) baseSchemaNode); + buildMapEntryBuilder(builder, (MapEntryNode) result, baseSchemaCtxTree, path, trim, + ((ListSchemaNode) baseSchemaNode).getKeyDefinition()); + return builder.build(); + } + + private void buildMapEntryBuilder( + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> builder, + final MapEntryNode result, final DataSchemaContextTree baseSchemaCtxTree, + final YangInstanceIdentifier actualPath, final boolean trim, final List<QName> keys) { + for (final DataContainerChild child : result.body()) { + final YangInstanceIdentifier path = actualPath.node(child.getIdentifier()); + final DataSchemaNode childSchema = baseSchemaCtxTree.findChild(path).orElseThrow().getDataSchemaNode(); + if (child instanceof ContainerNode) { + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> childBuilder = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) childSchema); + buildCont(childBuilder, (ContainerNode) child, baseSchemaCtxTree, path, trim); + builder.withChild(childBuilder.build()); + } else if (child instanceof MapNode) { + final CollectionNodeBuilder<MapEntryNode, SystemMapNode> childBuilder = + SchemaAwareBuilders.mapBuilder((ListSchemaNode) childSchema); + buildList(childBuilder, (MapNode) child, baseSchemaCtxTree, path, trim, + ((ListSchemaNode) childSchema).getKeyDefinition()); + builder.withChild(childBuilder.build()); + } else if (child instanceof LeafNode) { + final Object defaultVal = ((LeafSchemaNode) childSchema).getType().getDefaultValue().orElse(null); + final Object nodeVal = child.body(); + final NormalizedNodeBuilder<NodeIdentifier, Object, LeafNode<Object>> leafBuilder = + SchemaAwareBuilders.leafBuilder((LeafSchemaNode) childSchema); + if (keys.contains(child.getIdentifier().getNodeType())) { + leafBuilder.withValue(child.body()); + builder.withChild(leafBuilder.build()); + } else { + if (trim) { + if (defaultVal == null || !defaultVal.equals(nodeVal)) { + leafBuilder.withValue(child.body()); + builder.withChild(leafBuilder.build()); + } + } else { + if (defaultVal != null && defaultVal.equals(nodeVal)) { + leafBuilder.withValue(child.body()); + builder.withChild(leafBuilder.build()); + } + } + } + } + } + } + + private void buildList(final CollectionNodeBuilder<MapEntryNode, SystemMapNode> builder, final MapNode result, + final DataSchemaContextTree baseSchemaCtxTree, final YangInstanceIdentifier path, final boolean trim, + final List<QName> keys) { + for (final MapEntryNode mapEntryNode : result.body()) { + final YangInstanceIdentifier actualNode = path.node(mapEntryNode.getIdentifier()); + final DataSchemaNode childSchema = baseSchemaCtxTree.findChild(actualNode).orElseThrow() + .getDataSchemaNode(); + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> mapEntryBuilder = + SchemaAwareBuilders.mapEntryBuilder((ListSchemaNode) childSchema); + buildMapEntryBuilder(mapEntryBuilder, mapEntryNode, baseSchemaCtxTree, actualNode, trim, keys); + builder.withChild(mapEntryBuilder.build()); + } + } + + private void buildCont(final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> builder, + final ContainerNode result, final DataSchemaContextTree baseSchemaCtxTree, + final YangInstanceIdentifier actualPath, final boolean trim) { + for (final DataContainerChild child : result.body()) { + final YangInstanceIdentifier path = actualPath.node(child.getIdentifier()); + final DataSchemaNode childSchema = baseSchemaCtxTree.findChild(path).orElseThrow().getDataSchemaNode(); + if (child instanceof ContainerNode) { + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> builderChild = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) childSchema); + buildCont(builderChild, result, baseSchemaCtxTree, actualPath, trim); + builder.withChild(builderChild.build()); + } else if (child instanceof MapNode) { + final CollectionNodeBuilder<MapEntryNode, SystemMapNode> childBuilder = + SchemaAwareBuilders.mapBuilder((ListSchemaNode) childSchema); + buildList(childBuilder, (MapNode) child, baseSchemaCtxTree, path, trim, + ((ListSchemaNode) childSchema).getKeyDefinition()); + builder.withChild(childBuilder.build()); + } else if (child instanceof LeafNode) { + final Object defaultVal = ((LeafSchemaNode) childSchema).getType().getDefaultValue().orElse(null); + final Object nodeVal = child.body(); + final NormalizedNodeBuilder<NodeIdentifier, Object, LeafNode<Object>> leafBuilder = + SchemaAwareBuilders.leafBuilder((LeafSchemaNode) childSchema); + if (trim) { + if (defaultVal == null || !defaultVal.equals(nodeVal)) { + leafBuilder.withValue(child.body()); + builder.withChild(leafBuilder.build()); + } + } else { + if (defaultVal != null && defaultVal.equals(nodeVal)) { + leafBuilder.withValue(child.body()); + builder.withChild(leafBuilder.build()); + } + } + } + } + } + + /** + * POST data and submit transaction {@link DOMDataReadWriteTransaction}. + */ + private FluentFuture<? extends CommitInfo> postDataViaTransaction( + final DOMDataTreeReadWriteTransaction rwTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String insert, final String point) { + LOG.trace("POST {} via Restconf: {} with payload {}", datastore.name(), path, payload); + postData(rwTransaction, datastore, path, payload, schemaContext, insert, point); + return rwTransaction.commit(); + } + + /** + * POST data and do NOT submit transaction {@link DOMDataReadWriteTransaction}. + */ + private void postDataWithinTransaction( + final DOMDataTreeReadWriteTransaction rwTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + LOG.trace("POST {} within Restconf Patch: {} with payload {}", datastore.name(), path, payload); + postData(rwTransaction, datastore, path, payload, schemaContext, null, null); + } + + private void postData(final DOMDataTreeReadWriteTransaction rwTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String insert, final String point) { + if (insert == null) { + makeNormalPost(rwTransaction, datastore, path, payload, schemaContext); + return; + } + + final DataSchemaNode schemaNode = checkListAndOrderedType(schemaContext, path); + checkItemDoesNotExists(rwTransaction, datastore, path); + switch (insert) { + case "first": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = + (UserMapNode) this.readConfigurationData(path.getParent().getParent()); + if (readList == null || readList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + rwTransaction.delete(datastore, path.getParent().getParent()); + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + makeNormalPost(rwTransaction, datastore, path.getParent().getParent(), readList, + schemaContext); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + rwTransaction.delete(datastore, path.getParent()); + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + makeNormalPost(rwTransaction, datastore, path.getParent().getParent(), readLeafList, + schemaContext); + } + } + break; + case "last": + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + break; + case "before": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = + (UserMapNode) this.readConfigurationData(path.getParent().getParent()); + if (readList == null || readList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + insertWithPointListPost(rwTransaction, datastore, path, payload, schemaContext, point, + readList, + true); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + insertWithPointLeafListPost(rwTransaction, datastore, path, payload, schemaContext, point, + readLeafList, true); + } + } + break; + case "after": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = + (UserMapNode) this.readConfigurationData(path.getParent().getParent()); + if (readList == null || readList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + insertWithPointListPost(rwTransaction, datastore, path, payload, schemaContext, point, + readList, + false); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } else { + insertWithPointLeafListPost(rwTransaction, datastore, path, payload, schemaContext, point, + readLeafList, false); + } + } + break; + default: + throw new RestconfDocumentedException( + "Used bad value of insert parameter. Possible values are first, last, before or after, " + + "but was: " + insert); + } + } + + private void insertWithPointLeafListPost(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String point, final UserLeafSetNode<?> readLeafList, + final boolean before) { + rwTransaction.delete(datastore, path.getParent().getParent()); + final InstanceIdentifierContext instanceIdentifier = controllerContext.toInstanceIdentifier(point); + int lastItemPosition = 0; + for (final LeafSetEntryNode<?> nodeChild : readLeafList.body()) { + if (nodeChild.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) { + break; + } + lastItemPosition++; + } + if (!before) { + lastItemPosition++; + } + int lastInsertedPosition = 0; + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent().getParent()); + rwTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + for (final LeafSetEntryNode<?> nodeChild : readLeafList.body()) { + if (lastInsertedPosition == lastItemPosition) { + checkItemDoesNotExists(rwTransaction, datastore, path); + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } + final YangInstanceIdentifier childPath = path.getParent().getParent().node(nodeChild.getIdentifier()); + checkItemDoesNotExists(rwTransaction, datastore, childPath); + rwTransaction.put(datastore, childPath, nodeChild); + lastInsertedPosition++; + } + } + + private void insertWithPointListPost(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, final EffectiveModelContext schemaContext, + final String point, final MapNode readList, final boolean before) { + rwTransaction.delete(datastore, path.getParent().getParent()); + final InstanceIdentifierContext instanceIdentifier = controllerContext.toInstanceIdentifier(point); + int lastItemPosition = 0; + for (final MapEntryNode mapEntryNode : readList.body()) { + if (mapEntryNode.getIdentifier() + .equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) { + break; + } + lastItemPosition++; + } + if (!before) { + lastItemPosition++; + } + int lastInsertedPosition = 0; + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent().getParent()); + rwTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + for (final MapEntryNode mapEntryNode : readList.body()) { + if (lastInsertedPosition == lastItemPosition) { + checkItemDoesNotExists(rwTransaction, datastore, path); + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + } + final YangInstanceIdentifier childPath = path.getParent().getParent().node(mapEntryNode.getIdentifier()); + checkItemDoesNotExists(rwTransaction, datastore, childPath); + rwTransaction.put(datastore, childPath, mapEntryNode); + lastInsertedPosition++; + } + } + + private static DataSchemaNode checkListAndOrderedType(final EffectiveModelContext ctx, + final YangInstanceIdentifier path) { + final YangInstanceIdentifier parent = path.getParent(); + final DataSchemaContextNode<?> node = DataSchemaContextTree.from(ctx).findChild(parent).orElseThrow(); + final DataSchemaNode dataSchemaNode = node.getDataSchemaNode(); + + if (dataSchemaNode instanceof ListSchemaNode) { + if (!((ListSchemaNode) dataSchemaNode).isUserOrdered()) { + throw new RestconfDocumentedException("Insert parameter can be used only with ordered-by user list."); + } + return dataSchemaNode; + } + if (dataSchemaNode instanceof LeafListSchemaNode) { + if (!((LeafListSchemaNode) dataSchemaNode).isUserOrdered()) { + throw new RestconfDocumentedException( + "Insert parameter can be used only with ordered-by user leaf-list."); + } + return dataSchemaNode; + } + throw new RestconfDocumentedException("Insert parameter can be used only with list or leaf-list"); + } + + private void makeNormalPost(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + final Collection<? extends NormalizedNode> children; + if (payload instanceof MapNode) { + children = ((MapNode) payload).body(); + } else if (payload instanceof LeafSetNode) { + children = ((LeafSetNode<?>) payload).body(); + } else { + simplePostPut(rwTransaction, datastore, path, payload, schemaContext); + return; + } + + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); + if (children.isEmpty()) { + if (isMounted != null && !isMounted.get()) { + + rwTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), + emptySubtree); + ensureParentsByMerge(datastore, path, rwTransaction, schemaContext); + } + return; + } + + // Kick off batch existence check first... + final BatchedExistenceCheck check = BatchedExistenceCheck.start(rwTransaction, datastore, path, children); + + // ... now enqueue modifications. This relies on proper ordering of requests, i.e. these will not affect the + // result of the existence checks... + if (isMounted != null && !isMounted.get()) { + + rwTransaction.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + ensureParentsByMerge(datastore, path, rwTransaction, schemaContext); + } + for (final NormalizedNode child : children) { + // FIXME: we really want a create(YangInstanceIdentifier, NormalizedNode) method in the transaction, + // as that would allow us to skip the existence checks + rwTransaction.put(datastore, path.node(child.getIdentifier()), child); + } + + // ... finally collect existence checks and abort the transaction if any of them failed. + final Entry<YangInstanceIdentifier, ReadFailedException> failure; + try { + failure = check.getFailure(); + } catch (InterruptedException e) { + rwTransaction.cancel(); + throw new RestconfDocumentedException("Could not determine the existence of path " + path, e); + } + + if (failure != null) { + rwTransaction.cancel(); + final ReadFailedException e = failure.getValue(); + if (e == null) { + throw new RestconfDocumentedException("Data already exists for path: " + failure.getKey(), + ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS); + } + + throw new RestconfDocumentedException("Could not determine the existence of path " + failure.getKey(), e, + e.getErrorList()); + } + } + + private void simplePostPut(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + checkItemDoesNotExists(rwTransaction, datastore, path); + if (isMounted != null && !isMounted.get()) { + ensureParentsByMerge(datastore, path, rwTransaction, schemaContext); + } + rwTransaction.put(datastore, path, payload); + } + + private static boolean doesItemExist(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType store, final YangInstanceIdentifier path) { + try { + return rwTransaction.exists(store, path).get(); + } catch (InterruptedException e) { + rwTransaction.cancel(); + throw new RestconfDocumentedException("Could not determine the existence of path " + path, e); + } catch (ExecutionException e) { + rwTransaction.cancel(); + throw RestconfDocumentedException.decodeAndThrow("Could not determine the existence of path " + path, + Throwables.getCauseAs(e, ReadFailedException.class)); + } + } + + /** + * Check if item already exists. Throws error if it does NOT already exist. + * @param rwTransaction Current transaction + * @param store Used datastore + * @param path Path to item to verify its existence + */ + private static void checkItemExists(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType store, final YangInstanceIdentifier path) { + if (!doesItemExist(rwTransaction, store, path)) { + LOG.trace("Operation via Restconf was not executed because data at {} does not exist", path); + rwTransaction.cancel(); + throw new RestconfDocumentedException("Data does not exist for path: " + path, ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING); + } + } + + /** + * Check if item does NOT already exist. Throws error if it already exists. + * @param rwTransaction Current transaction + * @param store Used datastore + * @param path Path to item to verify its existence + */ + private static void checkItemDoesNotExists(final DOMDataTreeReadWriteTransaction rwTransaction, + final LogicalDatastoreType store, final YangInstanceIdentifier path) { + if (doesItemExist(rwTransaction, store, path)) { + LOG.trace("Operation via Restconf was not executed because data at {} already exists", path); + rwTransaction.cancel(); + throw new RestconfDocumentedException("Data already exists for path: " + path, ErrorType.PROTOCOL, + ErrorTag.DATA_EXISTS); + } + } + + /** + * PUT data and submit {@link DOMDataReadWriteTransaction}. + * + * @param point + * point + * @param insert + * insert + */ + private FluentFuture<? extends CommitInfo> putDataViaTransaction( + final DOMDataTreeReadWriteTransaction readWriteTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String insert, final String point) { + LOG.trace("Put {} via Restconf: {} with payload {}", datastore.name(), path, payload); + putData(readWriteTransaction, datastore, path, payload, schemaContext, insert, point); + return readWriteTransaction.commit(); + } + + /** + * PUT data and do NOT submit {@link DOMDataReadWriteTransaction}. + */ + private void putDataWithinTransaction( + final DOMDataTreeReadWriteTransaction writeTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + LOG.trace("Put {} within Restconf Patch: {} with payload {}", datastore.name(), path, payload); + putData(writeTransaction, datastore, path, payload, schemaContext, null, null); + } + + // FIXME: This is doing correct put for container and list children, not sure if this will work for choice case + private void putData(final DOMDataTreeReadWriteTransaction rwTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String insert, final String point) { + if (insert == null) { + makePut(rwTransaction, datastore, path, payload, schemaContext); + return; + } + + final DataSchemaNode schemaNode = checkListAndOrderedType(schemaContext, path); + checkItemDoesNotExists(rwTransaction, datastore, path); + switch (insert) { + case "first": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = (UserMapNode) this.readConfigurationData(path.getParent()); + if (readList == null || readList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + rwTransaction.delete(datastore, path.getParent()); + simplePut(datastore, path, rwTransaction, schemaContext, payload); + makePut(rwTransaction, datastore, path.getParent(), readList, schemaContext); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + rwTransaction.delete(datastore, path.getParent()); + simplePut(datastore, path, rwTransaction, schemaContext, payload); + makePut(rwTransaction, datastore, path.getParent(), readLeafList, + schemaContext); + } + } + break; + case "last": + simplePut(datastore, path, rwTransaction, schemaContext, payload); + break; + case "before": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = (UserMapNode) this.readConfigurationData(path.getParent()); + if (readList == null || readList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + insertWithPointListPut(rwTransaction, datastore, path, payload, schemaContext, point, + readList, true); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + insertWithPointLeafListPut(rwTransaction, datastore, path, payload, schemaContext, point, + readLeafList, true); + } + } + break; + case "after": + if (schemaNode instanceof ListSchemaNode) { + final UserMapNode readList = (UserMapNode) this.readConfigurationData(path.getParent()); + if (readList == null || readList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + insertWithPointListPut(rwTransaction, datastore, path, payload, schemaContext, point, + readList, false); + } + } else { + final UserLeafSetNode<?> readLeafList = + (UserLeafSetNode<?>) readConfigurationData(path.getParent()); + if (readLeafList == null || readLeafList.isEmpty()) { + simplePut(datastore, path, rwTransaction, schemaContext, payload); + } else { + insertWithPointLeafListPut(rwTransaction, datastore, path, payload, schemaContext, point, + readLeafList, false); + } + } + break; + default: + throw new RestconfDocumentedException( + "Used bad value of insert parameter. Possible values are first, last, before or after, but was: " + + insert); + } + } + + private void insertWithPointLeafListPut(final DOMDataTreeWriteTransaction tx, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext, final String point, final UserLeafSetNode<?> readLeafList, + final boolean before) { + tx.delete(datastore, path.getParent()); + final InstanceIdentifierContext instanceIdentifier = controllerContext.toInstanceIdentifier(point); + int index1 = 0; + for (final LeafSetEntryNode<?> nodeChild : readLeafList.body()) { + if (nodeChild.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) { + break; + } + index1++; + } + if (!before) { + index1++; + } + int index2 = 0; + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent()); + tx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + for (final LeafSetEntryNode<?> nodeChild : readLeafList.body()) { + if (index2 == index1) { + simplePut(datastore, path, tx, schemaContext, payload); + } + final YangInstanceIdentifier childPath = path.getParent().node(nodeChild.getIdentifier()); + tx.put(datastore, childPath, nodeChild); + index2++; + } + } + + private void insertWithPointListPut(final DOMDataTreeWriteTransaction tx, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, final EffectiveModelContext schemaContext, + final String point, final UserMapNode readList, final boolean before) { + tx.delete(datastore, path.getParent()); + final InstanceIdentifierContext instanceIdentifier = controllerContext.toInstanceIdentifier(point); + int index1 = 0; + for (final MapEntryNode mapEntryNode : readList.body()) { + if (mapEntryNode.getIdentifier().equals(instanceIdentifier.getInstanceIdentifier().getLastPathArgument())) { + break; + } + index1++; + } + if (!before) { + index1++; + } + int index2 = 0; + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path.getParent()); + tx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + for (final MapEntryNode mapEntryNode : readList.body()) { + if (index2 == index1) { + simplePut(datastore, path, tx, schemaContext, payload); + } + final YangInstanceIdentifier childPath = path.getParent().node(mapEntryNode.getIdentifier()); + tx.put(datastore, childPath, mapEntryNode); + index2++; + } + } + + private void makePut(final DOMDataTreeWriteTransaction tx, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + if (payload instanceof MapNode) { + final NormalizedNode emptySubtree = ImmutableNodes.fromInstanceId(schemaContext, path); + if (isMounted != null && !isMounted.get()) { + tx.merge(datastore, YangInstanceIdentifier.create(emptySubtree.getIdentifier()), emptySubtree); + ensureParentsByMerge(datastore, path, tx, schemaContext); + } + for (final MapEntryNode child : ((MapNode) payload).body()) { + final YangInstanceIdentifier childPath = path.node(child.getIdentifier()); + tx.put(datastore, childPath, child); + } + } else { + simplePut(datastore, path, tx, schemaContext, payload); + } + } + + private void simplePut(final LogicalDatastoreType datastore, final YangInstanceIdentifier path, + final DOMDataTreeWriteTransaction tx, final EffectiveModelContext schemaContext, + final NormalizedNode payload) { + if (isMounted != null && !isMounted.get()) { + ensureParentsByMerge(datastore, path, tx, schemaContext); + } + tx.put(datastore, path, payload); + } + + private static FluentFuture<? extends CommitInfo> deleteDataViaTransaction( + final DOMDataTreeReadWriteTransaction readWriteTransaction, final LogicalDatastoreType datastore, + final YangInstanceIdentifier path) { + LOG.trace("Delete {} via Restconf: {}", datastore.name(), path); + checkItemExists(readWriteTransaction, datastore, path); + readWriteTransaction.delete(datastore, path); + return readWriteTransaction.commit(); + } + + private static void deleteDataWithinTransaction(final DOMDataTreeWriteTransaction tx, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path) { + LOG.trace("Delete {} within Restconf Patch: {}", datastore.name(), path); + tx.delete(datastore, path); + } + + private static void mergeDataWithinTransaction(final DOMDataTreeWriteTransaction tx, + final LogicalDatastoreType datastore, final YangInstanceIdentifier path, final NormalizedNode payload, + final EffectiveModelContext schemaContext) { + LOG.trace("Merge {} within Restconf Patch: {} with payload {}", datastore.name(), path, payload); + ensureParentsByMerge(datastore, path, tx, schemaContext); + + // Since YANG Patch provides the option to specify what kind of operation for each edit, + // OpenDaylight should not change it. + tx.merge(datastore, path, payload); + } + + public void registerToListenNotification(final NotificationListenerAdapter listener) { + if (listener.isListening()) { + return; + } + + final ListenerRegistration<DOMNotificationListener> registration = domNotification + .registerNotificationListener(listener, listener.getSchemaPath()); + + listener.setRegistration(registration); + } + + private static void ensureParentsByMerge(final LogicalDatastoreType store, + final YangInstanceIdentifier normalizedPath, final DOMDataTreeWriteTransaction tx, + final EffectiveModelContext schemaContext) { + final List<PathArgument> normalizedPathWithoutChildArgs = new ArrayList<>(); + YangInstanceIdentifier rootNormalizedPath = null; + + final Iterator<PathArgument> it = normalizedPath.getPathArguments().iterator(); + + while (it.hasNext()) { + final PathArgument pathArgument = it.next(); + if (rootNormalizedPath == null) { + rootNormalizedPath = YangInstanceIdentifier.create(pathArgument); + } + + if (it.hasNext()) { + normalizedPathWithoutChildArgs.add(pathArgument); + } + } + + if (normalizedPathWithoutChildArgs.isEmpty()) { + return; + } + + checkArgument(rootNormalizedPath != null, "Empty path received"); + tx.merge(store, rootNormalizedPath, ImmutableNodes.fromInstanceId(schemaContext, + YangInstanceIdentifier.create(normalizedPathWithoutChildArgs))); + } + + private static RestconfDocumentedException dataBrokerUnavailable(final YangInstanceIdentifier path) { + LOG.warn("DOM data broker service is not available for mount point {}", path); + return new RestconfDocumentedException("DOM data broker service is not available for mount point " + path); + } + + private static EffectiveModelContext modelContext(final DOMMountPoint mountPoint) { + return mountPoint.getService(DOMSchemaService.class) + .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext())) + .orElse(null); + } + + private static final class PatchStatusContextHelper { + PatchStatusContext status; + + public PatchStatusContext getStatus() { + return status; + } + + public void setStatus(final PatchStatusContext status) { + this.status = status; + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/ControllerContext.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/ControllerContext.java new file mode 100644 index 0000000..8e05bf1 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/ControllerContext.java @@ -0,0 +1,1006 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; +import static com.google.common.base.Verify.verify; +import static java.util.Objects.requireNonNull; + +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Iterables; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.Closeable; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import javax.annotation.PreDestroy; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Response.Status; +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.mdsal.dom.api.DOMMountPoint; +import org.opendaylight.mdsal.dom.api.DOMMountPointService; +import org.opendaylight.mdsal.dom.api.DOMSchemaService; +import org.opendaylight.mdsal.dom.api.DOMYangTextSourceProvider; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.Draft02.RestConfModule; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.util.RestUtil; +import org.opendaylight.yangtools.concepts.IllegalArgumentCodec; +import org.opendaylight.yangtools.concepts.ListenerRegistration; +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.QNameModule; +import org.opendaylight.yangtools.yang.common.Revision; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.InstanceIdentifierBuilder; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode; +import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContextListener; +import org.opendaylight.yangtools.yang.model.api.GroupingDefinition; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +import org.opendaylight.yangtools.yang.model.api.TypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public final class ControllerContext implements EffectiveModelContextListener, Closeable { + // FIXME: this should be in md-sal somewhere + public static final String MOUNT = "yang-ext:mount"; + + private static final Logger LOG = LoggerFactory.getLogger(ControllerContext.class); + + private static final String NULL_VALUE = "null"; + + private static final String MOUNT_MODULE = "yang-ext"; + + private static final String MOUNT_NODE = "mount"; + + private static final Splitter SLASH_SPLITTER = Splitter.on('/'); + + private final AtomicReference<Map<QName, RpcDefinition>> qnameToRpc = new AtomicReference<>(Collections.emptyMap()); + + private final DOMMountPointService mountService; + private final DOMYangTextSourceProvider yangTextSourceProvider; + private final ListenerRegistration<?> listenerRegistration; + private volatile EffectiveModelContext globalSchema; + private volatile DataNormalizer dataNormalizer; + + @Inject + public ControllerContext(final DOMSchemaService schemaService, final DOMMountPointService mountService, + final DOMSchemaService domSchemaService) { + this.mountService = mountService; + yangTextSourceProvider = domSchemaService.getExtensions().getInstance(DOMYangTextSourceProvider.class); + + onModelContextUpdated(schemaService.getGlobalContext()); + listenerRegistration = schemaService.registerSchemaContextListener(this); + } + + /** + * Factory method. + * + * @deprecated Just use the + * {@link #ControllerContext(DOMSchemaService, DOMMountPointService, DOMSchemaService)} + * constructor instead. + */ + @Deprecated + public static ControllerContext newInstance(final DOMSchemaService schemaService, + final DOMMountPointService mountService, final DOMSchemaService domSchemaService) { + return new ControllerContext(schemaService, mountService, domSchemaService); + } + + private void setGlobalSchema(final EffectiveModelContext globalSchema) { + this.globalSchema = globalSchema; + dataNormalizer = new DataNormalizer(globalSchema); + } + + public DOMYangTextSourceProvider getYangTextSourceProvider() { + return yangTextSourceProvider; + } + + private void checkPreconditions() { + if (globalSchema == null) { + throw new RestconfDocumentedException(Status.SERVICE_UNAVAILABLE); + } + } + + @Override + @PreDestroy + public void close() { + listenerRegistration.close(); + } + + public void setSchemas(final EffectiveModelContext schemas) { + onModelContextUpdated(schemas); + } + + public InstanceIdentifierContext toInstanceIdentifier(final String restconfInstance) { + return toIdentifier(restconfInstance, false); + } + + public EffectiveModelContext getGlobalSchema() { + return globalSchema; + } + + public InstanceIdentifierContext toMountPointIdentifier(final String restconfInstance) { + return toIdentifier(restconfInstance, true); + } + + private InstanceIdentifierContext toIdentifier(final String restconfInstance, + final boolean toMountPointIdentifier) { + checkPreconditions(); + + if (restconfInstance == null) { + return InstanceIdentifierContext.ofLocalRoot(globalSchema); + } + + final List<String> pathArgs = urlPathArgsDecode(SLASH_SPLITTER.split(restconfInstance)); + omitFirstAndLastEmptyString(pathArgs); + if (pathArgs.isEmpty()) { + return null; + } + + final String first = pathArgs.iterator().next(); + final String startModule = toModuleName(first); + if (startModule == null) { + throw new RestconfDocumentedException("First node in URI has to be in format \"moduleName:nodeName\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final Collection<? extends Module> latestModule = globalSchema.findModules(startModule); + if (latestModule.isEmpty()) { + throw new RestconfDocumentedException("The module named '" + startModule + "' does not exist.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + + final InstanceIdentifierContext iiWithSchemaNode = collectPathArguments(YangInstanceIdentifier.builder(), + new ArrayDeque<>(), pathArgs, latestModule.iterator().next(), null, toMountPointIdentifier); + + if (iiWithSchemaNode == null) { + throw new RestconfDocumentedException("URI has bad format", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + return iiWithSchemaNode; + } + + private static List<String> omitFirstAndLastEmptyString(final List<String> list) { + if (list.isEmpty()) { + return list; + } + + final String head = list.iterator().next(); + if (head.isEmpty()) { + list.remove(0); + } + + if (list.isEmpty()) { + return list; + } + + final String last = list.get(list.size() - 1); + if (last.isEmpty()) { + list.remove(list.size() - 1); + } + + return list; + } + + public Module findModuleByName(final String moduleName) { + checkPreconditions(); + checkArgument(moduleName != null && !moduleName.isEmpty()); + return globalSchema.findModules(moduleName).stream().findFirst().orElse(null); + } + + public static Module findModuleByName(final DOMMountPoint mountPoint, final String moduleName) { + checkArgument(moduleName != null && mountPoint != null); + + final EffectiveModelContext mountPointSchema = getModelContext(mountPoint); + return mountPointSchema == null ? null + : mountPointSchema.findModules(moduleName).stream().findFirst().orElse(null); + } + + public Module findModuleByNamespace(final XMLNamespace namespace) { + checkPreconditions(); + checkArgument(namespace != null); + return globalSchema.findModules(namespace).stream().findFirst().orElse(null); + } + + public static Module findModuleByNamespace(final DOMMountPoint mountPoint, final XMLNamespace namespace) { + checkArgument(namespace != null && mountPoint != null); + + final EffectiveModelContext mountPointSchema = getModelContext(mountPoint); + return mountPointSchema == null ? null + : mountPointSchema.findModules(namespace).stream().findFirst().orElse(null); + } + + public Module findModuleByNameAndRevision(final String name, final Revision revision) { + checkPreconditions(); + checkArgument(name != null && revision != null); + + return globalSchema.findModule(name, revision).orElse(null); + } + + public Module findModuleByNameAndRevision(final DOMMountPoint mountPoint, final String name, + final Revision revision) { + checkPreconditions(); + checkArgument(name != null && revision != null && mountPoint != null); + + final EffectiveModelContext schemaContext = getModelContext(mountPoint); + return schemaContext == null ? null : schemaContext.findModule(name, revision).orElse(null); + } + + public DataNodeContainer getDataNodeContainerFor(final YangInstanceIdentifier path) { + checkPreconditions(); + + final Iterable<PathArgument> elements = path.getPathArguments(); + final PathArgument head = elements.iterator().next(); + final QName startQName = head.getNodeType(); + final Module initialModule = globalSchema.findModule(startQName.getModule()).orElse(null); + DataNodeContainer node = initialModule; + for (final PathArgument element : elements) { + final QName _nodeType = element.getNodeType(); + final DataSchemaNode potentialNode = childByQName(node, _nodeType); + if (potentialNode == null || !isListOrContainer(potentialNode)) { + return null; + } + node = (DataNodeContainer) potentialNode; + } + + return node; + } + + public String toFullRestconfIdentifier(final YangInstanceIdentifier path, final DOMMountPoint mount) { + checkPreconditions(); + + final Iterable<PathArgument> elements = path.getPathArguments(); + final StringBuilder builder = new StringBuilder(); + final PathArgument head = elements.iterator().next(); + final QName startQName = head.getNodeType(); + final EffectiveModelContext schemaContext; + if (mount != null) { + schemaContext = getModelContext(mount); + } else { + schemaContext = globalSchema; + } + final Module initialModule = schemaContext.findModule(startQName.getModule()).orElse(null); + DataNodeContainer node = initialModule; + for (final PathArgument element : elements) { + if (!(element instanceof AugmentationIdentifier)) { + final QName _nodeType = element.getNodeType(); + final DataSchemaNode potentialNode = childByQName(node, _nodeType); + if ((!(element instanceof NodeIdentifier) || !(potentialNode instanceof ListSchemaNode)) + && !(potentialNode instanceof ChoiceSchemaNode)) { + builder.append(convertToRestconfIdentifier(element, potentialNode, mount)); + if (potentialNode instanceof DataNodeContainer) { + node = (DataNodeContainer) potentialNode; + } + } + } + } + + return builder.toString(); + } + + public String findModuleNameByNamespace(final XMLNamespace namespace) { + checkPreconditions(); + + final Module module = findModuleByNamespace(namespace); + return module == null ? null : module.getName(); + } + + public static String findModuleNameByNamespace(final DOMMountPoint mountPoint, final XMLNamespace namespace) { + final Module module = findModuleByNamespace(mountPoint, namespace); + return module == null ? null : module.getName(); + } + + public XMLNamespace findNamespaceByModuleName(final String moduleName) { + final Module module = findModuleByName(moduleName); + return module == null ? null : module.getNamespace(); + } + + public static XMLNamespace findNamespaceByModuleName(final DOMMountPoint mountPoint, final String moduleName) { + final Module module = findModuleByName(mountPoint, moduleName); + return module == null ? null : module.getNamespace(); + } + + public Collection<? extends Module> getAllModules(final DOMMountPoint mountPoint) { + checkPreconditions(); + + final EffectiveModelContext schemaContext = mountPoint == null ? null : getModelContext(mountPoint); + return schemaContext == null ? null : schemaContext.getModules(); + } + + public Collection<? extends Module> getAllModules() { + checkPreconditions(); + return globalSchema.getModules(); + } + + private static String toRestconfIdentifier(final EffectiveModelContext context, final QName qname) { + final Module schema = context.findModule(qname.getModule()).orElse(null); + return schema == null ? null : schema.getName() + ':' + qname.getLocalName(); + } + + public String toRestconfIdentifier(final QName qname, final DOMMountPoint mountPoint) { + return mountPoint != null ? toRestconfIdentifier(getModelContext(mountPoint), qname) + : toRestconfIdentifier(qname); + } + + public String toRestconfIdentifier(final QName qname) { + checkPreconditions(); + + return toRestconfIdentifier(globalSchema, qname); + } + + public static String toRestconfIdentifier(final DOMMountPoint mountPoint, final QName qname) { + return mountPoint == null ? null : toRestconfIdentifier(getModelContext(mountPoint), qname); + } + + public Module getRestconfModule() { + return findModuleByNameAndRevision(Draft02.RestConfModule.NAME, Revision.of(Draft02.RestConfModule.REVISION)); + } + + public Entry<SchemaInferenceStack, ContainerSchemaNode> getRestconfModuleErrorsSchemaNode() { + checkPreconditions(); + + final var schema = globalSchema; + final var namespace = QNameModule.create(XMLNamespace.of(Draft02.RestConfModule.NAMESPACE), + Revision.of(Draft02.RestConfModule.REVISION)); + if (schema.findModule(namespace).isEmpty()) { + return null; + } + + final var stack = SchemaInferenceStack.of(globalSchema); + stack.enterGrouping(RestConfModule.ERRORS_QNAME); + final var stmt = stack.enterSchemaTree(RestConfModule.ERRORS_QNAME); + verify(stmt instanceof ContainerSchemaNode, "Unexpected statement %s", stmt); + return Map.entry(stack, (ContainerSchemaNode) stmt); + } + + public DataSchemaNode getRestconfModuleRestConfSchemaNode(final Module inRestconfModule, + final String schemaNodeName) { + Module restconfModule = inRestconfModule; + if (restconfModule == null) { + restconfModule = getRestconfModule(); + } + + if (restconfModule == null) { + return null; + } + + final Collection<? extends GroupingDefinition> groupings = restconfModule.getGroupings(); + final Iterable<? extends GroupingDefinition> filteredGroups = Iterables.filter(groupings, + g -> RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE.equals(g.getQName().getLocalName())); + final GroupingDefinition restconfGrouping = Iterables.getFirst(filteredGroups, null); + + final var instanceDataChildrenByName = findInstanceDataChildrenByName(restconfGrouping, + RestConfModule.RESTCONF_CONTAINER_SCHEMA_NODE); + final DataSchemaNode restconfContainer = getFirst(instanceDataChildrenByName); + + if (RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) { + final var instances = findInstanceDataChildrenByName( + (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + return getFirst(instances); + } else if (RestConfModule.STREAM_LIST_SCHEMA_NODE.equals(schemaNodeName)) { + var instances = findInstanceDataChildrenByName( + (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modules = getFirst(instances); + instances = findInstanceDataChildrenByName((DataNodeContainer) modules, + RestConfModule.STREAM_LIST_SCHEMA_NODE); + return getFirst(instances); + } else if (RestConfModule.MODULES_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) { + final var instances = findInstanceDataChildrenByName( + (DataNodeContainer) restconfContainer, RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + return getFirst(instances); + } else if (RestConfModule.MODULE_LIST_SCHEMA_NODE.equals(schemaNodeName)) { + var instances = findInstanceDataChildrenByName( + (DataNodeContainer) restconfContainer, RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + final DataSchemaNode modules = getFirst(instances); + instances = findInstanceDataChildrenByName((DataNodeContainer) modules, + RestConfModule.MODULE_LIST_SCHEMA_NODE); + return getFirst(instances); + } else if (RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE.equals(schemaNodeName)) { + final var instances = findInstanceDataChildrenByName( + (DataNodeContainer) restconfContainer, RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + return getFirst(instances); + } + + return null; + } + + public static @Nullable DataSchemaNode getFirst(final List<FoundChild> children) { + return children.isEmpty() ? null : children.get(0).child; + } + + private static DataSchemaNode childByQName(final ChoiceSchemaNode container, final QName name) { + for (final CaseSchemaNode caze : container.getCases()) { + final DataSchemaNode ret = childByQName(caze, name); + if (ret != null) { + return ret; + } + } + + return null; + } + + private static DataSchemaNode childByQName(final CaseSchemaNode container, final QName name) { + return container.dataChildByName(name); + } + + private static DataSchemaNode childByQName(final ContainerSchemaNode container, final QName name) { + return dataNodeChildByQName(container, name); + } + + private static DataSchemaNode childByQName(final ListSchemaNode container, final QName name) { + return dataNodeChildByQName(container, name); + } + + private static DataSchemaNode childByQName(final Module container, final QName name) { + return dataNodeChildByQName(container, name); + } + + private static DataSchemaNode childByQName(final DataSchemaNode container, final QName name) { + return null; + } + + + private static DataSchemaNode childByQName(final Object container, final QName name) { + if (container instanceof CaseSchemaNode) { + return childByQName((CaseSchemaNode) container, name); + } else if (container instanceof ChoiceSchemaNode) { + return childByQName((ChoiceSchemaNode) container, name); + } else if (container instanceof ContainerSchemaNode) { + return childByQName((ContainerSchemaNode) container, name); + } else if (container instanceof ListSchemaNode) { + return childByQName((ListSchemaNode) container, name); + } else if (container instanceof DataSchemaNode) { + return childByQName((DataSchemaNode) container, name); + } else if (container instanceof Module) { + return childByQName((Module) container, name); + } else { + throw new IllegalArgumentException("Unhandled parameter types: " + + Arrays.asList(container, name).toString()); + } + } + + private static DataSchemaNode dataNodeChildByQName(final DataNodeContainer container, final QName name) { + final DataSchemaNode ret = container.dataChildByName(name); + if (ret == null) { + for (final DataSchemaNode node : container.getChildNodes()) { + if (node instanceof ChoiceSchemaNode) { + final ChoiceSchemaNode choiceNode = (ChoiceSchemaNode) node; + final DataSchemaNode childByQName = childByQName(choiceNode, name); + if (childByQName != null) { + return childByQName; + } + } + } + } + return ret; + } + + private String toUriString(final Object object, final LeafSchemaNode leafNode, final DOMMountPoint mount) + throws UnsupportedEncodingException { + final IllegalArgumentCodec<Object, Object> codec = RestCodec.from(leafNode.getType(), mount, this); + return object == null ? "" : URLEncoder.encode(codec.serialize(object).toString(), StandardCharsets.UTF_8); + } + + @SuppressFBWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification = "Unrecognised NullableDecl") + private InstanceIdentifierContext collectPathArguments(final InstanceIdentifierBuilder builder, + final Deque<QName> schemaPath, final List<String> strings, final DataNodeContainer parentNode, + final DOMMountPoint mountPoint, final boolean returnJustMountPoint) { + requireNonNull(strings); + + if (parentNode == null) { + return null; + } + + if (strings.isEmpty()) { + return createContext(builder.build(), (DataSchemaNode) parentNode, mountPoint, + mountPoint != null ? getModelContext(mountPoint) : globalSchema); + } + + final String head = strings.iterator().next(); + + if (head.isEmpty()) { + final List<String> remaining = strings.subList(1, strings.size()); + return collectPathArguments(builder, schemaPath, remaining, parentNode, mountPoint, returnJustMountPoint); + } + + final String nodeName = toNodeName(head); + final String moduleName = toModuleName(head); + + DataSchemaNode targetNode = null; + if (!Strings.isNullOrEmpty(moduleName)) { + if (MOUNT_MODULE.equals(moduleName) && MOUNT_NODE.equals(nodeName)) { + if (mountPoint != null) { + throw new RestconfDocumentedException("Restconf supports just one mount point in URI.", + ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED); + } + + if (mountService == null) { + throw new RestconfDocumentedException( + "MountService was not found. Finding behind mount points does not work.", + ErrorType.APPLICATION, ErrorTag.OPERATION_NOT_SUPPORTED); + } + + final YangInstanceIdentifier partialPath = dataNormalizer.toNormalized(builder.build()).getKey(); + final Optional<DOMMountPoint> mountOpt = mountService.getMountPoint(partialPath); + if (mountOpt.isEmpty()) { + LOG.debug("Instance identifier to missing mount point: {}", partialPath); + throw new RestconfDocumentedException("Mount point does not exist.", ErrorType.PROTOCOL, + ErrorTag.DATA_MISSING); + } + final DOMMountPoint mount = mountOpt.get(); + + final EffectiveModelContext mountPointSchema = getModelContext(mount); + if (mountPointSchema == null) { + throw new RestconfDocumentedException("Mount point does not contain any schema with modules.", + ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT); + } + + if (returnJustMountPoint || strings.size() == 1) { + return InstanceIdentifierContext.ofMountPointRoot(mount, mountPointSchema); + } + + final String moduleNameBehindMountPoint = toModuleName(strings.get(1)); + if (moduleNameBehindMountPoint == null) { + throw new RestconfDocumentedException( + "First node after mount point in URI has to be in format \"moduleName:nodeName\"", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final Iterator<? extends Module> it = mountPointSchema.findModules(moduleNameBehindMountPoint) + .iterator(); + if (!it.hasNext()) { + throw new RestconfDocumentedException("\"" + moduleNameBehindMountPoint + + "\" module does not exist in mount point.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + + final List<String> subList = strings.subList(1, strings.size()); + return collectPathArguments(YangInstanceIdentifier.builder(), new ArrayDeque<>(), subList, it.next(), + mount, returnJustMountPoint); + } + + Module module = null; + if (mountPoint == null) { + checkPreconditions(); + module = globalSchema.findModules(moduleName).stream().findFirst().orElse(null); + if (module == null) { + throw new RestconfDocumentedException("\"" + moduleName + "\" module does not exist.", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + } else { + final EffectiveModelContext schemaContext = getModelContext(mountPoint); + if (schemaContext != null) { + module = schemaContext.findModules(moduleName).stream().findFirst().orElse(null); + } else { + module = null; + } + if (module == null) { + throw new RestconfDocumentedException("\"" + moduleName + + "\" module does not exist in mount point.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + } + + final var found = findInstanceDataChildByNameAndNamespace(parentNode, nodeName, module.getNamespace()); + if (found == null) { + if (parentNode instanceof Module) { + final RpcDefinition rpc; + if (mountPoint == null) { + rpc = getRpcDefinition(head, module.getRevision()); + } else { + rpc = getRpcDefinition(module, toNodeName(head)); + } + if (rpc != null) { + final var ctx = mountPoint == null ? globalSchema : getModelContext(mountPoint); + return InstanceIdentifierContext.ofRpcInput(ctx, rpc, mountPoint); + } + } + throw new RestconfDocumentedException("URI has bad format. Possible reasons:\n" + " 1. \"" + head + + "\" was not found in parent data node.\n" + " 2. \"" + head + + "\" is behind mount point. Then it should be in format \"/" + MOUNT + "/" + head + "\".", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + targetNode = found.child; + schemaPath.addAll(found.intermediate); + schemaPath.add(targetNode.getQName()); + } else { + final var potentialSchemaNodes = findInstanceDataChildrenByName(parentNode, nodeName); + if (potentialSchemaNodes.size() > 1) { + final StringBuilder strBuilder = new StringBuilder(); + for (var potentialNodeSchema : potentialSchemaNodes) { + strBuilder.append(" ").append(potentialNodeSchema.child.getQName().getNamespace()).append("\n"); + } + + throw new RestconfDocumentedException( + "URI has bad format. Node \"" + + nodeName + "\" is added as augment from more than one module. " + + "Therefore the node must have module name " + + "and it has to be in format \"moduleName:nodeName\"." + + "\nThe node is added as augment from modules with namespaces:\n" + + strBuilder.toString(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + if (potentialSchemaNodes.isEmpty()) { + throw new RestconfDocumentedException("\"" + nodeName + "\" in URI was not found in parent data node", + ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + + final var found = potentialSchemaNodes.get(0); + targetNode = found.child; + schemaPath.addAll(found.intermediate); + schemaPath.add(targetNode.getQName()); + } + + if (!isListOrContainer(targetNode)) { + throw new RestconfDocumentedException("URI has bad format. Node \"" + head + + "\" must be Container or List yang type.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + int consumed = 1; + if (targetNode instanceof ListSchemaNode) { + final ListSchemaNode listNode = (ListSchemaNode) targetNode; + final int keysSize = listNode.getKeyDefinition().size(); + if (strings.size() - consumed < keysSize) { + throw new RestconfDocumentedException("Missing key for list \"" + listNode.getQName().getLocalName() + + "\".", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING); + } + + final List<String> uriKeyValues = strings.subList(consumed, consumed + keysSize); + final HashMap<QName, Object> keyValues = new HashMap<>(); + int index = 0; + for (final QName key : listNode.getKeyDefinition()) { + final String uriKeyValue = uriKeyValues.get(index); + if (uriKeyValue.equals(NULL_VALUE)) { + throw new RestconfDocumentedException("URI has bad format. List \"" + + listNode.getQName().getLocalName() + "\" cannot contain \"null\" value as a key.", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final var keyChild = listNode.getDataChildByName(key); + schemaPath.addLast(keyChild.getQName()); + addKeyValue(keyValues, schemaPath, keyChild, uriKeyValue, mountPoint); + schemaPath.removeLast(); + index++; + } + + consumed = consumed + index; + builder.nodeWithKey(targetNode.getQName(), keyValues); + } else { + builder.node(targetNode.getQName()); + } + + if (targetNode instanceof DataNodeContainer) { + final List<String> remaining = strings.subList(consumed, strings.size()); + return collectPathArguments(builder, schemaPath, remaining, (DataNodeContainer) targetNode, mountPoint, + returnJustMountPoint); + } + + return createContext(builder.build(), targetNode, mountPoint, + mountPoint != null ? getModelContext(mountPoint) : globalSchema); + } + + private static InstanceIdentifierContext createContext(final YangInstanceIdentifier instance, + final DataSchemaNode dataSchemaNode, final DOMMountPoint mountPoint, + final EffectiveModelContext schemaContext) { + final var normalized = new DataNormalizer(schemaContext).toNormalized(instance); + return InstanceIdentifierContext.ofPath(normalized.getValue(), dataSchemaNode, normalized.getKey(), mountPoint); + } + + public static @Nullable FoundChild findInstanceDataChildByNameAndNamespace(final DataNodeContainer container, + final String name, final XMLNamespace namespace) { + requireNonNull(namespace); + + for (var node : findInstanceDataChildrenByName(container, name)) { + if (namespace.equals(node.child.getQName().getNamespace())) { + return node; + } + } + return null; + } + + public static List<FoundChild> findInstanceDataChildrenByName(final DataNodeContainer container, + final String name) { + final List<FoundChild> instantiatedDataNodeContainers = new ArrayList<>(); + collectInstanceDataNodeContainers(instantiatedDataNodeContainers, requireNonNull(container), + requireNonNull(name), List.of()); + return instantiatedDataNodeContainers; + } + + private static void collectInstanceDataNodeContainers(final List<FoundChild> potentialSchemaNodes, + final DataNodeContainer container, final String name, final List<QName> intermediate) { + // We perform two iterations to retain breadth-first ordering + for (var child : container.getChildNodes()) { + if (name.equals(child.getQName().getLocalName()) && isInstantiatedDataSchema(child)) { + potentialSchemaNodes.add(new FoundChild(child, intermediate)); + } + } + + for (var child : container.getChildNodes()) { + if (child instanceof ChoiceSchemaNode) { + for (var caze : ((ChoiceSchemaNode) child).getCases()) { + collectInstanceDataNodeContainers(potentialSchemaNodes, caze, name, + ImmutableList.<QName>builderWithExpectedSize(intermediate.size() + 2) + .addAll(intermediate).add(child.getQName()).add(caze.getQName()) + .build()); + } + } + } + } + + public static boolean isInstantiatedDataSchema(final DataSchemaNode node) { + return node instanceof LeafSchemaNode || node instanceof LeafListSchemaNode + || node instanceof ContainerSchemaNode || node instanceof ListSchemaNode + || node instanceof AnyxmlSchemaNode; + } + + private void addKeyValue(final HashMap<QName, Object> map, final Deque<QName> schemaPath, final DataSchemaNode node, + final String uriValue, final DOMMountPoint mountPoint) { + checkArgument(node instanceof LeafSchemaNode); + + final EffectiveModelContext schemaContext = mountPoint == null ? globalSchema : getModelContext(mountPoint); + final String urlDecoded = urlPathArgDecode(requireNonNull(uriValue)); + TypeDefinition<?> typedef = ((LeafSchemaNode) node).getType(); + final TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(typedef); + if (baseType instanceof LeafrefTypeDefinition) { + final var stack = SchemaInferenceStack.of(schemaContext); + schemaPath.forEach(stack::enterSchemaTree); + typedef = stack.resolveLeafref((LeafrefTypeDefinition) baseType); + } + final IllegalArgumentCodec<Object, Object> codec = RestCodec.from(typedef, mountPoint, this); + Object decoded = codec.deserialize(urlDecoded); + String additionalInfo = ""; + if (decoded == null) { + if (typedef instanceof IdentityrefTypeDefinition) { + decoded = toQName(schemaContext, urlDecoded); + additionalInfo = + "For key which is of type identityref it should be in format module_name:identity_name."; + } + } + + if (decoded == null) { + throw new RestconfDocumentedException(uriValue + " from URI can't be resolved. " + additionalInfo, + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + map.put(node.getQName(), decoded); + } + + private static String toModuleName(final String str) { + final int idx = str.indexOf(':'); + if (idx == -1) { + return null; + } + + // Make sure there is only one occurrence + if (str.indexOf(':', idx + 1) != -1) { + return null; + } + + return str.substring(0, idx); + } + + private static String toNodeName(final String str) { + final int idx = str.indexOf(':'); + if (idx == -1) { + return str; + } + + // Make sure there is only one occurrence + if (str.indexOf(':', idx + 1) != -1) { + return str; + } + + return str.substring(idx + 1); + } + + private QName toQName(final EffectiveModelContext schemaContext, final String name, + final Optional<Revision> revisionDate) { + checkPreconditions(); + final String module = toModuleName(name); + final String node = toNodeName(name); + final Module m = schemaContext.findModule(module, revisionDate).orElse(null); + return m == null ? null : QName.create(m.getQNameModule(), node); + } + + private QName toQName(final EffectiveModelContext schemaContext, final String name) { + checkPreconditions(); + final String module = toModuleName(name); + final String node = toNodeName(name); + final Collection<? extends Module> modules = schemaContext.findModules(module); + return modules.isEmpty() ? null : QName.create(modules.iterator().next().getQNameModule(), node); + } + + private static boolean isListOrContainer(final DataSchemaNode node) { + return node instanceof ListSchemaNode || node instanceof ContainerSchemaNode; + } + + public RpcDefinition getRpcDefinition(final String name, final Optional<Revision> revisionDate) { + final QName validName = toQName(globalSchema, name, revisionDate); + return validName == null ? null : qnameToRpc.get().get(validName); + } + + public RpcDefinition getRpcDefinition(final String name) { + final QName validName = toQName(globalSchema, name); + return validName == null ? null : qnameToRpc.get().get(validName); + } + + private static RpcDefinition getRpcDefinition(final Module module, final String rpcName) { + final QName rpcQName = QName.create(module.getQNameModule(), rpcName); + for (final RpcDefinition rpcDefinition : module.getRpcs()) { + if (rpcQName.equals(rpcDefinition.getQName())) { + return rpcDefinition; + } + } + return null; + } + + @Override + public void onModelContextUpdated(final EffectiveModelContext context) { + if (context != null) { + final Collection<? extends RpcDefinition> defs = context.getOperations(); + final Map<QName, RpcDefinition> newMap = new HashMap<>(defs.size()); + + for (final RpcDefinition operation : defs) { + newMap.put(operation.getQName(), operation); + } + + // FIXME: still not completely atomic + qnameToRpc.set(ImmutableMap.copyOf(newMap)); + setGlobalSchema(context); + } + } + + private static List<String> urlPathArgsDecode(final Iterable<String> strings) { + final List<String> decodedPathArgs = new ArrayList<>(); + for (final String pathArg : strings) { + final String _decode = URLDecoder.decode(pathArg, StandardCharsets.UTF_8); + decodedPathArgs.add(_decode); + } + return decodedPathArgs; + } + + static String urlPathArgDecode(final String pathArg) { + if (pathArg == null) { + return null; + } + return URLDecoder.decode(pathArg, StandardCharsets.UTF_8); + } + + private String convertToRestconfIdentifier(final PathArgument argument, final DataSchemaNode node, + final DOMMountPoint mount) { + if (argument instanceof NodeIdentifier) { + return convertToRestconfIdentifier((NodeIdentifier) argument, mount); + } else if (argument instanceof NodeIdentifierWithPredicates && node instanceof ListSchemaNode) { + return convertToRestconfIdentifierWithPredicates((NodeIdentifierWithPredicates) argument, + (ListSchemaNode) node, mount); + } else if (argument != null && node != null) { + throw new IllegalArgumentException("Conversion of generic path argument is not supported"); + } else { + throw new IllegalArgumentException("Unhandled parameter types: " + Arrays.asList(argument, node)); + } + } + + private String convertToRestconfIdentifier(final NodeIdentifier argument, final DOMMountPoint node) { + return "/" + toRestconfIdentifier(argument.getNodeType(), node); + } + + private String convertToRestconfIdentifierWithPredicates(final NodeIdentifierWithPredicates argument, + final ListSchemaNode node, final DOMMountPoint mount) { + final QName nodeType = argument.getNodeType(); + final String nodeIdentifier = toRestconfIdentifier(nodeType, mount); + + final StringBuilder builder = new StringBuilder().append('/').append(nodeIdentifier).append('/'); + + final List<QName> keyDefinition = node.getKeyDefinition(); + boolean hasElements = false; + for (final QName key : keyDefinition) { + for (final DataSchemaNode listChild : node.getChildNodes()) { + if (listChild.getQName().equals(key)) { + if (!hasElements) { + hasElements = true; + } else { + builder.append('/'); + } + + checkState(listChild instanceof LeafSchemaNode, + "List key has to consist of leaves, not %s", listChild); + + final Object value = argument.getValue(key); + try { + builder.append(toUriString(value, (LeafSchemaNode)listChild, mount)); + } catch (final UnsupportedEncodingException e) { + LOG.error("Error parsing URI: {}", value, e); + return null; + } + break; + } + } + } + + return builder.toString(); + } + + public YangInstanceIdentifier toXpathRepresentation(final YangInstanceIdentifier instanceIdentifier) { + if (dataNormalizer == null) { + throw new RestconfDocumentedException("Data normalizer isn't set. Normalization isn't possible"); + } + + try { + return dataNormalizer.toLegacy(instanceIdentifier); + } catch (final DataNormalizationException e) { + throw new RestconfDocumentedException("Data normalizer failed. Normalization isn't possible", e); + } + } + + public boolean isNodeMixin(final YangInstanceIdentifier path) { + final DataNormalizationOperation<?> operation; + try { + operation = dataNormalizer.getOperation(path); + } catch (final DataNormalizationException e) { + throw new RestconfDocumentedException("Data normalizer failed. Normalization isn't possible", e); + } + return operation.isMixin(); + } + + private static EffectiveModelContext getModelContext(final DOMMountPoint mountPoint) { + return mountPoint.getService(DOMSchemaService.class) + .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext())) + .orElse(null); + } + + public static final class FoundChild { + // Intermediate schema tree children, usually empty + public final @NonNull List<QName> intermediate; + public final @NonNull DataSchemaNode child; + + private FoundChild(final DataSchemaNode child, final List<QName> intermediate) { + this.child = requireNonNull(child); + this.intermediate = requireNonNull(intermediate); + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationException.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationException.java new file mode 100644 index 0000000..b9e7214 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationException.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +final class DataNormalizationException extends Exception { + private static final long serialVersionUID = 1L; + + DataNormalizationException(final String message) { + super(message); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationOperation.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationOperation.java new file mode 100644 index 0000000..eb41c78 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizationOperation.java @@ -0,0 +1,526 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static com.google.common.base.Verify.verifyNotNull; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.eclipse.jdt.annotation.Nullable; +import org.opendaylight.yangtools.concepts.Identifiable; +import org.opendaylight.yangtools.yang.common.Empty; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode; +import org.opendaylight.yangtools.yang.model.api.AnyxmlSchemaNode; +import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode; +import org.opendaylight.yangtools.yang.model.api.AugmentationTarget; +import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ContainerLike; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.util.EffectiveAugmentationSchema; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; + +abstract class DataNormalizationOperation<T extends PathArgument> implements Identifiable<T> { + private final T identifier; + + DataNormalizationOperation(final T identifier) { + this.identifier = identifier; + } + + static DataNormalizationOperation<?> from(final EffectiveModelContext ctx) { + return new ContainerNormalization(ctx); + } + + @Override + public T getIdentifier() { + return identifier; + } + + boolean isMixin() { + return false; + } + + Set<QName> getQNameIdentifiers() { + return ImmutableSet.of(identifier.getNodeType()); + } + + abstract DataNormalizationOperation<?> getChild(PathArgument child) throws DataNormalizationException; + + abstract DataNormalizationOperation<?> getChild(QName child) throws DataNormalizationException; + + abstract DataNormalizationOperation<?> enterChild(QName child, SchemaInferenceStack stack) + throws DataNormalizationException; + + abstract DataNormalizationOperation<?> enterChild(PathArgument child, SchemaInferenceStack stack) + throws DataNormalizationException; + + void pushToStack(final SchemaInferenceStack stack) { + // Accurate for most subclasses + stack.enterSchemaTree(getIdentifier().getNodeType()); + } + + private abstract static class SimpleTypeNormalization<T extends PathArgument> + extends DataNormalizationOperation<T> { + SimpleTypeNormalization(final T identifier) { + super(identifier); + } + + @Override + final DataNormalizationOperation<?> getChild(final PathArgument child) { + return null; + } + + @Override + final DataNormalizationOperation<?> getChild(final QName child) { + return null; + } + + @Override + final DataNormalizationOperation<?> enterChild(final QName child, final SchemaInferenceStack stack) { + return null; + } + + @Override + final DataNormalizationOperation<?> enterChild(final PathArgument child, final SchemaInferenceStack stack) { + return null; + } + } + + private static final class LeafNormalization extends SimpleTypeNormalization<NodeIdentifier> { + LeafNormalization(final LeafSchemaNode potential) { + super(new NodeIdentifier(potential.getQName())); + } + } + + private static final class LeafListEntryNormalization extends SimpleTypeNormalization<NodeWithValue> { + LeafListEntryNormalization(final LeafListSchemaNode potential) { + super(new NodeWithValue<>(potential.getQName(), Empty.value())); + } + + @Override + protected void pushToStack(final SchemaInferenceStack stack) { + // No-op + } + } + + private abstract static class DataContainerNormalizationOperation<T extends PathArgument> + extends DataNormalizationOperation<T> { + private final DataNodeContainer schema; + private final Map<QName, DataNormalizationOperation<?>> byQName = new ConcurrentHashMap<>(); + private final Map<PathArgument, DataNormalizationOperation<?>> byArg = new ConcurrentHashMap<>(); + + DataContainerNormalizationOperation(final T identifier, final DataNodeContainer schema) { + super(identifier); + this.schema = schema; + } + + @Override + DataNormalizationOperation<?> getChild(final PathArgument child) throws DataNormalizationException { + DataNormalizationOperation<?> potential = byArg.get(child); + if (potential != null) { + return potential; + } + potential = fromLocalSchema(child); + return register(potential); + } + + @Override + DataNormalizationOperation<?> getChild(final QName child) throws DataNormalizationException { + DataNormalizationOperation<?> potential = byQName.get(child); + if (potential != null) { + return potential; + } + potential = fromLocalSchemaAndQName(schema, child); + return register(potential); + } + + @Override + final DataNormalizationOperation<?> enterChild(final QName child, final SchemaInferenceStack stack) + throws DataNormalizationException { + return pushToStack(getChild(child), stack); + } + + @Override + final DataNormalizationOperation<?> enterChild(final PathArgument child, final SchemaInferenceStack stack) + throws DataNormalizationException { + return pushToStack(getChild(child), stack); + } + + private static DataNormalizationOperation<?> pushToStack(final DataNormalizationOperation<?> child, + final SchemaInferenceStack stack) { + if (child != null) { + child.pushToStack(stack); + } + return child; + } + + private DataNormalizationOperation<?> fromLocalSchema(final PathArgument child) + throws DataNormalizationException { + if (child instanceof AugmentationIdentifier) { + return fromSchemaAndQNameChecked(schema, ((AugmentationIdentifier) child).getPossibleChildNames() + .iterator().next()); + } + return fromSchemaAndQNameChecked(schema, child.getNodeType()); + } + + DataNormalizationOperation<?> fromLocalSchemaAndQName(final DataNodeContainer schema2, + final QName child) throws DataNormalizationException { + return fromSchemaAndQNameChecked(schema2, child); + } + + private DataNormalizationOperation<?> register(final DataNormalizationOperation<?> potential) { + if (potential != null) { + byArg.put(potential.getIdentifier(), potential); + for (final QName qname : potential.getQNameIdentifiers()) { + byQName.put(qname, potential); + } + } + return potential; + } + + private static DataNormalizationOperation<?> fromSchemaAndQNameChecked(final DataNodeContainer schema, + final QName child) throws DataNormalizationException { + + final DataSchemaNode result = findChildSchemaNode(schema, child); + if (result == null) { + throw new DataNormalizationException(String.format( + "Supplied QName %s is not valid according to schema %s, potential children nodes: %s", child, + schema,schema.getChildNodes())); + } + + // We try to look up if this node was added by augmentation + if (schema instanceof DataSchemaNode && result.isAugmenting()) { + return fromAugmentation(schema, (AugmentationTarget) schema, result); + } + return fromDataSchemaNode(result); + } + } + + private static final class ListItemNormalization extends + DataContainerNormalizationOperation<NodeIdentifierWithPredicates> { + ListItemNormalization(final NodeIdentifierWithPredicates identifier, final ListSchemaNode schema) { + super(identifier, schema); + } + + @Override + protected void pushToStack(final SchemaInferenceStack stack) { + // No-op + } + } + + private static final class UnkeyedListItemNormalization + extends DataContainerNormalizationOperation<NodeIdentifier> { + UnkeyedListItemNormalization(final ListSchemaNode schema) { + super(new NodeIdentifier(schema.getQName()), schema); + } + + @Override + protected void pushToStack(final SchemaInferenceStack stack) { + // No-op + } + } + + private static final class ContainerNormalization extends DataContainerNormalizationOperation<NodeIdentifier> { + ContainerNormalization(final ContainerLike schema) { + super(new NodeIdentifier(schema.getQName()), schema); + } + } + + private abstract static class MixinNormalizationOp<T extends PathArgument> extends DataNormalizationOperation<T> { + MixinNormalizationOp(final T identifier) { + super(identifier); + } + + @Override + final boolean isMixin() { + return true; + } + } + + private abstract static class ListLikeNormalizationOp<T extends PathArgument> extends MixinNormalizationOp<T> { + ListLikeNormalizationOp(final T identifier) { + super(identifier); + } + + @Override + protected final DataNormalizationOperation<?> enterChild(final QName child, final SchemaInferenceStack stack) + throws DataNormalizationException { + // Stack is already pointing to the corresponding statement, now we are just working with the child + return getChild(child); + } + + @Override + protected final DataNormalizationOperation<?> enterChild(final PathArgument child, + final SchemaInferenceStack stack) throws DataNormalizationException { + return getChild(child); + } + } + + private static final class LeafListMixinNormalization extends ListLikeNormalizationOp<NodeIdentifier> { + private final DataNormalizationOperation<?> innerOp; + + LeafListMixinNormalization(final LeafListSchemaNode potential) { + super(new NodeIdentifier(potential.getQName())); + innerOp = new LeafListEntryNormalization(potential); + } + + @Override + DataNormalizationOperation<?> getChild(final PathArgument child) { + if (child instanceof NodeWithValue) { + return innerOp; + } + return null; + } + + @Override + DataNormalizationOperation<?> getChild(final QName child) { + if (getIdentifier().getNodeType().equals(child)) { + return innerOp; + } + return null; + } + } + + private static final class AugmentationNormalization + extends DataContainerNormalizationOperation<AugmentationIdentifier> { + + AugmentationNormalization(final AugmentationSchemaNode augmentation, final DataNodeContainer schema) { + super(DataSchemaContextNode.augmentationIdentifierFrom(augmentation), + new EffectiveAugmentationSchema(augmentation, schema)); + } + + @Override + boolean isMixin() { + return true; + } + + @Override + DataNormalizationOperation<?> fromLocalSchemaAndQName(final DataNodeContainer schema, final QName child) { + final DataSchemaNode result = findChildSchemaNode(schema, child); + if (result == null) { + return null; + } + + // We try to look up if this node was added by augmentation + if (schema instanceof DataSchemaNode && result.isAugmenting()) { + return fromAugmentation(schema, (AugmentationTarget) schema, result); + } + return fromDataSchemaNode(result); + } + + @Override + Set<QName> getQNameIdentifiers() { + return getIdentifier().getPossibleChildNames(); + } + + @Override + void pushToStack(final SchemaInferenceStack stack) { + // No-op + } + } + + private static final class MapMixinNormalization extends ListLikeNormalizationOp<NodeIdentifier> { + private final ListItemNormalization innerNode; + + MapMixinNormalization(final ListSchemaNode list) { + super(new NodeIdentifier(list.getQName())); + innerNode = new ListItemNormalization(NodeIdentifierWithPredicates.of(list.getQName()), list); + } + + @Override + DataNormalizationOperation<?> getChild(final PathArgument child) { + if (child.getNodeType().equals(getIdentifier().getNodeType())) { + return innerNode; + } + return null; + } + + @Override + DataNormalizationOperation<?> getChild(final QName child) { + if (getIdentifier().getNodeType().equals(child)) { + return innerNode; + } + return null; + } + } + + private static final class UnkeyedListMixinNormalization extends ListLikeNormalizationOp<NodeIdentifier> { + private final UnkeyedListItemNormalization innerNode; + + UnkeyedListMixinNormalization(final ListSchemaNode list) { + super(new NodeIdentifier(list.getQName())); + innerNode = new UnkeyedListItemNormalization(list); + } + + @Override + DataNormalizationOperation<?> getChild(final PathArgument child) { + if (child.getNodeType().equals(getIdentifier().getNodeType())) { + return innerNode; + } + return null; + } + + @Override + DataNormalizationOperation<?> getChild(final QName child) { + if (getIdentifier().getNodeType().equals(child)) { + return innerNode; + } + return null; + } + } + + private static final class ChoiceNodeNormalization extends MixinNormalizationOp<NodeIdentifier> { + private final ImmutableMap<QName, DataNormalizationOperation<?>> byQName; + private final ImmutableMap<PathArgument, DataNormalizationOperation<?>> byArg; + private final ImmutableMap<DataNormalizationOperation<?>, QName> childToCase; + + ChoiceNodeNormalization(final ChoiceSchemaNode schema) { + super(new NodeIdentifier(schema.getQName())); + ImmutableMap.Builder<DataNormalizationOperation<?>, QName> childToCaseBuilder = ImmutableMap.builder(); + final ImmutableMap.Builder<QName, DataNormalizationOperation<?>> byQNameBuilder = ImmutableMap.builder(); + final ImmutableMap.Builder<PathArgument, DataNormalizationOperation<?>> byArgBuilder = + ImmutableMap.builder(); + + for (final CaseSchemaNode caze : schema.getCases()) { + for (final DataSchemaNode cazeChild : caze.getChildNodes()) { + final DataNormalizationOperation<?> childOp = fromDataSchemaNode(cazeChild); + byArgBuilder.put(childOp.getIdentifier(), childOp); + childToCaseBuilder.put(childOp, caze.getQName()); + for (final QName qname : childOp.getQNameIdentifiers()) { + byQNameBuilder.put(qname, childOp); + } + } + } + childToCase = childToCaseBuilder.build(); + byQName = byQNameBuilder.build(); + byArg = byArgBuilder.build(); + } + + @Override + DataNormalizationOperation<?> getChild(final PathArgument child) { + return byArg.get(child); + } + + @Override + DataNormalizationOperation<?> getChild(final QName child) { + return byQName.get(child); + } + + @Override + Set<QName> getQNameIdentifiers() { + return byQName.keySet(); + } + + @Override + DataNormalizationOperation<?> enterChild(final QName child, final SchemaInferenceStack stack) { + return pushToStack(getChild(child), stack); + } + + @Override + DataNormalizationOperation<?> enterChild(final PathArgument child, final SchemaInferenceStack stack) { + return pushToStack(getChild(child), stack); + } + + @Override + void pushToStack(final SchemaInferenceStack stack) { + stack.enterChoice(getIdentifier().getNodeType()); + } + + private DataNormalizationOperation<?> pushToStack(final DataNormalizationOperation<?> child, + final SchemaInferenceStack stack) { + if (child != null) { + final var caseName = verifyNotNull(childToCase.get(child), "No case statement for %s in %s", child, + this); + stack.enterSchemaTree(caseName); + child.pushToStack(stack); + } + return child; + } + } + + private static final class AnyxmlNormalization extends SimpleTypeNormalization<NodeIdentifier> { + AnyxmlNormalization(final AnyxmlSchemaNode schema) { + super(new NodeIdentifier(schema.getQName())); + } + } + + private static @Nullable DataSchemaNode findChildSchemaNode(final DataNodeContainer parent, final QName child) { + final DataSchemaNode potential = parent.dataChildByName(child); + return potential != null ? potential : findChoice(parent, child); + } + + private static @Nullable ChoiceSchemaNode findChoice(final DataNodeContainer parent, final QName child) { + for (final ChoiceSchemaNode choice : Iterables.filter(parent.getChildNodes(), ChoiceSchemaNode.class)) { + for (final CaseSchemaNode caze : choice.getCases()) { + if (findChildSchemaNode(caze, child) != null) { + return choice; + } + } + } + return null; + } + + /** + * Returns a DataNormalizationOperation for provided child node. + * + * <p> + * If supplied child is added by Augmentation this operation returns + * a DataNormalizationOperation for augmentation, + * otherwise returns a DataNormalizationOperation for child as + * call for {@link #fromDataSchemaNode(DataSchemaNode)}. + */ + private static DataNormalizationOperation<?> fromAugmentation(final DataNodeContainer parent, + final AugmentationTarget parentAug, final DataSchemaNode child) { + for (final AugmentationSchemaNode aug : parentAug.getAvailableAugmentations()) { + if (aug.dataChildByName(child.getQName()) != null) { + return new AugmentationNormalization(aug, parent); + } + } + return fromDataSchemaNode(child); + } + + static DataNormalizationOperation<?> fromDataSchemaNode(final DataSchemaNode potential) { + if (potential instanceof ContainerSchemaNode) { + return new ContainerNormalization((ContainerSchemaNode) potential); + } else if (potential instanceof ListSchemaNode) { + return fromListSchemaNode((ListSchemaNode) potential); + } else if (potential instanceof LeafSchemaNode) { + return new LeafNormalization((LeafSchemaNode) potential); + } else if (potential instanceof ChoiceSchemaNode) { + return new ChoiceNodeNormalization((ChoiceSchemaNode) potential); + } else if (potential instanceof LeafListSchemaNode) { + return new LeafListMixinNormalization((LeafListSchemaNode) potential); + } else if (potential instanceof AnyxmlSchemaNode) { + return new AnyxmlNormalization((AnyxmlSchemaNode) potential); + } + return null; + } + + private static DataNormalizationOperation<?> fromListSchemaNode(final ListSchemaNode potential) { + if (potential.getKeyDefinition().isEmpty()) { + return new UnkeyedListMixinNormalization(potential); + } + return new MapMixinNormalization(potential); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizer.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizer.java new file mode 100644 index 0000000..334a6c7 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/DataNormalizer.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static java.util.Objects.requireNonNull; + +import com.google.common.collect.ImmutableList; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; + +class DataNormalizer { + private final DataNormalizationOperation<?> operation; + private final EffectiveModelContext context; + + DataNormalizer(final EffectiveModelContext ctx) { + context = requireNonNull(ctx); + operation = DataNormalizationOperation.from(ctx); + } + + Entry<YangInstanceIdentifier, SchemaInferenceStack> toNormalized(final YangInstanceIdentifier legacy) { + List<PathArgument> normalizedArgs = new ArrayList<>(); + + DataNormalizationOperation<?> currentOp = operation; + Iterator<PathArgument> arguments = legacy.getPathArguments().iterator(); + SchemaInferenceStack stack = SchemaInferenceStack.of(context); + + try { + while (arguments.hasNext()) { + PathArgument legacyArg = arguments.next(); + currentOp = currentOp.enterChild(legacyArg, stack); + checkArgument(currentOp != null, + "Legacy Instance Identifier %s is not correct. Normalized Instance Identifier so far %s", + legacy, normalizedArgs); + while (currentOp.isMixin()) { + normalizedArgs.add(currentOp.getIdentifier()); + currentOp = currentOp.enterChild(legacyArg.getNodeType(), stack); + } + normalizedArgs.add(legacyArg); + } + } catch (DataNormalizationException e) { + throw new IllegalArgumentException("Failed to normalize path " + legacy, e); + } + + return Map.entry(YangInstanceIdentifier.create(normalizedArgs), stack); + } + + DataNormalizationOperation<?> getOperation(final YangInstanceIdentifier legacy) + throws DataNormalizationException { + DataNormalizationOperation<?> currentOp = operation; + + for (PathArgument pathArgument : legacy.getPathArguments()) { + currentOp = currentOp.getChild(pathArgument); + } + return currentOp; + } + + YangInstanceIdentifier toLegacy(final YangInstanceIdentifier normalized) throws DataNormalizationException { + ImmutableList.Builder<PathArgument> legacyArgs = ImmutableList.builder(); + DataNormalizationOperation<?> currentOp = operation; + for (PathArgument normalizedArg : normalized.getPathArguments()) { + currentOp = currentOp.getChild(normalizedArg); + if (!currentOp.isMixin()) { + legacyArgs.add(normalizedArg); + } + } + return YangInstanceIdentifier.create(legacyArgs.build()); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java new file mode 100644 index 0000000..296fd91 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/JSONRestconfServiceImpl.java @@ -0,0 +1,291 @@ +/* + * Copyright (c) 2015 Brocade Communications Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static java.util.Objects.requireNonNull; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.annotation.Annotation; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Optional; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.rest.impl.JsonNormalizedNodeBodyReader; +import org.opendaylight.netconf.sal.rest.impl.JsonToPatchBodyReader; +import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeContext; +import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeJsonBodyWriter; +import org.opendaylight.netconf.sal.rest.impl.PatchJsonBodyWriter; +import org.opendaylight.netconf.sal.restconf.api.JSONRestconfService; +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.common.util.SimpleUriInfo; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.OperationFailedException; +import org.opendaylight.yangtools.yang.common.RpcError; +import org.opendaylight.yangtools.yang.common.RpcResultBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Implementation of the JSONRestconfService interface using the restconf Draft02 implementation. + * + * @author Thomas Pantelis + * @deprecated Replaced by {JSONRestconfServiceRfc8040Impl from restconf-nb-rfc8040 + */ +@Singleton +@Deprecated +public class JSONRestconfServiceImpl implements JSONRestconfService { + private static final Logger LOG = LoggerFactory.getLogger(JSONRestconfServiceImpl.class); + + private static final Annotation[] EMPTY_ANNOTATIONS = new Annotation[0]; + + private final ControllerContext controllerContext; + private final RestconfService restconfService; + + @Inject + public JSONRestconfServiceImpl(final ControllerContext controllerContext, final RestconfImpl restconfService) { + this.controllerContext = controllerContext; + this.restconfService = restconfService; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public void put(final String uriPath, final String payload) throws OperationFailedException { + requireNonNull(payload, "payload can't be null"); + + LOG.debug("put: uriPath: {}, payload: {}", uriPath, payload); + + final InputStream entityStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)); + final NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, false, + controllerContext); + + LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", context.getData()); + + try { + restconfService.updateConfigurationData(uriPath, context, new SimpleUriInfo(uriPath)); + } catch (final Exception e) { + propagateExceptionAs(uriPath, e, "PUT"); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public void post(final String uriPath, final String payload) throws OperationFailedException { + requireNonNull(payload, "payload can't be null"); + + LOG.debug("post: uriPath: {}, payload: {}", uriPath, payload); + + final InputStream entityStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)); + final NormalizedNodeContext context = JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true, + controllerContext); + + LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", context.getData()); + + try { + restconfService.createConfigurationData(uriPath, context, new SimpleUriInfo(uriPath)); + } catch (final Exception e) { + propagateExceptionAs(uriPath, e, "POST"); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public void delete(final String uriPath) throws OperationFailedException { + LOG.debug("delete: uriPath: {}", uriPath); + + try { + restconfService.deleteConfigurationData(uriPath); + } catch (final Exception e) { + propagateExceptionAs(uriPath, e, "DELETE"); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Optional<String> get(final String uriPath, final LogicalDatastoreType datastoreType) + throws OperationFailedException { + LOG.debug("get: uriPath: {}", uriPath); + + try { + NormalizedNodeContext readData; + final SimpleUriInfo uriInfo = new SimpleUriInfo(uriPath); + if (datastoreType == LogicalDatastoreType.CONFIGURATION) { + readData = restconfService.readConfigurationData(uriPath, uriInfo); + } else { + readData = restconfService.readOperationalData(uriPath, uriInfo); + } + + final Optional<String> result = Optional.of(toJson(readData)); + + LOG.debug("get returning: {}", result.get()); + + return result; + } catch (final Exception e) { + if (!isDataMissing(e)) { + propagateExceptionAs(uriPath, e, "GET"); + } + + LOG.debug("Data missing - returning absent"); + return Optional.empty(); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @SuppressFBWarnings(value = "NP_NULL_PARAM_DEREF", justification = "Unrecognised NullableDecl") + @Override + public Optional<String> invokeRpc(final String uriPath, final Optional<String> input) + throws OperationFailedException { + requireNonNull(uriPath, "uriPath can't be null"); + + final String actualInput = input.isPresent() ? input.get() : null; + + LOG.debug("invokeRpc: uriPath: {}, input: {}", uriPath, actualInput); + + String output = null; + try { + NormalizedNodeContext outputContext; + if (actualInput != null) { + final InputStream entityStream = new ByteArrayInputStream(actualInput.getBytes(StandardCharsets.UTF_8)); + final NormalizedNodeContext inputContext = + JsonNormalizedNodeBodyReader.readFrom(uriPath, entityStream, true, controllerContext); + + LOG.debug("Parsed YangInstanceIdentifier: {}", inputContext.getInstanceIdentifierContext() + .getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", inputContext.getData()); + + outputContext = restconfService.invokeRpc(uriPath, inputContext, null); + } else { + outputContext = restconfService.invokeRpc(uriPath, null, null); + } + + if (outputContext.getData() != null) { + output = toJson(outputContext); + } + } catch (final RuntimeException | IOException e) { + propagateExceptionAs(uriPath, e, "RPC"); + } + + return Optional.ofNullable(output); + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Optional<String> patch(final String uriPath, final String payload) + throws OperationFailedException { + + String output = null; + requireNonNull(payload, "payload can't be null"); + + LOG.debug("patch: uriPath: {}, payload: {}", uriPath, payload); + + final InputStream entityStream = new ByteArrayInputStream(payload.getBytes(StandardCharsets.UTF_8)); + + JsonToPatchBodyReader jsonToPatchBodyReader = new JsonToPatchBodyReader(controllerContext); + final PatchContext context = jsonToPatchBodyReader.readFrom(uriPath, entityStream); + + LOG.debug("Parsed YangInstanceIdentifier: {}", context.getInstanceIdentifierContext().getInstanceIdentifier()); + LOG.debug("Parsed NormalizedNode: {}", context.getData()); + + try { + PatchStatusContext patchStatusContext = restconfService + .patchConfigurationData(context, new SimpleUriInfo(uriPath)); + output = toJson(patchStatusContext); + } catch (final Exception e) { + propagateExceptionAs(uriPath, e, "PATCH"); + } + return Optional.ofNullable(output); + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Optional<String> subscribeToStream(final String identifier, final MultivaluedMap<String, String> params) + throws OperationFailedException { + //Note: We use http://127.0.0.1 because the Uri parser requires something there though it does nothing + String uri = new StringBuilder("http://127.0.0.1:8081/restconf/streams/stream/").append(identifier).toString(); + MultivaluedMap queryParams = params != null ? params : new MultivaluedHashMap<String, String>(); + UriInfo uriInfo = new SimpleUriInfo(uri, queryParams); + + String jsonRes = null; + try { + NormalizedNodeContext res = restconfService.subscribeToStream(identifier, uriInfo); + jsonRes = toJson(res); + } catch (final Exception e) { + propagateExceptionAs(identifier, e, "RPC"); + } + + return Optional.ofNullable(jsonRes); + } + + private static String toJson(final PatchStatusContext patchStatusContext) throws IOException { + final PatchJsonBodyWriter writer = new PatchJsonBodyWriter(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writer.writeTo(patchStatusContext, PatchStatusContext.class, null, EMPTY_ANNOTATIONS, + MediaType.APPLICATION_JSON_TYPE, null, outputStream); + return outputStream.toString(StandardCharsets.UTF_8); + } + + private static String toJson(final NormalizedNodeContext readData) throws IOException { + final NormalizedNodeJsonBodyWriter writer = new NormalizedNodeJsonBodyWriter(); + final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + writer.writeTo(readData, NormalizedNodeContext.class, null, EMPTY_ANNOTATIONS, + MediaType.APPLICATION_JSON_TYPE, null, outputStream); + return outputStream.toString(StandardCharsets.UTF_8); + } + + private static boolean isDataMissing(final Exception exception) { + boolean dataMissing = false; + if (exception instanceof RestconfDocumentedException) { + final RestconfDocumentedException rde = (RestconfDocumentedException)exception; + if (!rde.getErrors().isEmpty()) { + if (rde.getErrors().get(0).getErrorTag() == ErrorTag.DATA_MISSING) { + dataMissing = true; + } + } + } + + return dataMissing; + } + + private static void propagateExceptionAs(final String uriPath, final Exception exception, final String operation) + throws OperationFailedException { + LOG.debug("Error for uriPath: {}", uriPath, exception); + + if (exception instanceof RestconfDocumentedException) { + throw new OperationFailedException(String.format( + "%s failed for URI %s", operation, uriPath), exception.getCause(), + toRpcErrors(((RestconfDocumentedException)exception).getErrors())); + } + + throw new OperationFailedException(String.format("%s failed for URI %s", operation, uriPath), exception); + } + + private static RpcError[] toRpcErrors(final List<RestconfError> from) { + final RpcError[] to = new RpcError[from.size()]; + int index = 0; + for (final RestconfError e: from) { + to[index++] = RpcResultBuilder.newError(e.getErrorType(), e.getErrorTag(), e.getErrorMessage()); + } + + return to; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/NormalizedDataPrunner.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/NormalizedDataPrunner.java new file mode 100644 index 0000000..975da60 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/NormalizedDataPrunner.java @@ -0,0 +1,143 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode; +import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode; +import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.MixinNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.UnkeyedListNode; +import org.opendaylight.yangtools.yang.data.api.schema.UserMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; + +class NormalizedDataPrunner { + + public DataContainerChild pruneDataAtDepth(final DataContainerChild node, final Integer depth) { + if (depth == null) { + return node; + } + + if (node instanceof LeafNode || node instanceof LeafSetNode || node instanceof AnyxmlNode) { + return node; + } else if (node instanceof MixinNode) { + return processMixinNode(node, depth); + } else if (node instanceof DataContainerNode) { + return processContainerNode(node, depth); + } + throw new IllegalStateException("Unexpected Mixin node occured why pruning data to requested depth"); + } + + private DataContainerChild processMixinNode(final NormalizedNode node, final Integer depth) { + if (node instanceof AugmentationNode) { + return processAugmentationNode(node, depth); + } else if (node instanceof ChoiceNode) { + return processChoiceNode(node, depth); + } else if (node instanceof UserMapNode) { + return processOrderedMapNode(node, depth); + } else if (node instanceof MapNode) { + return processMapNode(node, depth); + } else if (node instanceof UnkeyedListNode) { + return processUnkeyedListNode(node, depth); + } + throw new IllegalStateException("Unexpected Mixin node occured why pruning data to requested depth"); + } + + private DataContainerChild processContainerNode(final NormalizedNode node, final Integer depth) { + final ContainerNode containerNode = (ContainerNode) node; + DataContainerNodeBuilder<NodeIdentifier, ContainerNode> newContainerBuilder = Builders.containerBuilder() + .withNodeIdentifier(containerNode.getIdentifier()); + if (depth > 1) { + processDataContainerChild((DataContainerNode) node, depth, newContainerBuilder); + } + return newContainerBuilder.build(); + } + + private DataContainerChild processChoiceNode(final NormalizedNode node, final Integer depth) { + final ChoiceNode choiceNode = (ChoiceNode) node; + DataContainerNodeBuilder<NodeIdentifier, ChoiceNode> newChoiceBuilder = Builders.choiceBuilder() + .withNodeIdentifier(choiceNode.getIdentifier()); + + processDataContainerChild((DataContainerNode) node, depth, newChoiceBuilder); + + return newChoiceBuilder.build(); + } + + private DataContainerChild processAugmentationNode(final NormalizedNode node, final Integer depth) { + final AugmentationNode augmentationNode = (AugmentationNode) node; + DataContainerNodeBuilder<AugmentationIdentifier, ? extends DataContainerChild> newAugmentationBuilder = + Builders.augmentationBuilder().withNodeIdentifier(augmentationNode.getIdentifier()); + + processDataContainerChild((DataContainerNode) node, depth, newAugmentationBuilder); + + return newAugmentationBuilder.build(); + } + + private void processDataContainerChild(final DataContainerNode node, final Integer depth, + final DataContainerNodeBuilder<?, ?> newBuilder) { + for (DataContainerChild nodeValue : node.body()) { + newBuilder.withChild(pruneDataAtDepth(nodeValue, depth - 1)); + } + } + + private DataContainerChild processUnkeyedListNode(final NormalizedNode node, final Integer depth) { + CollectionNodeBuilder<UnkeyedListEntryNode, UnkeyedListNode> newUnkeyedListBuilder = Builders + .unkeyedListBuilder(); + if (depth > 1) { + for (UnkeyedListEntryNode oldUnkeyedListEntry : ((UnkeyedListNode) node).body()) { + DataContainerNodeBuilder<NodeIdentifier, UnkeyedListEntryNode> newUnkeyedListEntry = Builders + .unkeyedListEntryBuilder().withNodeIdentifier(oldUnkeyedListEntry.getIdentifier()); + for (DataContainerChild oldUnkeyedListEntryValue : oldUnkeyedListEntry.body()) { + newUnkeyedListEntry.withChild(pruneDataAtDepth(oldUnkeyedListEntryValue, depth - 1)); + } + newUnkeyedListBuilder.addChild(newUnkeyedListEntry.build()); + } + } + return newUnkeyedListBuilder.build(); + } + + private DataContainerChild processOrderedMapNode(final NormalizedNode node, final Integer depth) { + CollectionNodeBuilder<MapEntryNode, UserMapNode> newOrderedMapNodeBuilder = Builders.orderedMapBuilder(); + processMapEntries(node, depth, newOrderedMapNodeBuilder); + return newOrderedMapNodeBuilder.build(); + } + + private DataContainerChild processMapNode(final NormalizedNode node, final Integer depth) { + CollectionNodeBuilder<MapEntryNode, SystemMapNode> newMapNodeBuilder = Builders.mapBuilder(); + processMapEntries(node, depth, newMapNodeBuilder); + return newMapNodeBuilder.build(); + } + + private void processMapEntries(final NormalizedNode node, final Integer depth, + final CollectionNodeBuilder<MapEntryNode, ? extends MapNode> newOrderedMapNodeBuilder) { + if (depth > 1) { + for (MapEntryNode oldMapEntryNode : ((MapNode) node).body()) { + DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> newMapEntryNodeBuilder = + Builders.mapEntryBuilder().withNodeIdentifier(oldMapEntryNode.getIdentifier()); + for (DataContainerChild mapEntryNodeValue : oldMapEntryNode.body()) { + newMapEntryNodeBuilder.withChild(pruneDataAtDepth(mapEntryNodeValue, depth - 1)); + } + newOrderedMapNodeBuilder.withChild(newMapEntryNodeBuilder.build()); + } + } + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PutResult.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PutResult.java new file mode 100644 index 0000000..87e33b9 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/PutResult.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2016 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import com.google.common.util.concurrent.FluentFuture; +import javax.ws.rs.core.Response.Status; +import org.opendaylight.mdsal.common.api.CommitInfo; + +/** + * Wrapper for status and future of PUT operation. + */ +public class PutResult { + private final Status status; + private final FluentFuture<? extends CommitInfo> future; + + /** + * Wrap status and future by constructor - make this immutable. + * + * @param status + * status of operations + * @param future + * result of submit of PUT operation + */ + public PutResult(final Status status, final FluentFuture<? extends CommitInfo> future) { + this.status = status; + this.future = future; + } + + /** + * Get status. + * + * @return {@link Status} result + */ + public Status getStatus() { + return this.status; + } + + /** + * Get future. + * + * @return {@link FluentFuture} result + */ + public FluentFuture<? extends CommitInfo> getFutureOfPutData() { + return this.future; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/QueryParametersParser.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/QueryParametersParser.java new file mode 100644 index 0000000..098c53b --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/QueryParametersParser.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import com.google.common.base.Strings; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.sal.rest.impl.WriterParameters; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.errors.RestconfError; +import org.opendaylight.yangtools.yang.common.ErrorTag; +import org.opendaylight.yangtools.yang.common.ErrorType; + +public final class QueryParametersParser { + + private enum UriParameters { + PRETTY_PRINT("prettyPrint"), + DEPTH("depth"); + + private final String uriParameterName; + + UriParameters(final String uriParameterName) { + this.uriParameterName = uriParameterName; + } + + @Override + public String toString() { + return uriParameterName; + } + } + + private QueryParametersParser() { + + } + + public static WriterParameters parseWriterParameters(final UriInfo info) { + final WriterParameters.WriterParametersBuilder wpBuilder = new WriterParameters.WriterParametersBuilder(); + if (info == null) { + return wpBuilder.build(); + } + + String param = info.getQueryParameters(false).getFirst(UriParameters.DEPTH.toString()); + if (!Strings.isNullOrEmpty(param) && !"unbounded".equals(param)) { + try { + final int depth = Integer.parseInt(param); + if (depth < 1) { + throw new RestconfDocumentedException( + new RestconfError(ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, + "Invalid depth parameter: " + depth, null, + "The depth parameter must be an integer > 1 or \"unbounded\"")); + } + wpBuilder.setDepth(depth); + } catch (final NumberFormatException e) { + throw new RestconfDocumentedException(e, new RestconfError( + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, + "Invalid depth parameter: " + e.getMessage(), null, + "The depth parameter must be an integer > 1 or \"unbounded\"")); + } + } + param = info.getQueryParameters(false).getFirst(UriParameters.PRETTY_PRINT.toString()); + wpBuilder.setPrettyPrint("true".equals(param)); + return wpBuilder.build(); + } + +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestCodec.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestCodec.java new file mode 100644 index 0000000..5d7a840 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestCodec.java @@ -0,0 +1,380 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import org.opendaylight.mdsal.dom.api.DOMMountPoint; +import org.opendaylight.netconf.sal.rest.impl.StringModuleInstanceIdentifierCodec; +import org.opendaylight.restconf.common.util.IdentityValuesDTO; +import org.opendaylight.restconf.common.util.IdentityValuesDTO.IdentityValue; +import org.opendaylight.restconf.common.util.IdentityValuesDTO.Predicate; +import org.opendaylight.restconf.common.util.RestUtil; +import org.opendaylight.yangtools.concepts.IllegalArgumentCodec; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument; +import org.opendaylight.yangtools.yang.data.api.codec.IdentityrefCodec; +import org.opendaylight.yangtools.yang.data.api.codec.InstanceIdentifierCodec; +import org.opendaylight.yangtools.yang.data.api.codec.LeafrefCodec; +import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec; +import org.opendaylight.yangtools.yang.model.api.DataNodeContainer; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.TypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition; +import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class RestCodec { + + private static final Logger LOG = LoggerFactory.getLogger(RestCodec.class); + + private RestCodec() { + } + + // FIXME: IllegalArgumentCodec is not quite accurate + public static IllegalArgumentCodec<Object, Object> from(final TypeDefinition<?> typeDefinition, + final DOMMountPoint mountPoint, final ControllerContext controllerContext) { + return new ObjectCodec(typeDefinition, mountPoint, controllerContext); + } + + @SuppressWarnings("rawtypes") + public static final class ObjectCodec implements IllegalArgumentCodec<Object, Object> { + + private static final Logger LOG = LoggerFactory.getLogger(ObjectCodec.class); + + public static final IllegalArgumentCodec LEAFREF_DEFAULT_CODEC = new LeafrefCodecImpl(); + + private final ControllerContext controllerContext; + private final IllegalArgumentCodec instanceIdentifier; + private final IllegalArgumentCodec identityrefCodec; + + private final TypeDefinition<?> type; + + private ObjectCodec(final TypeDefinition<?> typeDefinition, final DOMMountPoint mountPoint, + final ControllerContext controllerContext) { + this.controllerContext = controllerContext; + type = RestUtil.resolveBaseTypeFrom(typeDefinition); + if (type instanceof IdentityrefTypeDefinition) { + identityrefCodec = new IdentityrefCodecImpl(mountPoint, controllerContext); + } else { + identityrefCodec = null; + } + if (type instanceof InstanceIdentifierTypeDefinition) { + instanceIdentifier = new InstanceIdentifierCodecImpl(mountPoint, controllerContext); + } else { + instanceIdentifier = null; + } + } + + @SuppressWarnings("unchecked") + @Override + @SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = "Legacy code") + public Object deserialize(final Object input) { + try { + if (type instanceof IdentityrefTypeDefinition) { + if (input instanceof IdentityValuesDTO) { + return identityrefCodec.deserialize(input); + } + if (LOG.isDebugEnabled()) { + LOG.debug( + "Value is not instance of IdentityrefTypeDefinition but is {}. " + + "Therefore NULL is used as translation of - {}", + input == null ? "null" : input.getClass(), String.valueOf(input)); + } + // FIXME: this should be a hard error + return null; + } else if (type instanceof InstanceIdentifierTypeDefinition) { + if (input instanceof IdentityValuesDTO) { + return instanceIdentifier.deserialize(input); + } else { + final StringModuleInstanceIdentifierCodec codec = new StringModuleInstanceIdentifierCodec( + controllerContext.getGlobalSchema()); + return codec.deserialize((String) input); + } + } else { + final TypeDefinitionAwareCodec<Object, ? extends TypeDefinition<?>> typeAwarecodec = + TypeDefinitionAwareCodec.from(type); + if (typeAwarecodec != null) { + if (input instanceof IdentityValuesDTO) { + return typeAwarecodec.deserialize(((IdentityValuesDTO) input).getOriginValue()); + } + return typeAwarecodec.deserialize(String.valueOf(input)); + } else { + LOG.debug("Codec for type \"{}\" is not implemented yet.", type.getQName().getLocalName()); + // FIXME: this should be a hard error + return null; + } + } + } catch (final ClassCastException e) { // TODO remove this catch when everyone use codecs + LOG.error("ClassCastException was thrown when codec is invoked with parameter {}", input, e); + // FIXME: this should be a hard error + return null; + } + } + + @SuppressWarnings("unchecked") + @Override + @SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = "legacy code") + public Object serialize(final Object input) { + try { + if (type instanceof IdentityrefTypeDefinition) { + return identityrefCodec.serialize(input); + } else if (type instanceof LeafrefTypeDefinition) { + return LEAFREF_DEFAULT_CODEC.serialize(input); + } else if (type instanceof InstanceIdentifierTypeDefinition) { + return instanceIdentifier.serialize(input); + } else { + final TypeDefinitionAwareCodec<Object, ? extends TypeDefinition<?>> typeAwarecodec = + TypeDefinitionAwareCodec.from(type); + if (typeAwarecodec != null) { + return typeAwarecodec.serialize(input); + } else { + LOG.debug("Codec for type \"{}\" is not implemented yet.", type.getQName().getLocalName()); + return null; + } + } + } catch (final ClassCastException e) { + // FIXME: remove this catch when everyone use codecs + LOG.error("ClassCastException was thrown when codec is invoked with parameter {}", input, e); + // FIXME: this should be a hard error + return input; + } + } + + } + + public static class IdentityrefCodecImpl implements IdentityrefCodec<IdentityValuesDTO> { + private static final Logger LOG = LoggerFactory.getLogger(IdentityrefCodecImpl.class); + + private final DOMMountPoint mountPoint; + private final ControllerContext controllerContext; + + public IdentityrefCodecImpl(final DOMMountPoint mountPoint, final ControllerContext controllerContext) { + this.mountPoint = mountPoint; + this.controllerContext = controllerContext; + } + + @Override + public IdentityValuesDTO serialize(final QName data) { + return new IdentityValuesDTO(data.getNamespace().toString(), data.getLocalName(), null, null); + } + + @Override + @SuppressFBWarnings(value = "NP_NONNULL_RETURN_VIOLATION", justification = "See FIXME below") + public QName deserialize(final IdentityValuesDTO data) { + final IdentityValue valueWithNamespace = data.getValuesWithNamespaces().get(0); + final Module module = getModuleByNamespace(valueWithNamespace.getNamespace(), mountPoint, + controllerContext); + if (module == null) { + // FIXME: this should be a hard error + LOG.info("Module was not found for namespace {}", valueWithNamespace.getNamespace()); + LOG.info("Idenetityref will be translated as NULL for data - {}", String.valueOf(valueWithNamespace)); + return null; + } + + return QName.create(module.getNamespace(), module.getRevision(), valueWithNamespace.getValue()); + } + } + + public static class LeafrefCodecImpl implements LeafrefCodec<String> { + + @Override + public String serialize(final Object data) { + return String.valueOf(data); + } + + @Override + public Object deserialize(final String data) { + return data; + } + + } + + public static class InstanceIdentifierCodecImpl implements InstanceIdentifierCodec<IdentityValuesDTO> { + private static final Logger LOG = LoggerFactory.getLogger(InstanceIdentifierCodecImpl.class); + + private final DOMMountPoint mountPoint; + private final ControllerContext controllerContext; + + public InstanceIdentifierCodecImpl(final DOMMountPoint mountPoint, + final ControllerContext controllerContext) { + this.mountPoint = mountPoint; + this.controllerContext = controllerContext; + } + + @Override + public IdentityValuesDTO serialize(final YangInstanceIdentifier data) { + final IdentityValuesDTO identityValuesDTO = new IdentityValuesDTO(); + for (final PathArgument pathArgument : data.getPathArguments()) { + final IdentityValue identityValue = qNameToIdentityValue(pathArgument.getNodeType()); + if (pathArgument instanceof NodeIdentifierWithPredicates && identityValue != null) { + final List<Predicate> predicates = + keyValuesToPredicateList(((NodeIdentifierWithPredicates) pathArgument).entrySet()); + identityValue.setPredicates(predicates); + } else if (pathArgument instanceof NodeWithValue && identityValue != null) { + final List<Predicate> predicates = new ArrayList<>(); + final String value = String.valueOf(((NodeWithValue<?>) pathArgument).getValue()); + predicates.add(new Predicate(null, value)); + identityValue.setPredicates(predicates); + } + identityValuesDTO.add(identityValue); + } + return identityValuesDTO; + } + + @SuppressFBWarnings(value = { "RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", "NP_NONNULL_RETURN_VIOLATION" }, + justification = "Unrecognised NullableDecl") + @Override + public YangInstanceIdentifier deserialize(final IdentityValuesDTO data) { + final List<PathArgument> result = new ArrayList<>(); + final IdentityValue valueWithNamespace = data.getValuesWithNamespaces().get(0); + final Module module = getModuleByNamespace(valueWithNamespace.getNamespace(), mountPoint, + controllerContext); + if (module == null) { + LOG.info("Module by namespace '{}' of first node in instance-identifier was not found.", + valueWithNamespace.getNamespace()); + LOG.info("Instance-identifier will be translated as NULL for data - {}", + String.valueOf(valueWithNamespace.getValue())); + // FIXME: this should be a hard error + return null; + } + + DataNodeContainer parentContainer = module; + final List<IdentityValue> identities = data.getValuesWithNamespaces(); + for (int i = 0; i < identities.size(); i++) { + final IdentityValue identityValue = identities.get(i); + XMLNamespace validNamespace = resolveValidNamespace(identityValue.getNamespace(), mountPoint, + controllerContext); + final var found = ControllerContext.findInstanceDataChildByNameAndNamespace( + parentContainer, identityValue.getValue(), validNamespace); + if (found == null) { + LOG.info("'{}' node was not found in {}", identityValue, parentContainer.getChildNodes()); + LOG.info("Instance-identifier will be translated as NULL for data - {}", + String.valueOf(identityValue.getValue())); + // FIXME: this should be a hard error + return null; + } + final DataSchemaNode node = found.child; + final QName qName = node.getQName(); + PathArgument pathArgument = null; + if (identityValue.getPredicates().isEmpty()) { + pathArgument = new NodeIdentifier(qName); + } else { + if (node instanceof LeafListSchemaNode) { // predicate is value of leaf-list entry + final Predicate leafListPredicate = identityValue.getPredicates().get(0); + if (!leafListPredicate.isLeafList()) { + LOG.info("Predicate's data is not type of leaf-list. It should be in format \".='value'\""); + LOG.info("Instance-identifier will be translated as NULL for data - {}", + String.valueOf(identityValue.getValue())); + // FIXME: this should be a hard error + return null; + } + pathArgument = new NodeWithValue<>(qName, leafListPredicate.getValue()); + } else if (node instanceof ListSchemaNode) { // predicates are keys of list + final DataNodeContainer listNode = (DataNodeContainer) node; + final Map<QName, Object> predicatesMap = new HashMap<>(); + for (final Predicate predicate : identityValue.getPredicates()) { + validNamespace = resolveValidNamespace(predicate.getName().getNamespace(), mountPoint, + controllerContext); + final var listKey = ControllerContext + .findInstanceDataChildByNameAndNamespace(listNode, predicate.getName().getValue(), + validNamespace); + predicatesMap.put(listKey.child.getQName(), predicate.getValue()); + } + pathArgument = NodeIdentifierWithPredicates.of(qName, predicatesMap); + } else { + LOG.info("Node {} is not List or Leaf-list.", node); + LOG.info("Instance-identifier will be translated as NULL for data - {}", + String.valueOf(identityValue.getValue())); + // FIXME: this should be a hard error + return null; + } + } + result.add(pathArgument); + if (i < identities.size() - 1) { // last element in instance-identifier can be other than + // DataNodeContainer + if (node instanceof DataNodeContainer) { + parentContainer = (DataNodeContainer) node; + } else { + LOG.info("Node {} isn't instance of DataNodeContainer", node); + LOG.info("Instance-identifier will be translated as NULL for data - {}", + String.valueOf(identityValue.getValue())); + // FIXME: this should be a hard error + return null; + } + } + } + + return result.isEmpty() ? null : YangInstanceIdentifier.create(result); + } + + private static List<Predicate> keyValuesToPredicateList(final Set<Entry<QName, Object>> keyValues) { + final List<Predicate> result = new ArrayList<>(); + for (final Entry<QName, Object> entry : keyValues) { + final QName qualifiedName = entry.getKey(); + final Object value = entry.getValue(); + result.add(new Predicate(qNameToIdentityValue(qualifiedName), String.valueOf(value))); + } + return result; + } + + private static IdentityValue qNameToIdentityValue(final QName qualifiedName) { + if (qualifiedName != null) { + return new IdentityValue(qualifiedName.getNamespace().toString(), qualifiedName.getLocalName()); + } + return null; + } + } + + private static Module getModuleByNamespace(final String namespace, final DOMMountPoint mountPoint, + final ControllerContext controllerContext) { + final XMLNamespace validNamespace = resolveValidNamespace(namespace, mountPoint, controllerContext); + + Module module = null; + if (mountPoint != null) { + module = ControllerContext.findModuleByNamespace(mountPoint, validNamespace); + } else { + module = controllerContext.findModuleByNamespace(validNamespace); + } + if (module == null) { + LOG.info("Module for namespace {} was not found.", validNamespace); + return null; + } + return module; + } + + private static XMLNamespace resolveValidNamespace(final String namespace, final DOMMountPoint mountPoint, + final ControllerContext controllerContext) { + XMLNamespace validNamespace; + if (mountPoint != null) { + validNamespace = ControllerContext.findNamespaceByModuleName(mountPoint, namespace); + } else { + validNamespace = controllerContext.findNamespaceByModuleName(namespace); + } + if (validNamespace == null) { + validNamespace = XMLNamespace.of(namespace); + } + + return validNamespace; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java new file mode 100644 index 0000000..b3a1a9c --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfImpl.java @@ -0,0 +1,1546 @@ +/* + * Copyright (c) 2014 - 2016 Brocade Communication Systems, Inc., Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.base.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Predicates; +import com.google.common.base.Splitter; +import com.google.common.base.Strings; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.net.URI; +import java.time.Instant; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.time.format.DateTimeParseException; +import java.time.temporal.ChronoField; +import java.time.temporal.TemporalAccessor; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.ResponseBuilder; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriBuilder; +import javax.ws.rs.core.UriInfo; +import org.eclipse.jdt.annotation.NonNull; +import org.opendaylight.mdsal.common.api.CommitInfo; +import org.opendaylight.mdsal.common.api.LogicalDatastoreType; +import org.opendaylight.mdsal.common.api.OptimisticLockFailedException; +import org.opendaylight.mdsal.common.api.TransactionCommitFailedException; +import org.opendaylight.mdsal.dom.api.DOMMountPoint; +import org.opendaylight.mdsal.dom.api.DOMRpcImplementationNotAvailableException; +import org.opendaylight.mdsal.dom.api.DOMRpcResult; +import org.opendaylight.mdsal.dom.api.DOMRpcService; +import org.opendaylight.mdsal.dom.api.DOMSchemaService; +import org.opendaylight.mdsal.dom.spi.DefaultDOMRpcResult; +import org.opendaylight.netconf.sal.rest.api.Draft02; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeContext; +import org.opendaylight.netconf.sal.restconf.impl.ControllerContext.FoundChild; +import org.opendaylight.netconf.sal.streams.listeners.ListenerAdapter; +import org.opendaylight.netconf.sal.streams.listeners.NotificationListenerAdapter; +import org.opendaylight.netconf.sal.streams.listeners.Notificator; +import org.opendaylight.netconf.sal.streams.websockets.WebSocketServer; +import org.opendaylight.restconf.common.OperationsContent; +import org.opendaylight.restconf.common.context.InstanceIdentifierContext; +import org.opendaylight.restconf.common.errors.RestconfDocumentedException; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchStatusContext; +import org.opendaylight.restconf.common.util.OperationsResourceUtils; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.types.rev130715.DateAndTime; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.CreateDataChangeEventSubscriptionInput1.Scope; +import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev140708.NotificationOutputTypeGrouping.NotificationOutputType; +import org.opendaylight.yangtools.yang.common.Empty; +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.QNameModule; +import org.opendaylight.yangtools.yang.common.Revision; +import org.opendaylight.yangtools.yang.common.XMLNamespace; +import org.opendaylight.yangtools.yang.common.YangConstants; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.AugmentationIdentifier; +import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier; +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.AugmentationNode; +import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode; +import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode; +import org.opendaylight.yangtools.yang.data.api.schema.MapNode; +import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode; +import org.opendaylight.yangtools.yang.data.api.schema.SystemLeafSetNode; +import org.opendaylight.yangtools.yang.data.api.schema.SystemMapNode; +import org.opendaylight.yangtools.yang.data.api.schema.builder.CollectionNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.DataContainerNodeBuilder; +import org.opendaylight.yangtools.yang.data.api.schema.builder.ListNodeBuilder; +import org.opendaylight.yangtools.yang.data.impl.schema.Builders; +import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes; +import org.opendaylight.yangtools.yang.data.impl.schema.SchemaAwareBuilders; +import org.opendaylight.yangtools.yang.data.tree.api.ModifiedNodeDoesNotExistException; +import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode; +import org.opendaylight.yangtools.yang.model.api.DataSchemaNode; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.FeatureDefinition; +import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode; +import org.opendaylight.yangtools.yang.model.api.ListSchemaNode; +import org.opendaylight.yangtools.yang.model.api.Module; +import org.opendaylight.yangtools.yang.model.api.NotificationDefinition; +import org.opendaylight.yangtools.yang.model.api.RpcDefinition; +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.opendaylight.yangtools.yang.model.util.SchemaInferenceStack; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Singleton +public final class RestconfImpl implements RestconfService { + /** + * Notifications are served on port 8181. + */ + private static final int NOTIFICATION_PORT = 8181; + + private static final int CHAR_NOT_FOUND = -1; + + private static final String SAL_REMOTE_NAMESPACE = "urn:opendaylight:params:xml:ns:yang:controller:md:sal:remote"; + + private static final Logger LOG = LoggerFactory.getLogger(RestconfImpl.class); + + private static final LogicalDatastoreType DEFAULT_DATASTORE = LogicalDatastoreType.CONFIGURATION; + + private static final XMLNamespace NAMESPACE_EVENT_SUBSCRIPTION_AUGMENT = + XMLNamespace.of("urn:sal:restconf:event:subscription"); + + private static final String DATASTORE_PARAM_NAME = "datastore"; + + private static final String SCOPE_PARAM_NAME = "scope"; + + private static final String OUTPUT_TYPE_PARAM_NAME = "notification-output-type"; + + private static final String NETCONF_BASE = "urn:ietf:params:xml:ns:netconf:base:1.0"; + + private static final String NETCONF_BASE_PAYLOAD_NAME = "data"; + + private static final QName NETCONF_BASE_QNAME = QName.create(QNameModule.create(XMLNamespace.of(NETCONF_BASE)), + NETCONF_BASE_PAYLOAD_NAME).intern(); + + private static final QNameModule SAL_REMOTE_AUGMENT = QNameModule.create(NAMESPACE_EVENT_SUBSCRIPTION_AUGMENT, + Revision.of("2014-07-08")); + + private static final AugmentationIdentifier SAL_REMOTE_AUG_IDENTIFIER = + new AugmentationIdentifier(ImmutableSet.of( + QName.create(SAL_REMOTE_AUGMENT, "scope"), QName.create(SAL_REMOTE_AUGMENT, "datastore"), + QName.create(SAL_REMOTE_AUGMENT, "notification-output-type"))); + + public static final String DATA_SUBSCR = "data-change-event-subscription"; + private static final String CREATE_DATA_SUBSCR = "create-" + DATA_SUBSCR; + + public static final String NOTIFICATION_STREAM = "notification-stream"; + private static final String CREATE_NOTIFICATION_STREAM = "create-" + NOTIFICATION_STREAM; + + private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder() + .appendValue(ChronoField.YEAR, 4).appendLiteral('-') + .appendValue(ChronoField.MONTH_OF_YEAR, 2).appendLiteral('-') + .appendValue(ChronoField.DAY_OF_MONTH, 2).appendLiteral('T') + .appendValue(ChronoField.HOUR_OF_DAY, 2).appendLiteral(':') + .appendValue(ChronoField.MINUTE_OF_HOUR, 2).appendLiteral(':') + .appendValue(ChronoField.SECOND_OF_MINUTE, 2) + .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true) + .appendOffset("+HH:MM", "Z").toFormatter(); + + private final BrokerFacade broker; + + private final ControllerContext controllerContext; + + @Inject + public RestconfImpl(final BrokerFacade broker, final ControllerContext controllerContext) { + this.broker = broker; + this.controllerContext = controllerContext; + } + + /** + * Factory method. + * + * @deprecated Just use {@link #RestconfImpl(BrokerFacade, ControllerContext)} constructor instead. + */ + @Deprecated + public static RestconfImpl newInstance(final BrokerFacade broker, final ControllerContext controllerContext) { + return new RestconfImpl(broker, controllerContext); + } + + @Override + @Deprecated + public NormalizedNodeContext getModules(final UriInfo uriInfo) { + final Module restconfModule = getRestconfModule(); + final var stack = SchemaInferenceStack.of(controllerContext.getGlobalSchema()); + final var restconf = QName.create(restconfModule.getQNameModule(), + Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE); + stack.enterGrouping(restconf); + stack.enterSchemaTree(restconf); + final var modules = QName.create(restconf, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + final var modulesSchemaNode = stack.enterSchemaTree(modules); + checkState(modulesSchemaNode instanceof ContainerSchemaNode); + + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> moduleContainerBuilder = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) modulesSchemaNode); + moduleContainerBuilder.withChild(makeModuleMapNode(controllerContext.getAllModules())); + + return new NormalizedNodeContext(InstanceIdentifierContext.ofStack(stack, null), + moduleContainerBuilder.build(), QueryParametersParser.parseWriterParameters(uriInfo)); + } + + /** + * Valid only for mount point. + */ + @Override + @Deprecated + public NormalizedNodeContext getModules(final String identifier, final UriInfo uriInfo) { + if (!identifier.contains(ControllerContext.MOUNT)) { + final String errMsg = "URI has bad format. If modules behind mount point should be showed," + + " URI has to end with " + ControllerContext.MOUNT; + LOG.debug("{} for {}", errMsg, identifier); + throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final InstanceIdentifierContext mountPointIdentifier = + controllerContext.toMountPointIdentifier(identifier); + final DOMMountPoint mountPoint = mountPointIdentifier.getMountPoint(); + final MapNode mountPointModulesMap = makeModuleMapNode(controllerContext.getAllModules(mountPoint)); + + final Module restconfModule = getRestconfModule(); + final var stack = SchemaInferenceStack.of(controllerContext.getGlobalSchema()); + final var restconf = QName.create(restconfModule.getQNameModule(), + Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE); + stack.enterGrouping(restconf); + stack.enterSchemaTree(restconf); + final var modules = QName.create(restconf, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE); + final var modulesSchemaNode = stack.enterSchemaTree(modules); + checkState(modulesSchemaNode instanceof ContainerSchemaNode); + + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> moduleContainerBuilder = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) modulesSchemaNode); + moduleContainerBuilder.withChild(mountPointModulesMap); + + return new NormalizedNodeContext(InstanceIdentifierContext.ofStack(stack, null), + moduleContainerBuilder.build(), QueryParametersParser.parseWriterParameters(uriInfo)); + } + + @Override + @Deprecated + public NormalizedNodeContext getModule(final String identifier, final UriInfo uriInfo) { + final Entry<String, Revision> nameRev = getModuleNameAndRevision(requireNonNull(identifier)); + final Module module; + final DOMMountPoint mountPoint; + if (identifier.contains(ControllerContext.MOUNT)) { + final InstanceIdentifierContext mountPointIdentifier = + controllerContext.toMountPointIdentifier(identifier); + mountPoint = mountPointIdentifier.getMountPoint(); + module = controllerContext.findModuleByNameAndRevision(mountPoint, nameRev.getKey(), + nameRev.getValue()); + } else { + mountPoint = null; + module = controllerContext.findModuleByNameAndRevision(nameRev.getKey(), nameRev.getValue()); + } + + if (module == null) { + LOG.debug("Module with name '{}' and revision '{}' was not found.", nameRev.getKey(), nameRev.getValue()); + throw new RestconfDocumentedException("Module with name '" + nameRev.getKey() + "' and revision '" + + nameRev.getValue() + "' was not found.", ErrorType.PROTOCOL, ErrorTag.UNKNOWN_ELEMENT); + } + + final Module restconfModule = getRestconfModule(); + final var stack = SchemaInferenceStack.of(controllerContext.getGlobalSchema()); + final var restconf = QName.create(restconfModule.getQNameModule(), + Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE); + stack.enterGrouping(restconf); + stack.enterSchemaTree(restconf); + stack.enterSchemaTree(QName.create(restconf, Draft02.RestConfModule.MODULES_CONTAINER_SCHEMA_NODE)); + stack.enterSchemaTree(QName.create(restconf, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE)); + + return new NormalizedNodeContext(InstanceIdentifierContext.ofStack(stack, mountPoint), + makeModuleMapNode(Set.of(module)), QueryParametersParser.parseWriterParameters(uriInfo)); + } + + @Override + @Deprecated + public NormalizedNodeContext getAvailableStreams(final UriInfo uriInfo) { + final Set<String> availableStreams = Notificator.getStreamNames(); + final Module restconfModule = getRestconfModule(); + final DataSchemaNode streamSchemaNode = controllerContext + .getRestconfModuleRestConfSchemaNode(restconfModule, Draft02.RestConfModule.STREAM_LIST_SCHEMA_NODE); + checkState(streamSchemaNode instanceof ListSchemaNode); + + final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listStreamsBuilder = + SchemaAwareBuilders.mapBuilder((ListSchemaNode) streamSchemaNode); + + for (final String streamName : availableStreams) { + listStreamsBuilder.withChild(toStreamEntryNode(streamName, streamSchemaNode)); + } + + final var stack = SchemaInferenceStack.of(controllerContext.getGlobalSchema()); + final var restconf = QName.create(restconfModule.getQNameModule(), + Draft02.RestConfModule.RESTCONF_GROUPING_SCHEMA_NODE); + stack.enterGrouping(restconf); + stack.enterSchemaTree(restconf); + final var streams = QName.create(restconf, Draft02.RestConfModule.STREAMS_CONTAINER_SCHEMA_NODE); + final var streamsContainerSchemaNode = stack.enterSchemaTree(streams); + checkState(streamsContainerSchemaNode instanceof ContainerSchemaNode); + + final DataContainerNodeBuilder<NodeIdentifier, ContainerNode> streamsContainerBuilder = + SchemaAwareBuilders.containerBuilder((ContainerSchemaNode) streamsContainerSchemaNode); + streamsContainerBuilder.withChild(listStreamsBuilder.build()); + + return new NormalizedNodeContext(InstanceIdentifierContext.ofStack(stack), + streamsContainerBuilder.build(), QueryParametersParser.parseWriterParameters(uriInfo)); + } + + @Override + @Deprecated + public String getOperationsJSON() { + return OperationsContent.JSON.bodyFor(controllerContext.getGlobalSchema()); + } + + @Override + @Deprecated + public String getOperationsXML() { + return OperationsContent.XML.bodyFor(controllerContext.getGlobalSchema()); + } + + @Override + @Deprecated + public NormalizedNodeContext getOperations(final String identifier, final UriInfo uriInfo) { + if (!identifier.contains(ControllerContext.MOUNT)) { + final String errMsg = "URI has bad format. If operations behind mount point should be showed, URI has to " + + " end with " + ControllerContext.MOUNT; + LOG.debug("{} for {}", errMsg, identifier); + throw new RestconfDocumentedException(errMsg, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final InstanceIdentifierContext mountPointIdentifier = controllerContext.toMountPointIdentifier(identifier); + final DOMMountPoint mountPoint = mountPointIdentifier.getMountPoint(); + final var entry = OperationsResourceUtils.contextForModelContext(modelContext(mountPoint), mountPoint); + return new NormalizedNodeContext(entry.getKey(), entry.getValue()); + } + + private Module getRestconfModule() { + final Module restconfModule = controllerContext.getRestconfModule(); + if (restconfModule == null) { + LOG.debug("ietf-restconf module was not found."); + throw new RestconfDocumentedException("ietf-restconf module was not found.", ErrorType.APPLICATION, + ErrorTag.OPERATION_NOT_SUPPORTED); + } + + return restconfModule; + } + + private static Entry<String, Revision> getModuleNameAndRevision(final String identifier) { + final int mountIndex = identifier.indexOf(ControllerContext.MOUNT); + String moduleNameAndRevision = ""; + if (mountIndex >= 0) { + moduleNameAndRevision = identifier.substring(mountIndex + ControllerContext.MOUNT.length()); + } else { + moduleNameAndRevision = identifier; + } + + final Splitter splitter = Splitter.on('/').omitEmptyStrings(); + final List<String> pathArgs = splitter.splitToList(moduleNameAndRevision); + if (pathArgs.size() < 2) { + LOG.debug("URI has bad format. It should be \'moduleName/yyyy-MM-dd\' {}", identifier); + throw new RestconfDocumentedException( + "URI has bad format. End of URI should be in format \'moduleName/yyyy-MM-dd\'", ErrorType.PROTOCOL, + ErrorTag.INVALID_VALUE); + } + + try { + return Map.entry(pathArgs.get(0), Revision.of(pathArgs.get(1))); + } catch (final DateTimeParseException e) { + LOG.debug("URI has bad format. It should be \'moduleName/yyyy-MM-dd\' {}", identifier); + throw new RestconfDocumentedException("URI has bad format. It should be \'moduleName/yyyy-MM-dd\'", + ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e); + } + } + + @Override + public Object getRoot() { + return null; + } + + @Override + public NormalizedNodeContext invokeRpc(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + if (payload == null) { + // no payload specified, reroute this to no payload invokeRpc implementation + return invokeRpc(identifier, uriInfo); + } + + final SchemaNode schema = payload.getInstanceIdentifierContext().getSchemaNode(); + final ListenableFuture<? extends DOMRpcResult> response; + final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint(); + final NormalizedNode input = nonnullInput(schema, payload.getData()); + final EffectiveModelContext schemaContext; + + if (mountPoint != null) { + final Optional<DOMRpcService> mountRpcServices = mountPoint.getService(DOMRpcService.class); + if (mountRpcServices.isEmpty()) { + LOG.debug("Error: Rpc service is missing."); + throw new RestconfDocumentedException("Rpc service is missing."); + } + schemaContext = modelContext(mountPoint); + response = mountRpcServices.get().invokeRpc(schema.getQName(), input); + } else { + final XMLNamespace namespace = schema.getQName().getNamespace(); + if (namespace.toString().equals(SAL_REMOTE_NAMESPACE)) { + if (identifier.contains(CREATE_DATA_SUBSCR)) { + response = invokeSalRemoteRpcSubscribeRPC(payload); + } else if (identifier.contains(CREATE_NOTIFICATION_STREAM)) { + response = invokeSalRemoteRpcNotifiStrRPC(payload); + } else { + final String msg = "Not supported operation"; + LOG.warn(msg); + throw new RestconfDocumentedException(msg, ErrorType.RPC, ErrorTag.OPERATION_NOT_SUPPORTED); + } + } else { + response = broker.invokeRpc(schema.getQName(), input); + } + schemaContext = controllerContext.getGlobalSchema(); + } + + final DOMRpcResult result = checkRpcResponse(response); + + final NormalizedNode resultData; + if (result != null && result.getResult() != null) { + resultData = result.getResult(); + } else { + resultData = null; + } + + if (resultData != null && ((ContainerNode) resultData).isEmpty()) { + throw new WebApplicationException(Response.Status.NO_CONTENT); + } + + final var resultNodeSchema = (RpcDefinition) payload.getInstanceIdentifierContext().getSchemaNode(); + return new NormalizedNodeContext( + InstanceIdentifierContext.ofRpcOutput(schemaContext, resultNodeSchema, mountPoint), resultData, + QueryParametersParser.parseWriterParameters(uriInfo)); + } + + @SuppressFBWarnings(value = "NP_LOAD_OF_KNOWN_NULL_VALUE", + justification = "Looks like a false positive, see below FIXME") + private NormalizedNodeContext invokeRpc(final String identifier, final UriInfo uriInfo) { + final DOMMountPoint mountPoint; + final String identifierEncoded; + final EffectiveModelContext schemaContext; + if (identifier.contains(ControllerContext.MOUNT)) { + // mounted RPC call - look up mount instance. + final InstanceIdentifierContext mountPointId = controllerContext.toMountPointIdentifier(identifier); + mountPoint = mountPointId.getMountPoint(); + schemaContext = modelContext(mountPoint); + final int startOfRemoteRpcName = + identifier.lastIndexOf(ControllerContext.MOUNT) + ControllerContext.MOUNT.length() + 1; + final String remoteRpcName = identifier.substring(startOfRemoteRpcName); + identifierEncoded = remoteRpcName; + + } else if (identifier.indexOf('/') == CHAR_NOT_FOUND) { + identifierEncoded = identifier; + mountPoint = null; + schemaContext = controllerContext.getGlobalSchema(); + } else { + LOG.debug("Identifier {} cannot contain slash character (/).", identifier); + throw new RestconfDocumentedException(String.format("Identifier %n%s%ncan\'t contain slash character (/).%n" + + "If slash is part of identifier name then use %%2F placeholder.", identifier), ErrorType.PROTOCOL, + ErrorTag.INVALID_VALUE); + } + + final String identifierDecoded = ControllerContext.urlPathArgDecode(identifierEncoded); + final RpcDefinition rpc; + if (mountPoint == null) { + rpc = controllerContext.getRpcDefinition(identifierDecoded); + } else { + rpc = findRpc(modelContext(mountPoint), identifierDecoded); + } + + if (rpc == null) { + LOG.debug("RPC {} does not exist.", identifierDecoded); + throw new RestconfDocumentedException("RPC does not exist.", ErrorType.RPC, ErrorTag.UNKNOWN_ELEMENT); + } + + if (!rpc.getInput().getChildNodes().isEmpty()) { + LOG.debug("No input specified for RPC {} with an input section", rpc); + throw new RestconfDocumentedException("No input specified for RPC " + rpc + + " with an input section defined", ErrorType.RPC, ErrorTag.MISSING_ELEMENT); + } + + final ContainerNode input = defaultInput(rpc.getQName()); + final ListenableFuture<? extends DOMRpcResult> response; + if (mountPoint != null) { + final Optional<DOMRpcService> mountRpcServices = mountPoint.getService(DOMRpcService.class); + if (mountRpcServices.isEmpty()) { + throw new RestconfDocumentedException("Rpc service is missing."); + } + response = mountRpcServices.get().invokeRpc(rpc.getQName(), input); + } else { + response = broker.invokeRpc(rpc.getQName(), input); + } + + final NormalizedNode result = checkRpcResponse(response).getResult(); + if (result != null && ((ContainerNode) result).isEmpty()) { + throw new WebApplicationException(Response.Status.NO_CONTENT); + } + + // FIXME: in reference to the above @SupressFBWarnings: "mountPoint" reference here trips up SpotBugs, as it + // thinks it can only ever be null. Except it can very much be non-null. The core problem is the horrible + // structure of this code where we have a sh*tload of checks for mountpoint above and all over the + // codebase where all that difference should have been properly encapsulated. + // + // This is legacy code, so if anybody cares to do that refactor, feel free to contribute, but I am not + // doing that work. + final var iic = mountPoint == null ? InstanceIdentifierContext.ofLocalRpcOutput(schemaContext, rpc) + : InstanceIdentifierContext.ofMountPointRpcOutput(mountPoint, schemaContext, rpc); + return new NormalizedNodeContext(iic, result, QueryParametersParser.parseWriterParameters(uriInfo)); + } + + private static @NonNull NormalizedNode nonnullInput(final SchemaNode rpc, final NormalizedNode input) { + return input != null ? input : defaultInput(rpc.getQName()); + } + + private static @NonNull ContainerNode defaultInput(final QName rpcName) { + return ImmutableNodes.containerNode(YangConstants.operationInputQName(rpcName.getModule())); + } + + @SuppressWarnings("checkstyle:avoidHidingCauseException") + private static DOMRpcResult checkRpcResponse(final ListenableFuture<? extends DOMRpcResult> response) { + if (response == null) { + return null; + } + try { + final DOMRpcResult retValue = response.get(); + if (retValue.getErrors().isEmpty()) { + return retValue; + } + LOG.debug("RpcError message {}", retValue.getErrors()); + throw new RestconfDocumentedException("RpcError message", null, retValue.getErrors()); + } catch (final InterruptedException e) { + final String errMsg = "The operation was interrupted while executing and did not complete."; + LOG.debug("Rpc Interrupt - {}", errMsg, e); + throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION, e); + } catch (final ExecutionException e) { + LOG.debug("Execution RpcError: ", e); + Throwable cause = e.getCause(); + if (cause == null) { + throw new RestconfDocumentedException("The operation encountered an unexpected error while executing.", + e); + } + while (cause.getCause() != null) { + cause = cause.getCause(); + } + + if (cause instanceof IllegalArgumentException) { + throw new RestconfDocumentedException(cause.getMessage(), ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } else if (cause instanceof DOMRpcImplementationNotAvailableException) { + throw new RestconfDocumentedException(cause.getMessage(), ErrorType.APPLICATION, + ErrorTag.OPERATION_NOT_SUPPORTED); + } + throw new RestconfDocumentedException("The operation encountered an unexpected error while executing.", + cause); + } catch (final CancellationException e) { + final String errMsg = "The operation was cancelled while executing."; + LOG.debug("Cancel RpcExecution: {}", errMsg, e); + throw new RestconfDocumentedException(errMsg, ErrorType.RPC, ErrorTag.PARTIAL_OPERATION); + } + } + + private static void validateInput(final SchemaNode inputSchema, final NormalizedNodeContext payload) { + if (inputSchema != null && payload.getData() == null) { + // expected a non null payload + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } else if (inputSchema == null && payload.getData() != null) { + // did not expect any input + throw new RestconfDocumentedException("No input expected.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + } + + private ListenableFuture<DOMRpcResult> invokeSalRemoteRpcSubscribeRPC(final NormalizedNodeContext payload) { + final ContainerNode value = (ContainerNode) payload.getData(); + final QName rpcQName = payload.getInstanceIdentifierContext().getSchemaNode().getQName(); + final Optional<DataContainerChild> path = + value.findChildByArg(new NodeIdentifier(QName.create(rpcQName, "path"))); + final Object pathValue = path.isPresent() ? path.get().body() : null; + + if (!(pathValue instanceof YangInstanceIdentifier)) { + LOG.debug("Instance identifier {} was not normalized correctly", rpcQName); + throw new RestconfDocumentedException("Instance identifier was not normalized correctly", + ErrorType.APPLICATION, ErrorTag.OPERATION_FAILED); + } + + final YangInstanceIdentifier pathIdentifier = (YangInstanceIdentifier) pathValue; + final String streamName; + NotificationOutputType outputType = null; + if (!pathIdentifier.isEmpty()) { + final String fullRestconfIdentifier = + DATA_SUBSCR + controllerContext.toFullRestconfIdentifier(pathIdentifier, null); + + LogicalDatastoreType datastore = + parseEnumTypeParameter(value, LogicalDatastoreType.class, DATASTORE_PARAM_NAME); + datastore = datastore == null ? DEFAULT_DATASTORE : datastore; + + Scope scope = parseEnumTypeParameter(value, Scope.class, SCOPE_PARAM_NAME); + scope = scope == null ? Scope.BASE : scope; + + outputType = parseEnumTypeParameter(value, NotificationOutputType.class, OUTPUT_TYPE_PARAM_NAME); + outputType = outputType == null ? NotificationOutputType.XML : outputType; + + streamName = Notificator + .createStreamNameFromUri(fullRestconfIdentifier + "/datastore=" + datastore + "/scope=" + scope); + } else { + streamName = CREATE_DATA_SUBSCR; + } + + if (Strings.isNullOrEmpty(streamName)) { + LOG.debug("Path is empty or contains value node which is not Container or List built-in type at {}", + pathIdentifier); + throw new RestconfDocumentedException("Path is empty or contains value node which is not Container or List " + + "built-in type.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + if (!Notificator.existListenerFor(streamName)) { + Notificator.createListener(pathIdentifier, streamName, outputType, controllerContext); + } + + return Futures.immediateFuture(new DefaultDOMRpcResult(Builders.containerBuilder() + .withNodeIdentifier(new NodeIdentifier(QName.create(rpcQName, "output"))) + .withChild(ImmutableNodes.leafNode(QName.create(rpcQName, "stream-name"), streamName)) + .build())); + } + + private static RpcDefinition findRpc(final SchemaContext schemaContext, final String identifierDecoded) { + final String[] splittedIdentifier = identifierDecoded.split(":"); + if (splittedIdentifier.length != 2) { + LOG.debug("{} could not be split to 2 parts (module:rpc name)", identifierDecoded); + throw new RestconfDocumentedException(identifierDecoded + " could not be split to 2 parts " + + "(module:rpc name)", ErrorType.APPLICATION, ErrorTag.INVALID_VALUE); + } + for (final Module module : schemaContext.getModules()) { + if (module.getName().equals(splittedIdentifier[0])) { + for (final RpcDefinition rpcDefinition : module.getRpcs()) { + if (rpcDefinition.getQName().getLocalName().equals(splittedIdentifier[1])) { + return rpcDefinition; + } + } + } + } + return null; + } + + @Override + public NormalizedNodeContext readConfigurationData(final String identifier, final UriInfo uriInfo) { + boolean withDefaUsed = false; + String withDefa = null; + + for (final Entry<String, List<String>> 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; + } + } + + // TODO: this flag is always ignored + boolean tagged = false; + if (withDefaUsed) { + if ("report-all-tagged".equals(withDefa)) { + tagged = true; + withDefa = null; + } + if ("report-all".equals(withDefa)) { + withDefa = null; + } + } + + final InstanceIdentifierContext iiWithData = controllerContext.toInstanceIdentifier(identifier); + final DOMMountPoint mountPoint = iiWithData.getMountPoint(); + NormalizedNode data = null; + final YangInstanceIdentifier normalizedII = iiWithData.getInstanceIdentifier(); + if (mountPoint != null) { + data = broker.readConfigurationData(mountPoint, normalizedII, withDefa); + } else { + data = broker.readConfigurationData(normalizedII, withDefa); + } + if (data == null) { + throw dataMissing(identifier); + } + return new NormalizedNodeContext(iiWithData, data, QueryParametersParser.parseWriterParameters(uriInfo)); + } + + @Override + public NormalizedNodeContext readOperationalData(final String identifier, final UriInfo uriInfo) { + final InstanceIdentifierContext iiWithData = controllerContext.toInstanceIdentifier(identifier); + final DOMMountPoint mountPoint = iiWithData.getMountPoint(); + NormalizedNode data = null; + final YangInstanceIdentifier normalizedII = iiWithData.getInstanceIdentifier(); + if (mountPoint != null) { + data = broker.readOperationalData(mountPoint, normalizedII); + } else { + data = broker.readOperationalData(normalizedII); + } + if (data == null) { + throw dataMissing(identifier); + } + return new NormalizedNodeContext(iiWithData, data, QueryParametersParser.parseWriterParameters(uriInfo)); + } + + private static RestconfDocumentedException dataMissing(final String identifier) { + LOG.debug("Request could not be completed because the relevant data model content does not exist {}", + identifier); + return new RestconfDocumentedException("Request could not be completed because the relevant data model content " + + "does not exist", ErrorType.APPLICATION, ErrorTag.DATA_MISSING); + } + + @Override + public Response updateConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + boolean insertUsed = false; + boolean pointUsed = false; + String insert = null; + String point = null; + + for (final Entry<String, List<String>> 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()); + } + } + + 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."); + } + + requireNonNull(identifier); + + final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext(); + + validateInput(iiWithData.getSchemaNode(), payload); + validateTopLevelNodeName(payload, iiWithData.getInstanceIdentifier()); + validateListKeysEqualityInPayloadAndUri(payload); + + final DOMMountPoint mountPoint = iiWithData.getMountPoint(); + final YangInstanceIdentifier normalizedII = iiWithData.getInstanceIdentifier(); + + /* + * There is a small window where another write transaction could be + * updating the same data simultaneously and we get an + * OptimisticLockFailedException. This error is likely transient and The + * WriteTransaction#submit API docs state that a retry will likely + * succeed. So we'll try again if that scenario occurs. If it fails a + * third time then it probably will never succeed so we'll fail in that + * case. + * + * By retrying we're attempting to hide the internal implementation of + * the data store and how it handles concurrent updates from the + * restconf client. The client has instructed us to put the data and we + * should make every effort to do so without pushing optimistic lock + * failures back to the client and forcing them to handle it via retry + * (and having to document the behavior). + */ + PutResult result = null; + int tries = 2; + while (true) { + if (mountPoint != null) { + + result = broker.commitMountPointDataPut(mountPoint, normalizedII, payload.getData(), insert, + point); + } else { + result = broker.commitConfigurationDataPut(controllerContext.getGlobalSchema(), normalizedII, + payload.getData(), insert, point); + } + + try { + result.getFutureOfPutData().get(); + } catch (final InterruptedException e) { + LOG.debug("Update failed for {}", identifier, e); + throw new RestconfDocumentedException(e.getMessage(), e); + } catch (final ExecutionException e) { + final TransactionCommitFailedException failure = Throwables.getCauseAs(e, + TransactionCommitFailedException.class); + if (failure instanceof OptimisticLockFailedException) { + if (--tries <= 0) { + LOG.debug("Got OptimisticLockFailedException on last try - failing {}", identifier); + throw new RestconfDocumentedException(e.getMessage(), e, failure.getErrorList()); + } + + LOG.debug("Got OptimisticLockFailedException - trying again {}", identifier); + continue; + } + + LOG.debug("Update failed for {}", identifier, e); + throw RestconfDocumentedException.decodeAndThrow(e.getMessage(), failure); + } + + return Response.status(result.getStatus()).build(); + } + } + + private static void validateTopLevelNodeName(final NormalizedNodeContext node, + final YangInstanceIdentifier identifier) { + + final String payloadName = node.getData().getIdentifier().getNodeType().getLocalName(); + + // no arguments + if (identifier.isEmpty()) { + // no "data" payload + if (!node.getData().getIdentifier().getNodeType().equals(NETCONF_BASE_QNAME)) { + throw new RestconfDocumentedException("Instance identifier has to contain at least one path argument", + ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + // any arguments + } else { + final String identifierName = identifier.getLastPathArgument().getNodeType().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 + * + */ + private static void validateListKeysEqualityInPayloadAndUri(final NormalizedNodeContext payload) { + checkArgument(payload != null); + 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<QName> keyDefinitions = ((ListSchemaNode) schemaNode).getKeyDefinition(); + if (lastPathArgument instanceof NodeIdentifierWithPredicates && data instanceof MapEntryNode) { + final Map<QName, Object> uriKeyValues = ((NodeIdentifierWithPredicates) lastPathArgument).asMap(); + isEqualUriAndPayloadKeyValues(uriKeyValues, (MapEntryNode) data, keyDefinitions); + } + } + } + + @VisibleForTesting + public static void isEqualUriAndPayloadKeyValues(final Map<QName, Object> uriKeyValues, final MapEntryNode payload, + final List<QName> keyDefinitions) { + + final Map<QName, Object> mutableCopyUriKeyValues = new HashMap<>(uriKeyValues); + for (final QName keyDefinition : keyDefinitions) { + final Object uriKeyValue = RestconfDocumentedException.throwIfNull( + // should be caught during parsing URI to InstanceIdentifier + mutableCopyUriKeyValues.remove(keyDefinition), ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, + "Missing key %s in URI.", keyDefinition); + + final Object dataKeyValue = payload.getIdentifier().getValue(keyDefinition); + + if (!Objects.deepEquals(uriKeyValue, 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); + } + } + } + + @Override + public Response createConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + return createConfigurationData(payload, uriInfo); + } + + @Override + public Response createConfigurationData(final NormalizedNodeContext payload, final UriInfo uriInfo) { + if (payload == null) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + final DOMMountPoint mountPoint = payload.getInstanceIdentifierContext().getMountPoint(); + final InstanceIdentifierContext iiWithData = payload.getInstanceIdentifierContext(); + final YangInstanceIdentifier normalizedII = iiWithData.getInstanceIdentifier(); + + boolean insertUsed = false; + boolean pointUsed = false; + String insert = null; + String point = null; + + if (uriInfo != null) { + for (final Entry<String, List<String>> 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()); + } + } + } + + 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."); + } + + FluentFuture<? extends CommitInfo> future; + if (mountPoint != null) { + future = broker.commitConfigurationDataPost(mountPoint, normalizedII, payload.getData(), insert, + point); + } else { + future = broker.commitConfigurationDataPost(controllerContext.getGlobalSchema(), normalizedII, + payload.getData(), insert, point); + } + + try { + future.get(); + } catch (final InterruptedException e) { + LOG.info("Error creating data {}", uriInfo != null ? uriInfo.getPath() : "", e); + throw new RestconfDocumentedException(e.getMessage(), e); + } catch (final ExecutionException e) { + LOG.info("Error creating data {}", uriInfo != null ? uriInfo.getPath() : "", e); + throw RestconfDocumentedException.decodeAndThrow(e.getMessage(), Throwables.getCauseAs(e, + TransactionCommitFailedException.class)); + } + + LOG.trace("Successfuly created data."); + + final ResponseBuilder responseBuilder = Response.status(Status.NO_CONTENT); + // FIXME: Provide path to result. + final URI location = resolveLocation(uriInfo, "", mountPoint, normalizedII); + if (location != null) { + responseBuilder.location(location); + } + return responseBuilder.build(); + } + + @SuppressWarnings("checkstyle:IllegalCatch") + private URI resolveLocation(final UriInfo uriInfo, final String uriBehindBase, final DOMMountPoint mountPoint, + final YangInstanceIdentifier normalizedII) { + if (uriInfo == null) { + // This is null if invoked internally + return null; + } + + final UriBuilder uriBuilder = uriInfo.getBaseUriBuilder(); + uriBuilder.path("config"); + try { + uriBuilder.path(controllerContext.toFullRestconfIdentifier(normalizedII, mountPoint)); + } catch (final Exception e) { + LOG.info("Location for instance identifier {} was not created", normalizedII, e); + return null; + } + return uriBuilder.build(); + } + + @Override + public Response deleteConfigurationData(final String identifier) { + final InstanceIdentifierContext iiWithData = controllerContext.toInstanceIdentifier(identifier); + final DOMMountPoint mountPoint = iiWithData.getMountPoint(); + final YangInstanceIdentifier normalizedII = iiWithData.getInstanceIdentifier(); + + final FluentFuture<? extends CommitInfo> future; + if (mountPoint != null) { + future = broker.commitConfigurationDataDelete(mountPoint, normalizedII); + } else { + future = broker.commitConfigurationDataDelete(normalizedII); + } + + try { + future.get(); + } catch (final InterruptedException e) { + throw new RestconfDocumentedException(e.getMessage(), e); + } catch (final ExecutionException e) { + final Optional<Throwable> searchedException = Iterables.tryFind(Throwables.getCausalChain(e), + Predicates.instanceOf(ModifiedNodeDoesNotExistException.class)).toJavaUtil(); + if (searchedException.isPresent()) { + throw new RestconfDocumentedException("Data specified for delete doesn't exist.", ErrorType.APPLICATION, + ErrorTag.DATA_MISSING, e); + } + + throw RestconfDocumentedException.decodeAndThrow(e.getMessage(), Throwables.getCauseAs(e, + TransactionCommitFailedException.class)); + } + + return Response.status(Status.OK).build(); + } + + /** + * Subscribes to some path in schema context (stream) to listen on changes + * on this stream. + * + * <p> + * Additional parameters for subscribing to stream are loaded via rpc input + * parameters: + * <ul> + * <li>datastore - default CONFIGURATION (other values of + * {@link LogicalDatastoreType} enum type)</li> + * <li>scope - default BASE (other values of {@link Scope})</li> + * </ul> + */ + @Override + public NormalizedNodeContext subscribeToStream(final String identifier, final UriInfo uriInfo) { + boolean startTimeUsed = false; + boolean stopTimeUsed = false; + Instant start = Instant.now(); + Instant stop = null; + boolean filterUsed = false; + String filter = null; + boolean leafNodesOnlyUsed = false; + boolean leafNodesOnly = false; + boolean skipNotificationDataUsed = false; + boolean skipNotificationData = false; + + for (final Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) { + switch (entry.getKey()) { + case "start-time": + if (!startTimeUsed) { + startTimeUsed = true; + start = parseDateFromQueryParam(entry); + } else { + throw new RestconfDocumentedException("Start-time parameter can be used only once."); + } + break; + case "stop-time": + if (!stopTimeUsed) { + stopTimeUsed = true; + stop = parseDateFromQueryParam(entry); + } else { + throw new RestconfDocumentedException("Stop-time parameter can be used only once."); + } + break; + case "filter": + if (!filterUsed) { + filterUsed = true; + filter = entry.getValue().iterator().next(); + } else { + throw new RestconfDocumentedException("Filter parameter can be used only once."); + } + break; + case "odl-leaf-nodes-only": + if (!leafNodesOnlyUsed) { + leafNodesOnlyUsed = true; + leafNodesOnly = Boolean.parseBoolean(entry.getValue().iterator().next()); + } else { + throw new RestconfDocumentedException("Odl-leaf-nodes-only parameter can be used only once."); + } + break; + case "odl-skip-notification-data": + if (!skipNotificationDataUsed) { + skipNotificationDataUsed = true; + skipNotificationData = Boolean.parseBoolean(entry.getValue().iterator().next()); + } else { + throw new RestconfDocumentedException( + "Odl-skip-notification-data parameter can be used only once."); + } + break; + default: + throw new RestconfDocumentedException("Bad parameter used with notifications: " + entry.getKey()); + } + } + if (!startTimeUsed && stopTimeUsed) { + throw new RestconfDocumentedException("Stop-time parameter has to be used with start-time parameter."); + } + URI response = null; + if (identifier.contains(DATA_SUBSCR)) { + response = dataSubs(identifier, uriInfo, start, stop, filter, leafNodesOnly, skipNotificationData); + } else if (identifier.contains(NOTIFICATION_STREAM)) { + response = notifStream(identifier, uriInfo, start, stop, filter); + } + + if (response != null) { + // prepare node with value of location + + final QName qnameBase = QName.create("subscribe:to:notification", "2016-10-28", "notifi"); + final QName locationQName = QName.create(qnameBase, "location"); + + final var stack = SchemaInferenceStack.of(controllerContext.getGlobalSchema()); + stack.enterSchemaTree(qnameBase); + stack.enterSchemaTree(locationQName); + + // prepare new header with location + return new NormalizedNodeContext(InstanceIdentifierContext.ofStack(stack), + ImmutableNodes.leafNode(locationQName, response.toString()), ImmutableMap.of("Location", response)); + } + + final String msg = "Bad type of notification of sal-remote"; + LOG.warn(msg); + throw new RestconfDocumentedException(msg); + } + + private static Instant parseDateFromQueryParam(final Entry<String, List<String>> entry) { + final DateAndTime event = new DateAndTime(entry.getValue().iterator().next()); + final String value = event.getValue(); + final TemporalAccessor p; + try { + p = FORMATTER.parse(value); + } catch (final DateTimeParseException e) { + throw new RestconfDocumentedException("Cannot parse of value in date: " + value, e); + } + return Instant.from(p); + } + + /** + * Register notification listener by stream name. + * + * @param identifier + * stream name + * @param uriInfo + * uriInfo + * @param stop + * stop-time of getting notification + * @param start + * start-time of getting notification + * @param filter + * indicate which subset of all possible events are of interest + * @return {@link URI} of location + */ + private URI notifStream(final String identifier, final UriInfo uriInfo, final Instant start, + final Instant stop, final String filter) { + final String streamName = Notificator.createStreamNameFromUri(identifier); + if (Strings.isNullOrEmpty(streamName)) { + throw new RestconfDocumentedException("Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + final List<NotificationListenerAdapter> listeners = Notificator.getNotificationListenerFor(streamName); + if (listeners == null || listeners.isEmpty()) { + throw new RestconfDocumentedException("Stream was not found.", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT); + } + + for (final NotificationListenerAdapter listener : listeners) { + broker.registerToListenNotification(listener); + listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), false, false); + } + + final UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder(); + + final WebSocketServer webSocketServerInstance = WebSocketServer.getInstance(NOTIFICATION_PORT); + final int notificationPort = webSocketServerInstance.getPort(); + + + final UriBuilder uriToWebsocketServerBuilder = uriBuilder.port(notificationPort).scheme(getWsScheme(uriInfo)); + + return uriToWebsocketServerBuilder.replacePath(streamName).build(); + } + + private static String getWsScheme(final UriInfo uriInfo) { + URI uri = uriInfo.getAbsolutePath(); + if (uri == null) { + return "ws"; + } + String subscriptionScheme = uri.getScheme().toLowerCase(Locale.ROOT); + return subscriptionScheme.equals("https") ? "wss" : "ws"; + } + + /** + * Register data change listener by stream name. + * + * @param identifier + * stream name + * @param uriInfo + * uri info + * @param stop + * start-time of getting notification + * @param start + * stop-time of getting notification + * @param filter + * indicate which subset of all possible events are of interest + * @return {@link URI} of location + */ + private URI dataSubs(final String identifier, final UriInfo uriInfo, final Instant start, final Instant stop, + final String filter, final boolean leafNodesOnly, final boolean skipNotificationData) { + final String streamName = Notificator.createStreamNameFromUri(identifier); + if (Strings.isNullOrEmpty(streamName)) { + throw new RestconfDocumentedException("Stream name is empty.", ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE); + } + + final ListenerAdapter listener = Notificator.getListenerFor(streamName); + if (listener == null) { + throw new RestconfDocumentedException("Stream was not found.", ErrorType.PROTOCOL, + ErrorTag.UNKNOWN_ELEMENT); + } + listener.setQueryParams(start, Optional.ofNullable(stop), Optional.ofNullable(filter), leafNodesOnly, + skipNotificationData); + + final Map<String, String> paramToValues = resolveValuesFromUri(identifier); + final LogicalDatastoreType datastore = + parserURIEnumParameter(LogicalDatastoreType.class, paramToValues.get(DATASTORE_PARAM_NAME)); + if (datastore == null) { + throw new RestconfDocumentedException("Stream name doesn't contains datastore value (pattern /datastore=)", + ErrorType.APPLICATION, ErrorTag.MISSING_ATTRIBUTE); + } + final Scope scope = parserURIEnumParameter(Scope.class, paramToValues.get(SCOPE_PARAM_NAME)); + if (scope == null) { + throw new RestconfDocumentedException("Stream name doesn't contains datastore value (pattern /scope=)", + ErrorType.APPLICATION, ErrorTag.MISSING_ATTRIBUTE); + } + + broker.registerToListenDataChanges(datastore, scope, listener); + + final UriBuilder uriBuilder = uriInfo.getAbsolutePathBuilder(); + + final WebSocketServer webSocketServerInstance = WebSocketServer.getInstance(NOTIFICATION_PORT); + final int notificationPort = webSocketServerInstance.getPort(); + + final UriBuilder uriToWebsocketServerBuilder = uriBuilder.port(notificationPort).scheme(getWsScheme(uriInfo)); + + return uriToWebsocketServerBuilder.replacePath(streamName).build(); + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public PatchStatusContext patchConfigurationData(final String identifier, final PatchContext context, + final UriInfo uriInfo) { + if (context == null) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + try { + return broker.patchConfigurationDataWithinTransaction(context); + } catch (final Exception e) { + LOG.debug("Patch transaction failed", e); + throw new RestconfDocumentedException(e.getMessage(), e); + } + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public PatchStatusContext patchConfigurationData(final PatchContext context, @Context final UriInfo uriInfo) { + if (context == null) { + throw new RestconfDocumentedException("Input is required.", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); + } + + try { + return broker.patchConfigurationDataWithinTransaction(context); + } catch (final Exception e) { + LOG.debug("Patch transaction failed", e); + throw new RestconfDocumentedException(e.getMessage(), e); + } + } + + /** + * Load parameter for subscribing to stream from input composite node. + * + * @param value + * contains value + * @return enum object if its string value is equal to {@code paramName}. In + * other cases null. + */ + private static <T> T parseEnumTypeParameter(final ContainerNode value, final Class<T> classDescriptor, + final String paramName) { + final Optional<DataContainerChild> optAugNode = value.findChildByArg(SAL_REMOTE_AUG_IDENTIFIER); + if (optAugNode.isEmpty()) { + return null; + } + final DataContainerChild augNode = optAugNode.get(); + if (!(augNode instanceof AugmentationNode)) { + return null; + } + final Optional<DataContainerChild> enumNode = ((AugmentationNode) augNode).findChildByArg( + new NodeIdentifier(QName.create(SAL_REMOTE_AUGMENT, paramName))); + if (enumNode.isEmpty()) { + return null; + } + final Object rawValue = enumNode.get().body(); + if (!(rawValue instanceof String)) { + return null; + } + + return resolveAsEnum(classDescriptor, (String) rawValue); + } + + /** + * Checks whether {@code value} is one of the string representation of + * enumeration {@code classDescriptor}. + * + * @return enum object if string value of {@code classDescriptor} + * enumeration is equal to {@code value}. Other cases null. + */ + private static <T> T parserURIEnumParameter(final Class<T> classDescriptor, final String value) { + if (Strings.isNullOrEmpty(value)) { + return null; + } + return resolveAsEnum(classDescriptor, value); + } + + private static <T> T resolveAsEnum(final Class<T> classDescriptor, final String value) { + final T[] enumConstants = classDescriptor.getEnumConstants(); + if (enumConstants != null) { + for (final T enm : classDescriptor.getEnumConstants()) { + if (((Enum<?>) enm).name().equals(value)) { + return enm; + } + } + } + return null; + } + + private static Map<String, String> resolveValuesFromUri(final String uri) { + final Map<String, String> result = new HashMap<>(); + final String[] tokens = uri.split("/"); + for (int i = 1; i < tokens.length; i++) { + final String[] parameterTokens = tokens[i].split("="); + if (parameterTokens.length == 2) { + result.put(parameterTokens[0], parameterTokens[1]); + } + } + return result; + } + + private MapNode makeModuleMapNode(final Collection<? extends Module> modules) { + requireNonNull(modules); + final Module restconfModule = getRestconfModule(); + final DataSchemaNode moduleSchemaNode = controllerContext + .getRestconfModuleRestConfSchemaNode(restconfModule, Draft02.RestConfModule.MODULE_LIST_SCHEMA_NODE); + checkState(moduleSchemaNode instanceof ListSchemaNode); + + final CollectionNodeBuilder<MapEntryNode, SystemMapNode> listModuleBuilder = + SchemaAwareBuilders.mapBuilder((ListSchemaNode) moduleSchemaNode); + + for (final Module module : modules) { + listModuleBuilder.withChild(toModuleEntryNode(module, moduleSchemaNode)); + } + return listModuleBuilder.build(); + } + + private static MapEntryNode toModuleEntryNode(final Module module, final DataSchemaNode moduleSchemaNode) { + checkArgument(moduleSchemaNode instanceof ListSchemaNode, + "moduleSchemaNode has to be of type ListSchemaNode"); + final ListSchemaNode listModuleSchemaNode = (ListSchemaNode) moduleSchemaNode; + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> moduleNodeValues = + SchemaAwareBuilders.mapEntryBuilder(listModuleSchemaNode); + + var instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listModuleSchemaNode, "name"); + final LeafSchemaNode nameSchemaNode = getFirstLeaf(instanceDataChildrenByName); + moduleNodeValues.withChild( + SchemaAwareBuilders.leafBuilder(nameSchemaNode).withValue(module.getName()).build()); + + final QNameModule qNameModule = module.getQNameModule(); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listModuleSchemaNode, "revision"); + final LeafSchemaNode revisionSchemaNode = getFirstLeaf(instanceDataChildrenByName); + final Optional<Revision> revision = qNameModule.getRevision(); + moduleNodeValues.withChild(SchemaAwareBuilders.leafBuilder(revisionSchemaNode) + .withValue(revision.map(Revision::toString).orElse("")).build()); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listModuleSchemaNode, "namespace"); + final LeafSchemaNode namespaceSchemaNode = getFirstLeaf(instanceDataChildrenByName); + moduleNodeValues.withChild(SchemaAwareBuilders.leafBuilder(namespaceSchemaNode) + .withValue(qNameModule.getNamespace().toString()).build()); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listModuleSchemaNode, "feature"); + final LeafListSchemaNode featureSchemaNode = getFirst(instanceDataChildrenByName, LeafListSchemaNode.class); + final ListNodeBuilder<Object, SystemLeafSetNode<Object>> featuresBuilder = + SchemaAwareBuilders.leafSetBuilder(featureSchemaNode); + for (final FeatureDefinition feature : module.getFeatures()) { + featuresBuilder.withChild(SchemaAwareBuilders.leafSetEntryBuilder(featureSchemaNode) + .withValue(feature.getQName().getLocalName()).build()); + } + moduleNodeValues.withChild(featuresBuilder.build()); + + return moduleNodeValues.build(); + } + + protected MapEntryNode toStreamEntryNode(final String streamName, final DataSchemaNode streamSchemaNode) { + checkArgument(streamSchemaNode instanceof ListSchemaNode, + "streamSchemaNode has to be of type ListSchemaNode"); + final ListSchemaNode listStreamSchemaNode = (ListSchemaNode) streamSchemaNode; + final DataContainerNodeBuilder<NodeIdentifierWithPredicates, MapEntryNode> streamNodeValues = + SchemaAwareBuilders.mapEntryBuilder(listStreamSchemaNode); + + var instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listStreamSchemaNode, "name"); + final LeafSchemaNode nameSchemaNode = getFirstLeaf(instanceDataChildrenByName); + streamNodeValues.withChild( + SchemaAwareBuilders.leafBuilder(nameSchemaNode).withValue(streamName).build()); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listStreamSchemaNode, "description"); + final LeafSchemaNode descriptionSchemaNode = getFirstLeaf(instanceDataChildrenByName); + streamNodeValues.withChild(SchemaAwareBuilders.leafBuilder(descriptionSchemaNode) + .withValue("DESCRIPTION_PLACEHOLDER") + .build()); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listStreamSchemaNode, "replay-support"); + final LeafSchemaNode replaySupportSchemaNode = getFirstLeaf(instanceDataChildrenByName); + streamNodeValues.withChild(SchemaAwareBuilders.leafBuilder(replaySupportSchemaNode) + .withValue(Boolean.TRUE).build()); + + instanceDataChildrenByName = + ControllerContext.findInstanceDataChildrenByName(listStreamSchemaNode, "replay-log-creation-time"); + final LeafSchemaNode replayLogCreationTimeSchemaNode = getFirstLeaf(instanceDataChildrenByName); + streamNodeValues.withChild( + SchemaAwareBuilders.leafBuilder(replayLogCreationTimeSchemaNode).withValue("").build()); + + instanceDataChildrenByName = ControllerContext.findInstanceDataChildrenByName(listStreamSchemaNode, "events"); + final LeafSchemaNode eventsSchemaNode = getFirstLeaf(instanceDataChildrenByName); + streamNodeValues.withChild( + SchemaAwareBuilders.leafBuilder(eventsSchemaNode).withValue(Empty.value()).build()); + + return streamNodeValues.build(); + } + + /** + * Prepare stream for notification. + * + * @param payload + * contains list of qnames of notifications + * @return - checked future object + */ + private ListenableFuture<DOMRpcResult> invokeSalRemoteRpcNotifiStrRPC(final NormalizedNodeContext payload) { + final ContainerNode data = (ContainerNode) payload.getData(); + LeafSetNode leafSet = null; + String outputType = "XML"; + for (final DataContainerChild dataChild : data.body()) { + if (dataChild instanceof LeafSetNode) { + leafSet = (LeafSetNode) dataChild; + } else if (dataChild instanceof AugmentationNode) { + outputType = (String) ((AugmentationNode) dataChild).body().iterator().next().body(); + } + } + + final Collection<LeafSetEntryNode<?>> entryNodes = leafSet.body(); + final List<Absolute> paths = new ArrayList<>(); + + StringBuilder streamNameBuilder = new StringBuilder(CREATE_NOTIFICATION_STREAM).append('/'); + final Iterator<LeafSetEntryNode<?>> iterator = entryNodes.iterator(); + while (iterator.hasNext()) { + final QName valueQName = QName.create((String) iterator.next().body()); + final XMLNamespace namespace = valueQName.getModule().getNamespace(); + final Module module = controllerContext.findModuleByNamespace(namespace); + checkNotNull(module, "Module for namespace %s does not exist", namespace); + NotificationDefinition notifiDef = null; + for (final NotificationDefinition notification : module.getNotifications()) { + if (notification.getQName().equals(valueQName)) { + notifiDef = notification; + break; + } + } + final String moduleName = module.getName(); + if (notifiDef == null) { + throw new IllegalArgumentException("Notification " + valueQName + " does not exist in module " + + moduleName); + } + + paths.add(Absolute.of(notifiDef.getQName())); + streamNameBuilder.append(moduleName).append(':').append(valueQName.getLocalName()); + if (iterator.hasNext()) { + streamNameBuilder.append(','); + } + } + + final String streamName = streamNameBuilder.toString(); + final QName rpcQName = payload.getInstanceIdentifierContext().getSchemaNode().getQName(); + + if (!Notificator.existNotificationListenerFor(streamName)) { + Notificator.createNotificationListener(paths, streamName, outputType, controllerContext); + } + + return Futures.immediateFuture(new DefaultDOMRpcResult(Builders.containerBuilder() + .withNodeIdentifier(new NodeIdentifier(QName.create(rpcQName, "output"))) + .withChild(ImmutableNodes.leafNode(QName.create(rpcQName, "notification-stream-identifier"), streamName)) + .build())); + } + + private static LeafSchemaNode getFirstLeaf(final List<FoundChild> children) { + return getFirst(children, LeafSchemaNode.class); + } + + private static <T extends DataSchemaNode> T getFirst(final List<FoundChild> children, final Class<T> expected) { + checkState(!children.isEmpty()); + final var first = children.get(0); + checkState(expected.isInstance(first.child)); + return expected.cast(first.child); + } + + private static EffectiveModelContext modelContext(final DOMMountPoint mountPoint) { + return mountPoint.getService(DOMSchemaService.class) + .flatMap(svc -> Optional.ofNullable(svc.getGlobalContext())) + .orElse(null); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfProviderImpl.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfProviderImpl.java new file mode 100644 index 0000000..5b6608e --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/RestconfProviderImpl.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import static java.util.Objects.requireNonNull; + +import java.math.BigInteger; +import org.opendaylight.controller.md.sal.common.util.jmx.AbstractMXBean; +import org.opendaylight.netconf.sal.rest.api.RestConnector; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Config; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Delete; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Get; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Operational; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Post; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Put; +import org.opendaylight.netconf.sal.restconf.impl.jmx.RestConnectorRuntimeMXBean; +import org.opendaylight.netconf.sal.restconf.impl.jmx.Rpcs; +import org.opendaylight.netconf.sal.streams.websockets.WebSocketServer; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.IpAddress; +import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.inet.types.rev130715.PortNumber; + +public class RestconfProviderImpl extends AbstractMXBean + implements AutoCloseable, RestConnector, RestConnectorRuntimeMXBean { + private final IpAddress websocketAddress; + private final PortNumber websocketPort; + private final StatisticsRestconfServiceWrapper stats; + private Thread webSocketServerThread; + + public RestconfProviderImpl(final StatisticsRestconfServiceWrapper stats, final IpAddress websocketAddress, + final PortNumber websocketPort) { + super("Draft02ProviderStatistics", "restconf-connector", null); + this.stats = requireNonNull(stats); + this.websocketAddress = requireNonNull(websocketAddress); + this.websocketPort = requireNonNull(websocketPort); + } + + public void start() { + this.webSocketServerThread = new Thread(WebSocketServer.createInstance( + websocketAddress.stringValue(), websocketPort.getValue().toJava())); + this.webSocketServerThread.setName("Web socket server on port " + websocketPort); + this.webSocketServerThread.start(); + + registerMBean(); + } + + @Override + public void close() { + WebSocketServer.destroyInstance(); + if (this.webSocketServerThread != null) { + this.webSocketServerThread.interrupt(); + } + + unregisterMBean(); + } + + @Override + public Config getConfig() { + final Config config = new Config(); + + final Get get = new Get(); + get.setReceivedRequests(this.stats.getConfigGet()); + get.setSuccessfulResponses(this.stats.getSuccessGetConfig()); + get.setFailedResponses(this.stats.getFailureGetConfig()); + config.setGet(get); + + final Post post = new Post(); + post.setReceivedRequests(this.stats.getConfigPost()); + post.setSuccessfulResponses(this.stats.getSuccessPost()); + post.setFailedResponses(this.stats.getFailurePost()); + config.setPost(post); + + final Put put = new Put(); + put.setReceivedRequests(this.stats.getConfigPut()); + put.setSuccessfulResponses(this.stats.getSuccessPut()); + put.setFailedResponses(this.stats.getFailurePut()); + config.setPut(put); + + final Delete delete = new Delete(); + delete.setReceivedRequests(this.stats.getConfigDelete()); + delete.setSuccessfulResponses(this.stats.getSuccessDelete()); + delete.setFailedResponses(this.stats.getFailureDelete()); + config.setDelete(delete); + + return config; + } + + @Override + public Operational getOperational() { + final BigInteger opGet = this.stats.getOperationalGet(); + final Operational operational = new Operational(); + final Get get = new Get(); + get.setReceivedRequests(opGet); + get.setSuccessfulResponses(this.stats.getSuccessGetOperational()); + get.setFailedResponses(this.stats.getFailureGetOperational()); + operational.setGet(get); + return operational; + } + + @Override + public Rpcs getRpcs() { + final BigInteger rpcInvoke = this.stats.getRpc(); + final Rpcs rpcs = new Rpcs(); + rpcs.setReceivedRequests(rpcInvoke); + return rpcs; + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java new file mode 100644 index 0000000..afe4be0 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/StatisticsRestconfServiceWrapper.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl; + +import java.math.BigInteger; +import java.util.concurrent.atomic.AtomicLong; +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.UriInfo; +import org.opendaylight.netconf.sal.rest.api.RestconfService; +import org.opendaylight.netconf.sal.rest.impl.NormalizedNodeContext; +import org.opendaylight.restconf.common.patch.PatchContext; +import org.opendaylight.restconf.common.patch.PatchStatusContext; + +@Singleton +public final class StatisticsRestconfServiceWrapper implements RestconfService { + + AtomicLong operationalGet = new AtomicLong(); + AtomicLong configGet = new AtomicLong(); + AtomicLong rpc = new AtomicLong(); + AtomicLong configPost = new AtomicLong(); + AtomicLong configPut = new AtomicLong(); + AtomicLong configDelete = new AtomicLong(); + AtomicLong successGetConfig = new AtomicLong(); + AtomicLong successGetOperational = new AtomicLong(); + AtomicLong successPost = new AtomicLong(); + AtomicLong successPut = new AtomicLong(); + AtomicLong successDelete = new AtomicLong(); + AtomicLong failureGetConfig = new AtomicLong(); + AtomicLong failureGetOperational = new AtomicLong(); + AtomicLong failurePost = new AtomicLong(); + AtomicLong failurePut = new AtomicLong(); + AtomicLong failureDelete = new AtomicLong(); + + private final RestconfService delegate; + + @Inject + public StatisticsRestconfServiceWrapper(final RestconfImpl delegate) { + this.delegate = delegate; + } + + /** + * Factory method. + * + * @deprecated Just use {@link #StatisticsRestconfServiceWrapper(RestconfImpl)} constructor instead. + */ + @Deprecated + public static StatisticsRestconfServiceWrapper newInstance(RestconfImpl delegate) { + return new StatisticsRestconfServiceWrapper(delegate); + } + + @Override + public Object getRoot() { + return this.delegate.getRoot(); + } + + @Override + public NormalizedNodeContext getModules(final UriInfo uriInfo) { + return this.delegate.getModules(uriInfo); + } + + @Override + public NormalizedNodeContext getModules(final String identifier, final UriInfo uriInfo) { + return this.delegate.getModules(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext getModule(final String identifier, final UriInfo uriInfo) { + return this.delegate.getModule(identifier, uriInfo); + } + + @Override + public String getOperationsJSON() { + return this.delegate.getOperationsJSON(); + } + + @Override + public String getOperationsXML() { + return this.delegate.getOperationsXML(); + } + + @Override + public NormalizedNodeContext getOperations(final String identifier, final UriInfo uriInfo) { + return this.delegate.getOperations(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext invokeRpc(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + this.rpc.incrementAndGet(); + return this.delegate.invokeRpc(identifier, payload, uriInfo); + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public NormalizedNodeContext readConfigurationData(final String identifier, final UriInfo uriInfo) { + this.configGet.incrementAndGet(); + NormalizedNodeContext normalizedNodeContext = null; + try { + normalizedNodeContext = this.delegate.readConfigurationData(identifier, uriInfo); + if (normalizedNodeContext.getData() != null) { + this.successGetConfig.incrementAndGet(); + } else { + this.failureGetConfig.incrementAndGet(); + } + } catch (final Exception e) { + this.failureGetConfig.incrementAndGet(); + throw e; + } + return normalizedNodeContext; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public NormalizedNodeContext readOperationalData(final String identifier, final UriInfo uriInfo) { + this.operationalGet.incrementAndGet(); + NormalizedNodeContext normalizedNodeContext = null; + try { + normalizedNodeContext = this.delegate.readOperationalData(identifier, uriInfo); + if (normalizedNodeContext.getData() != null) { + this.successGetOperational.incrementAndGet(); + } else { + this.failureGetOperational.incrementAndGet(); + } + } catch (final Exception e) { + this.failureGetOperational.incrementAndGet(); + throw e; + } + return normalizedNodeContext; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Response updateConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + this.configPut.incrementAndGet(); + Response response = null; + try { + response = this.delegate.updateConfigurationData(identifier, payload, uriInfo); + if (response.getStatus() == Status.OK.getStatusCode()) { + this.successPut.incrementAndGet(); + } else { + this.failurePut.incrementAndGet(); + } + } catch (final Exception e) { + this.failurePut.incrementAndGet(); + throw e; + } + return response; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Response createConfigurationData(final String identifier, final NormalizedNodeContext payload, + final UriInfo uriInfo) { + this.configPost.incrementAndGet(); + Response response = null; + try { + response = this.delegate.createConfigurationData(identifier, payload, uriInfo); + if (response.getStatus() == Status.OK.getStatusCode()) { + this.successPost.incrementAndGet(); + } else { + this.failurePost.incrementAndGet(); + } + } catch (final Exception e) { + this.failurePost.incrementAndGet(); + throw e; + } + return response; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Response createConfigurationData(final NormalizedNodeContext payload, final UriInfo uriInfo) { + this.configPost.incrementAndGet(); + Response response = null; + try { + response = this.delegate.createConfigurationData(payload, uriInfo); + if (response.getStatus() == Status.OK.getStatusCode()) { + this.successPost.incrementAndGet(); + } else { + this.failurePost.incrementAndGet(); + } + } catch (final Exception e) { + this.failurePost.incrementAndGet(); + throw e; + } + return response; + } + + @SuppressWarnings("checkstyle:IllegalCatch") + @Override + public Response deleteConfigurationData(final String identifier) { + this.configDelete.incrementAndGet(); + Response response = null; + try { + response = this.delegate.deleteConfigurationData(identifier); + if (response.getStatus() == Status.OK.getStatusCode()) { + this.successDelete.incrementAndGet(); + } else { + this.failureDelete.incrementAndGet(); + } + } catch (final Exception e) { + this.failureDelete.incrementAndGet(); + throw e; + } + return response; + } + + @Override + public NormalizedNodeContext subscribeToStream(final String identifier, final UriInfo uriInfo) { + return this.delegate.subscribeToStream(identifier, uriInfo); + } + + @Override + public NormalizedNodeContext getAvailableStreams(final UriInfo uriInfo) { + return this.delegate.getAvailableStreams(uriInfo); + } + + @Override + public PatchStatusContext patchConfigurationData(final String identifier, final PatchContext payload, + final UriInfo uriInfo) { + return this.delegate.patchConfigurationData(identifier, payload, uriInfo); + } + + @Override + public PatchStatusContext patchConfigurationData(final PatchContext payload, final UriInfo uriInfo) { + return this.delegate.patchConfigurationData(payload, uriInfo); + } + + public BigInteger getConfigDelete() { + return BigInteger.valueOf(this.configDelete.get()); + } + + public BigInteger getConfigGet() { + return BigInteger.valueOf(this.configGet.get()); + } + + public BigInteger getConfigPost() { + return BigInteger.valueOf(this.configPost.get()); + } + + public BigInteger getConfigPut() { + return BigInteger.valueOf(this.configPut.get()); + } + + public BigInteger getOperationalGet() { + return BigInteger.valueOf(this.operationalGet.get()); + } + + public BigInteger getRpc() { + return BigInteger.valueOf(this.rpc.get()); + } + + public BigInteger getSuccessGetConfig() { + return BigInteger.valueOf(this.successGetConfig.get()); + } + + public BigInteger getSuccessGetOperational() { + return BigInteger.valueOf(this.successGetOperational.get()); + } + + public BigInteger getSuccessPost() { + return BigInteger.valueOf(this.successPost.get()); + } + + public BigInteger getSuccessPut() { + return BigInteger.valueOf(this.successPut.get()); + } + + public BigInteger getSuccessDelete() { + return BigInteger.valueOf(this.successDelete.get()); + } + + public BigInteger getFailureGetConfig() { + return BigInteger.valueOf(this.failureGetConfig.get()); + } + + public BigInteger getFailureGetOperational() { + return BigInteger.valueOf(this.failureGetOperational.get()); + } + + public BigInteger getFailurePost() { + return BigInteger.valueOf(this.failurePost.get()); + } + + public BigInteger getFailurePut() { + return BigInteger.valueOf(this.failurePut.get()); + } + + public BigInteger getFailureDelete() { + return BigInteger.valueOf(this.failureDelete.get()); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Config.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Config.java new file mode 100644 index 0000000..a115254 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Config.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +public class Config { + private Delete delete; + + private Post post; + + private Get get; + + private Put put; + + public Delete getDelete() { + return delete; + } + + public void setDelete(Delete delete) { + this.delete = delete; + } + + public Post getPost() { + return post; + } + + public void setPost(Post post) { + this.post = post; + } + + public Get getGet() { + return get; + } + + public void setGet(Get get) { + this.get = get; + } + + public Put getPut() { + return put; + } + + public void setPut(Put put) { + this.put = put; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(delete, post, get, put); + + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Config that = (Config) obj; + if (!java.util.Objects.equals(delete, that.delete)) { + return false; + } + + if (!java.util.Objects.equals(post, that.post)) { + return false; + } + + if (!java.util.Objects.equals(get, that.get)) { + return false; + } + + return java.util.Objects.equals(put, that.put); + + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Delete.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Delete.java new file mode 100644 index 0000000..16a815b --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Delete.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +import java.math.BigInteger; + +public class Delete { + private BigInteger successfulResponses; + + private BigInteger receivedRequests; + + private BigInteger failedResponses; + + public BigInteger getSuccessfulResponses() { + return successfulResponses; + } + + public void setSuccessfulResponses(BigInteger successfulResponses) { + this.successfulResponses = successfulResponses; + } + + public BigInteger getReceivedRequests() { + return receivedRequests; + } + + public void setReceivedRequests(BigInteger receivedRequests) { + this.receivedRequests = receivedRequests; + } + + public BigInteger getFailedResponses() { + return failedResponses; + } + + public void setFailedResponses(BigInteger failedResponses) { + this.failedResponses = failedResponses; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(successfulResponses, receivedRequests, failedResponses); + + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Delete that = (Delete) obj; + if (!java.util.Objects.equals(successfulResponses, that.successfulResponses)) { + return false; + } + + if (!java.util.Objects.equals(receivedRequests, that.receivedRequests)) { + return false; + } + + return java.util.Objects.equals(failedResponses, that.failedResponses); + + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Get.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Get.java new file mode 100644 index 0000000..45603a8 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Get.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +import java.math.BigInteger; + +public class Get { + private BigInteger successfulResponses; + + private BigInteger receivedRequests; + + private BigInteger failedResponses; + + public BigInteger getSuccessfulResponses() { + return successfulResponses; + } + + public void setSuccessfulResponses(BigInteger successfulResponses) { + this.successfulResponses = successfulResponses; + } + + public BigInteger getReceivedRequests() { + return receivedRequests; + } + + public void setReceivedRequests(BigInteger receivedRequests) { + this.receivedRequests = receivedRequests; + } + + public BigInteger getFailedResponses() { + return failedResponses; + } + + public void setFailedResponses(BigInteger failedResponses) { + this.failedResponses = failedResponses; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(successfulResponses, receivedRequests, failedResponses); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Get that = (Get) obj; + if (!java.util.Objects.equals(successfulResponses, that.successfulResponses)) { + return false; + } + + if (!java.util.Objects.equals(receivedRequests, that.receivedRequests)) { + return false; + } + + return java.util.Objects.equals(failedResponses, that.failedResponses); + + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Operational.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Operational.java new file mode 100644 index 0000000..5d9989b --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Operational.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +public class Operational { + private Get get; + + public Get getGet() { + return get; + } + + public void setGet(Get get) { + this.get = get; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(get); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Operational that = (Operational) obj; + return java.util.Objects.equals(get, that.get); + + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Post.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Post.java new file mode 100644 index 0000000..a466c01 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Post.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +import java.math.BigInteger; + +public class Post { + private BigInteger successfulResponses; + + private BigInteger receivedRequests; + + private BigInteger failedResponses; + + public BigInteger getSuccessfulResponses() { + return successfulResponses; + } + + public void setSuccessfulResponses(BigInteger successfulResponses) { + this.successfulResponses = successfulResponses; + } + + public BigInteger getReceivedRequests() { + return receivedRequests; + } + + public void setReceivedRequests(BigInteger receivedRequests) { + this.receivedRequests = receivedRequests; + } + + public BigInteger getFailedResponses() { + return failedResponses; + } + + public void setFailedResponses(BigInteger failedResponses) { + this.failedResponses = failedResponses; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(successfulResponses, receivedRequests, failedResponses); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Post that = (Post) obj; + if (!java.util.Objects.equals(successfulResponses, that.successfulResponses)) { + return false; + } + + if (!java.util.Objects.equals(receivedRequests, that.receivedRequests)) { + return false; + } + + return java.util.Objects.equals(failedResponses, that.failedResponses); + + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Put.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Put.java new file mode 100644 index 0000000..589f02c --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Put.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +import java.math.BigInteger; + +public class Put { + private BigInteger successfulResponses; + + private BigInteger receivedRequests; + + private BigInteger failedResponses; + + public BigInteger getSuccessfulResponses() { + return successfulResponses; + } + + public void setSuccessfulResponses(BigInteger successfulResponses) { + this.successfulResponses = successfulResponses; + } + + public BigInteger getReceivedRequests() { + return receivedRequests; + } + + public void setReceivedRequests(BigInteger receivedRequests) { + this.receivedRequests = receivedRequests; + } + + public BigInteger getFailedResponses() { + return failedResponses; + } + + public void setFailedResponses(BigInteger failedResponses) { + this.failedResponses = failedResponses; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(successfulResponses, receivedRequests, failedResponses); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Put that = (Put) obj; + if (!java.util.Objects.equals(successfulResponses, that.successfulResponses)) { + return false; + } + + if (!java.util.Objects.equals(receivedRequests, that.receivedRequests)) { + return false; + } + + return java.util.Objects.equals(failedResponses, that.failedResponses); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/RestConnectorRuntimeMXBean.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/RestConnectorRuntimeMXBean.java new file mode 100644 index 0000000..cafcb8f --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/RestConnectorRuntimeMXBean.java @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +public interface RestConnectorRuntimeMXBean { + Operational getOperational(); + + Rpcs getRpcs(); + + Config getConfig(); +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Rpcs.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Rpcs.java new file mode 100644 index 0000000..d849a4d --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/Rpcs.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; + +import java.math.BigInteger; + +public class Rpcs { + private BigInteger successfulResponses; + + private BigInteger receivedRequests; + + private BigInteger failedResponses; + + public BigInteger getSuccessfulResponses() { + return successfulResponses; + } + + public void setSuccessfulResponses(BigInteger successfulResponses) { + this.successfulResponses = successfulResponses; + } + + public BigInteger getReceivedRequests() { + return receivedRequests; + } + + public void setReceivedRequests(BigInteger receivedRequests) { + this.receivedRequests = receivedRequests; + } + + public BigInteger getFailedResponses() { + return failedResponses; + } + + public void setFailedResponses(BigInteger failedResponses) { + this.failedResponses = failedResponses; + } + + @Override + public int hashCode() { + return java.util.Objects.hash(successfulResponses, receivedRequests, failedResponses); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final Rpcs that = (Rpcs) obj; + if (!java.util.Objects.equals(successfulResponses, that.successfulResponses)) { + return false; + } + + if (!java.util.Objects.equals(receivedRequests, that.receivedRequests)) { + return false; + } + + return java.util.Objects.equals(failedResponses, that.failedResponses); + } +} diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/package-info.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/package-info.java new file mode 100644 index 0000000..d67ee37 --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/jmx/package-info.java @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2017 Inocybe Technologies and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ + +/** + * This package contains the statistical JMX classes for the Draft02 restconf implementation. Originally these classes + * were generated by the CSS code generator and were moved to this package on conversion to blueprint. + */ +package org.opendaylight.netconf.sal.restconf.impl.jmx; diff --git a/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/package-info.java b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/package-info.java new file mode 100644 index 0000000..e78fddd --- /dev/null +++ b/netconf/restconf/restconf-nb-bierman02/src/main/java/org/opendaylight/netconf/sal/restconf/impl/package-info.java @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2014 Cisco Systems, Inc. and others. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v1.0 which accompanies this distribution, + * and is available at http://www.eclipse.org/legal/epl-v10.html + */ +package org.opendaylight.netconf.sal.restconf.impl;
\ No newline at end of file |