Schema determination is asynchronous 87/109987/12
authorRobert Varga <robert.varga@pantheon.tech>
Thu, 25 Jan 2024 03:47:38 +0000 (04:47 +0100)
committerRobert Varga <nite@hq.sk>
Sun, 18 Feb 2024 14:53:06 +0000 (14:53 +0000)
One of the reasons NetconfDevice needs an Executor is because it is
invoking Future.get() -- which must not happen on the invoking thread,
because then we would not get a response as we would block the Netty
thread.

Refactor schema acquisition to be asynchronous, which means it can be
executed immediately and we can then follow up with schema setup through
a callback.

The introduced asynchrony means we cannot really use things like
Verify.verify() and Preconditions.checkState(). We essentially write the
parsing code from scratch with proper defenses against the dark arts.

This removes artificial content stripping, as that is just not
appropriate -- leading to one test schema being ignored.

JIRA: NETCONF-1233
Change-Id: I527e2d8da6bdbd74baf5ffcd9eae256c2ade9e6c
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/DeviceSourcesResolver.java [deleted file]
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/LibraryModulesSchemas.java
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/NetconfDevice.java
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/NetconfStateSchemas.java
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/NetconfStateSchemasResolverImpl.java
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/api/NetconfDeviceSchemasResolver.java
plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/impl/NetconfMessageTransformUtil.java
plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/NetconfDeviceTest.java
plugins/netconf-client-mdsal/src/test/java/org/opendaylight/netconf/client/mdsal/NetconfStateSchemasTest.java

diff --git a/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/DeviceSourcesResolver.java b/plugins/netconf-client-mdsal/src/main/java/org/opendaylight/netconf/client/mdsal/DeviceSourcesResolver.java
deleted file mode 100644 (file)
index 31075b5..0000000
+++ /dev/null
@@ -1,80 +0,0 @@
-/*
- * Copyright (c) 2019 PANTHEON.tech, 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.client.mdsal;
-
-import static java.util.Objects.requireNonNull;
-
-import com.google.common.collect.Sets;
-import java.util.HashSet;
-import java.util.concurrent.Callable;
-import org.opendaylight.netconf.client.mdsal.api.BaseNetconfSchema;
-import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemasResolver;
-import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
-import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
-import org.opendaylight.netconf.client.mdsal.spi.NetconfDeviceRpc;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Schema building callable.
- */
-final class DeviceSourcesResolver implements Callable<DeviceSources> {
-    private static final Logger LOG = LoggerFactory.getLogger(DeviceSourcesResolver.class);
-
-    private final NetconfSessionPreferences remoteSessionCapabilities;
-    private final NetconfDeviceSchemasResolver stateSchemasResolver;
-    private final NetconfDeviceRpc deviceRpc;
-    private final BaseNetconfSchema baseSchema;
-    private final RemoteDeviceId id;
-
-    DeviceSourcesResolver(final RemoteDeviceId id, final BaseNetconfSchema baseSchema, final NetconfDeviceRpc deviceRpc,
-            final NetconfSessionPreferences remoteSessionCapabilities,
-            final NetconfDeviceSchemasResolver stateSchemasResolver) {
-        this.id = requireNonNull(id);
-        this.baseSchema = requireNonNull(baseSchema);
-        this.deviceRpc = requireNonNull(deviceRpc);
-        this.remoteSessionCapabilities = requireNonNull(remoteSessionCapabilities);
-        this.stateSchemasResolver = requireNonNull(stateSchemasResolver);
-    }
-
-    @Override
-    public DeviceSources call() {
-        final var availableSchemas = stateSchemasResolver.resolve(deviceRpc, remoteSessionCapabilities, id,
-            baseSchema.modelContext());
-        LOG.debug("{}: Schemas exposed by ietf-netconf-monitoring: {}", id,
-            availableSchemas.getAvailableYangSchemasQNames());
-
-        final var requiredSources = new HashSet<>(remoteSessionCapabilities.moduleBasedCaps().keySet());
-        final var providedSources = availableSchemas.getAvailableYangSchemasQNames();
-        final var requiredSourcesNotProvided = Sets.difference(requiredSources, providedSources);
-        if (!requiredSourcesNotProvided.isEmpty()) {
-            LOG.warn("{}: Netconf device does not provide all yang models reported in hello message capabilities,"
-                    + " required but not provided: {}", id, requiredSourcesNotProvided);
-            LOG.warn("{}: Attempting to build schema context from required sources", id);
-        }
-
-        // Here all the sources reported in netconf monitoring are merged with those reported in hello.
-        // It is necessary to perform this since submodules are not mentioned in hello but still required.
-        // This clashes with the option of a user to specify supported yang models manually in configuration
-        // for netconf-connector and as a result one is not able to fully override yang models of a device.
-        // It is only possible to add additional models.
-        final var providedSourcesNotRequired = Sets.difference(providedSources, requiredSources);
-        if (!providedSourcesNotRequired.isEmpty()) {
-            LOG.warn("{}: Netconf device provides additional yang models not reported in "
-                    + "hello message capabilities: {}", id, providedSourcesNotRequired);
-            LOG.warn("{}: Adding provided but not required sources as required to prevent failures", id);
-            LOG.debug("{}: Netconf device reported in hello: {}", id, requiredSources);
-            requiredSources.addAll(providedSourcesNotRequired);
-        }
-
-        final var sourceProvider = availableSchemas instanceof LibraryModulesSchemas libraryModule
-            ? new LibrarySchemaSourceProvider(id, libraryModule.getAvailableModels())
-                : new MonitoringSchemaSourceProvider(id, deviceRpc.domRpcService());
-        return new DeviceSources(requiredSources, providedSources, sourceProvider);
-    }
-}
\ No newline at end of file
index 7c6c7b898a838a75bb9df9330ecc1e30fb52ca9f..afbfb8325442bbff4192cb3deda3fb08dc7c3b52 100644 (file)
@@ -12,9 +12,15 @@ import static com.google.common.base.Preconditions.checkState;
 import static java.util.Objects.requireNonNull;
 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
 import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_GET_NODEID;
+import static org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.YangModuleInfoImpl.qnameOf;
 
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
 import com.google.gson.stream.JsonReader;
 import java.io.IOException;
 import java.io.InputStream;
@@ -26,13 +32,11 @@ import java.net.URLConnection;
 import java.nio.charset.Charset;
 import java.nio.charset.StandardCharsets;
 import java.util.Base64;
-import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import java.util.regex.Pattern;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.stream.XMLStreamException;
@@ -50,6 +54,8 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.librar
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.YangLibrary;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev190104.module.list.Module;
 import org.opendaylight.yangtools.util.xml.UntrustedXML;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.QName;
 import org.opendaylight.yangtools.yang.common.Revision;
 import org.opendaylight.yangtools.yang.common.XMLNamespace;
@@ -86,7 +92,6 @@ import org.xml.sax.SAXException;
  * ietf-netconf-yang-library/modules-state/modules node.
  */
 public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
-
     private static final Logger LOG = LoggerFactory.getLogger(LibraryModulesSchemas.class);
     private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
     private static final EffectiveModelContext LIBRARY_CONTEXT = BindingRuntimeHelpers.createEffectiveModel(
@@ -97,13 +102,10 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
     // FIXME: this is legacy RFC7895, add support for RFC8525 containers, too
     private static final NodeIdentifier MODULES_STATE_NID = NodeIdentifier.create(ModulesState.QNAME);
     private static final NodeIdentifier MODULE_NID = NodeIdentifier.create(Module.QNAME);
-    private static final NodeIdentifier NAME_NID = NodeIdentifier.create(QName.create(Module.QNAME, "name").intern());
-    private static final NodeIdentifier REVISION_NID = NodeIdentifier.create(
-        QName.create(Module.QNAME, "revision").intern());
-    private static final NodeIdentifier SCHEMA_NID = NodeIdentifier.create(
-        QName.create(Module.QNAME, "schema").intern());
-    private static final NodeIdentifier NAMESPACE_NID = NodeIdentifier.create(
-        QName.create(Module.QNAME, "namespace").intern());
+    private static final NodeIdentifier NAME_NID = NodeIdentifier.create(qnameOf("name"));
+    private static final NodeIdentifier REVISION_NID = NodeIdentifier.create(qnameOf("revision"));
+    private static final NodeIdentifier SCHEMA_NID = NodeIdentifier.create(qnameOf("schema"));
+    private static final NodeIdentifier NAMESPACE_NID = NodeIdentifier.create(qnameOf("namespace"));
 
     private static final JSONCodecFactory JSON_CODECS = JSONCodecFactorySupplier.DRAFT_LHOTKA_NETMOD_YANG_JSON_02
             .getShared(LIBRARY_CONTEXT);
@@ -115,6 +117,7 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
             .withNodeIdentifier(NETCONF_GET_NODEID)
             .withChild(NetconfMessageTransformUtil.toFilterStructure(MODULES_STATE_MODULE_LIST, LIBRARY_CONTEXT))
             .build();
+    private static final @NonNull LibraryModulesSchemas EMPTY = new LibraryModulesSchemas(ImmutableMap.of());
 
     private final ImmutableMap<QName, URL> availableModels;
 
@@ -122,10 +125,61 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
         this.availableModels = requireNonNull(availableModels);
     }
 
+    // FIXME: this should work on NetconfRpcService
+    public static ListenableFuture<LibraryModulesSchemas> forDevice(final NetconfDeviceRpc deviceRpc,
+            final RemoteDeviceId deviceId) {
+        final var future = SettableFuture.<LibraryModulesSchemas>create();
+        Futures.addCallback(deviceRpc.invokeNetconf(Get.QNAME, GET_MODULES_STATE_MODULE_LIST_RPC),
+            new FutureCallback<DOMRpcResult>() {
+                @Override
+                public void onSuccess(final DOMRpcResult result) {
+                    onGetModulesStateResult(future, deviceId, result);
+                }
+
+                @Override
+                public void onFailure(final Throwable cause) {
+                    // debug, because we expect this error to be reported by caller
+                    LOG.debug("{}: Unable to detect available schemas", deviceId, cause);
+                    future.setException(cause);
+                }
+            }, MoreExecutors.directExecutor());
+        return future;
+    }
+
+    private static void onGetModulesStateResult(final SettableFuture<LibraryModulesSchemas> future,
+            final RemoteDeviceId deviceId, final DOMRpcResult result) {
+        // Two-pass error reporting: first check if there is a hard error, then log any remaining warnings
+        final var errors = result.errors();
+        if (errors.stream().anyMatch(error -> error.getSeverity() == ErrorSeverity.ERROR)) {
+            // FIXME: a good exception, which can report the contents of errors?
+            future.setException(new OperationFailedException("Failed to get modules-state", errors));
+            return;
+        }
+        for (var error : errors) {
+            LOG.info("{}: schema retrieval warning: {}", deviceId, error);
+        }
+
+        final var value = result.value();
+        if (value == null) {
+            LOG.warn("{}: missing RPC output", deviceId);
+            future.set(EMPTY);
+            return;
+        }
+        final var data = value.childByArg(NETCONF_DATA_NODEID);
+        if (data == null) {
+            LOG.warn("{}: missing RPC data", deviceId);
+            future.set(EMPTY);
+            return;
+        }
+        // FIXME: right: this was never implemented correctly, as the origical code always produces CCE because it
+        //        interpreted 'data' anyxml as a DataContainerNode!
+        future.setException(new UnsupportedOperationException("Missing normalization logic for " + data.prettyTree()));
+    }
+
     public Map<SourceIdentifier, URL> getAvailableModels() {
-        final Map<SourceIdentifier, URL> result = new HashMap<>();
-        for (final Entry<QName, URL> entry : availableModels.entrySet()) {
-            final SourceIdentifier sId = new SourceIdentifier(entry.getKey().getLocalName(),
+        final var result = new HashMap<SourceIdentifier, URL>();
+        for (var entry : availableModels.entrySet()) {
+            final var sId = new SourceIdentifier(entry.getKey().getLocalName(),
                 entry.getKey().getRevision().map(Revision::toString).orElse(null));
             result.put(sId, entry.getValue());
         }
@@ -159,58 +213,23 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
         }
     }
 
-
-    public static LibraryModulesSchemas create(final NetconfDeviceRpc deviceRpc, final RemoteDeviceId deviceId) {
-        final DOMRpcResult moduleListNodeResult;
-        try {
-            moduleListNodeResult = deviceRpc.domRpcService().invokeRpc(Get.QNAME, GET_MODULES_STATE_MODULE_LIST_RPC)
-                .get();
-        } catch (final InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new IllegalStateException(deviceId + ": Interrupted while waiting for response to "
-                    + MODULES_STATE_MODULE_LIST, e);
-        } catch (final ExecutionException e) {
-            LOG.warn("{}: Unable to detect available schemas, get to {} failed", deviceId,
-                    MODULES_STATE_MODULE_LIST, e);
-            return new LibraryModulesSchemas(ImmutableMap.of());
-        }
-
-        if (!moduleListNodeResult.errors().isEmpty()) {
-            LOG.warn("{}: Unable to detect available schemas, get to {} failed, {}",
-                    deviceId, MODULES_STATE_MODULE_LIST, moduleListNodeResult.errors());
-            return new LibraryModulesSchemas(ImmutableMap.of());
-        }
-
-        final Optional<DataContainerChild> modulesStateNode =
-                findModulesStateNode(moduleListNodeResult.value());
-        if (modulesStateNode.isPresent()) {
-            final DataContainerChild node = modulesStateNode.orElseThrow();
-            checkState(node instanceof ContainerNode, "Expecting container containing schemas, but was %s", node);
-            return create((ContainerNode) node);
-        }
-
-        LOG.warn("{}: Unable to detect available schemas, get to {} was empty", deviceId, MODULES_STATE_NID);
-        return new LibraryModulesSchemas(ImmutableMap.of());
-    }
-
     private static LibraryModulesSchemas create(final ContainerNode modulesStateNode) {
         final Optional<DataContainerChild> moduleListNode = modulesStateNode.findChildByArg(MODULE_NID);
         checkState(moduleListNode.isPresent(), "Unable to find list: %s in %s", MODULE_NID, modulesStateNode);
-        final DataContainerChild node = moduleListNode.orElseThrow();
+        final var node = moduleListNode.orElseThrow();
         checkState(node instanceof MapNode, "Unexpected structure for container: %s in : %s. Expecting a list",
             MODULE_NID, modulesStateNode);
 
-        final MapNode moduleList = (MapNode) node;
-        final Collection<MapEntryNode> modules = moduleList.body();
-        final ImmutableMap.Builder<QName, URL> schemasMapping = ImmutableMap.builderWithExpectedSize(modules.size());
-        for (final MapEntryNode moduleNode : modules) {
-            final Entry<QName, URL> entry = createFromEntry(moduleNode);
+        final var moduleList = (MapNode) node;
+        final var builder = ImmutableMap.<QName, URL>builderWithExpectedSize(moduleList.size());
+        for (var moduleNode : moduleList.body()) {
+            final var entry = createFromEntry(moduleNode);
             if (entry != null) {
-                schemasMapping.put(entry);
+                builder.put(entry);
             }
         }
 
-        return new LibraryModulesSchemas(schemasMapping.build());
+        return new LibraryModulesSchemas(builder.build());
     }
 
     /**
@@ -233,18 +252,6 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
         return createFromURLConnection(connection);
     }
 
-    private static Optional<DataContainerChild> findModulesStateNode(final NormalizedNode result) {
-        if (result == null) {
-            return Optional.empty();
-        }
-        final Optional<DataContainerChild> dataNode = ((DataContainerNode) result).findChildByArg(NETCONF_DATA_NODEID);
-        if (dataNode.isEmpty()) {
-            return Optional.empty();
-        }
-
-        return ((DataContainerNode) dataNode.orElseThrow()).findChildByArg(MODULES_STATE_NID);
-    }
-
     private static LibraryModulesSchemas createFromURLConnection(final URLConnection connection) {
 
         String contentType = connection.getContentType();
@@ -293,7 +300,7 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
 
         jsonParser.parse(reader);
 
-        final NormalizationResult result = resultHolder.result();
+        final NormalizationResult<?> result = resultHolder.result();
         return result == null ? Optional.empty() : Optional.of(result.data());
     }
 
index 3caa773c960afff5853113b7a5dfce3ea6a7a89a..8260b755f19a9c114bbb52d12771e548c2a90a05 100644 (file)
@@ -144,8 +144,43 @@ public class NetconfDevice implements RemoteDevice<NetconfDeviceCommunicator> {
         final var baseSchema = baseSchemas.baseSchemaForCapabilities(remoteSessionCapabilities);
         final var initRpc = new NetconfDeviceRpc(baseSchema.modelContext(), listener,
             new NetconfMessageTransformer(baseSchema.mountPointContext(), false, baseSchema));
-        final var sourceResolverFuture = Futures.submit(new DeviceSourcesResolver(id, baseSchema, initRpc,
-                remoteSessionCapabilities, stateSchemasResolver), processingExecutor);
+
+        // Acquire schemas
+        final var futureSchemas = stateSchemasResolver.resolve(initRpc, remoteSessionCapabilities, id,
+            baseSchema.modelContext());
+
+        // Convert to sources
+        final var sourceResolverFuture = Futures.transform(futureSchemas, availableSchemas -> {
+            final var providedSources = availableSchemas.getAvailableYangSchemasQNames();
+            LOG.debug("{}: Schemas exposed by ietf-netconf-monitoring: {}", id, providedSources);
+
+            final var requiredSources = new HashSet<>(remoteSessionCapabilities.moduleBasedCaps().keySet());
+            final var requiredSourcesNotProvided = Sets.difference(requiredSources, providedSources);
+            if (!requiredSourcesNotProvided.isEmpty()) {
+                LOG.warn("{}: Netconf device does not provide all yang models reported in hello message capabilities,"
+                        + " required but not provided: {}", id, requiredSourcesNotProvided);
+                LOG.warn("{}: Attempting to build schema context from required sources", id);
+            }
+
+            // Here all the sources reported in netconf monitoring are merged with those reported in hello.
+            // It is necessary to perform this since submodules are not mentioned in hello but still required.
+            // This clashes with the option of a user to specify supported yang models manually in configuration
+            // for netconf-connector and as a result one is not able to fully override yang models of a device.
+            // It is only possible to add additional models.
+            final var providedSourcesNotRequired = Sets.difference(providedSources, requiredSources);
+            if (!providedSourcesNotRequired.isEmpty()) {
+                LOG.warn("{}: Netconf device provides additional yang models not reported in "
+                        + "hello message capabilities: {}", id, providedSourcesNotRequired);
+                LOG.warn("{}: Adding provided but not required sources as required to prevent failures", id);
+                LOG.debug("{}: Netconf device reported in hello: {}", id, requiredSources);
+                requiredSources.addAll(providedSourcesNotRequired);
+            }
+
+            final var sourceProvider = availableSchemas instanceof LibraryModulesSchemas libraryModule
+                ? new LibrarySchemaSourceProvider(id, libraryModule.getAvailableModels())
+                    : new MonitoringSchemaSourceProvider(id, initRpc.domRpcService());
+            return new DeviceSources(requiredSources, providedSources, sourceProvider);
+        }, MoreExecutors.directExecutor());
 
         if (shouldListenOnSchemaChange(remoteSessionCapabilities)) {
             registerToBaseNetconfStream(initRpc, listener);
index 97703a79c1a4e0bc855f3e838ca5e958355cc888..c8e2ae8349c52e8033862b2e9eb88066223129ef 100644 (file)
@@ -7,24 +7,25 @@
  */
 package org.opendaylight.netconf.client.mdsal;
 
-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 org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
-import static org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil.toId;
+import static org.opendaylight.yang.svc.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.YangModuleInfoImpl.qnameOf;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Strings;
+import com.google.common.base.VerifyException;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.MoreExecutors;
+import com.google.common.util.concurrent.SettableFuture;
 import java.io.IOException;
 import java.net.URISyntaxException;
-import java.util.HashSet;
-import java.util.Optional;
+import java.time.format.DateTimeParseException;
 import java.util.Set;
-import java.util.concurrent.ExecutionException;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.transform.dom.DOMSource;
 import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
 import org.opendaylight.mdsal.dom.api.DOMRpcResult;
 import org.opendaylight.mdsal.dom.api.DOMRpcService;
 import org.opendaylight.netconf.api.NamespaceURN;
@@ -32,7 +33,6 @@ import org.opendaylight.netconf.api.xml.XmlUtil;
 import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemas;
 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
-import org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil;
 import org.opendaylight.netconf.common.mdsal.NormalizedDataUtil;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.Get;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.GetInput;
@@ -41,25 +41,26 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.mon
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.Yang;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.Schemas;
 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.monitoring.rev101004.netconf.state.schemas.Schema.Location;
+import org.opendaylight.yangtools.yang.common.ErrorSeverity;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.QName;
+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.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.AnyxmlNode;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
-import org.opendaylight.yangtools.yang.data.api.schema.DOMSourceAnyxmlNode;
-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.LeafSetEntryNode;
-import org.opendaylight.yangtools.yang.data.api.schema.LeafSetNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
 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.spi.node.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
-import org.w3c.dom.Element;
 import org.w3c.dom.Node;
 import org.w3c.dom.traversal.DocumentTraversal;
 import org.w3c.dom.traversal.NodeFilter;
@@ -73,18 +74,22 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
     public static final NetconfStateSchemas EMPTY = new NetconfStateSchemas(ImmutableSet.of());
 
     private static final Logger LOG = LoggerFactory.getLogger(NetconfStateSchemas.class);
-    private static final YangInstanceIdentifier STATE_SCHEMAS_IDENTIFIER = YangInstanceIdentifier.builder()
-        .node(NetconfState.QNAME).node(Schemas.QNAME).build();
     private static final String MONITORING_NAMESPACE = NetconfState.QNAME.getNamespace().toString();
+    private static final @NonNull NodeIdentifier SCHEMA_FORMAT_NODEID = NodeIdentifier.create(qnameOf("format"));
+    private static final @NonNull NodeIdentifier SCHEMA_LOCATION_NODEID = NodeIdentifier.create(qnameOf("location"));
+    private static final @NonNull NodeIdentifier SCHEMA_NAMESPACE_NODEID = NodeIdentifier.create(qnameOf("namespace"));
+    private static final @NonNull NodeIdentifier SCHEMA_IDENTIFIER_NODEID =
+        NodeIdentifier.create(qnameOf("identifier"));
+    private static final @NonNull NodeIdentifier SCHEMA_VERSION_NODEID = NodeIdentifier.create(qnameOf("version"));
+    private static final @NonNull String NETCONF_LOCATION = Location.Enumeration.NETCONF.getName();
     private static final @NonNull ContainerNode GET_SCHEMAS_RPC;
 
     static {
-        final Document document = XmlUtil.newDocument();
-
-        final Element filterElem = document.createElementNS(NamespaceURN.BASE, "filter");
+        final var document = XmlUtil.newDocument();
+        final var filterElem = document.createElementNS(NamespaceURN.BASE, "filter");
         filterElem.setAttribute("type", "subtree");
 
-        final Element stateElem = document.createElementNS(NetconfState.QNAME.getNamespace().toString(),
+        final var stateElem = document.createElementNS(NetconfState.QNAME.getNamespace().toString(),
             NetconfState.QNAME.getLocalName());
         stateElem.appendChild(document.createElementNS(Schemas.QNAME.getNamespace().toString(),
             Schemas.QNAME.getLocalName()));
@@ -99,127 +104,237 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
             .build();
     }
 
-    private final Set<RemoteYangSchema> availableYangSchemas;
-
-    public NetconfStateSchemas(final Set<RemoteYangSchema> availableYangSchemas) {
-        this.availableYangSchemas = availableYangSchemas;
-    }
+    private final ImmutableSet<QName> availableYangSchemasQNames;
 
-    public Set<RemoteYangSchema> getAvailableYangSchemas() {
-        return availableYangSchemas;
+    public NetconfStateSchemas(final Set<QName> availableYangSchemasQNames) {
+        this.availableYangSchemasQNames = ImmutableSet.copyOf(availableYangSchemasQNames);
     }
 
     @Override
     public Set<QName> getAvailableYangSchemasQNames() {
-        return availableYangSchemas.stream()
-            .map(RemoteYangSchema::getQName)
-            .collect(ImmutableSet.toImmutableSet());
+        return availableYangSchemasQNames;
     }
 
     /**
      * Issue get request to remote device and parse response to find all schemas under netconf-state/schemas.
      */
-    static NetconfStateSchemas create(final DOMRpcService deviceRpc,
+    static ListenableFuture<NetconfStateSchemas> forDevice(final DOMRpcService deviceRpc,
             final NetconfSessionPreferences remoteSessionCapabilities, final RemoteDeviceId id,
-            final EffectiveModelContext schemaContext) {
+            final EffectiveModelContext modelContext) {
         if (!remoteSessionCapabilities.isMonitoringSupported()) {
             // TODO - need to search for get-schema support, not just ietf-netconf-monitoring support
             // issue might be a deviation to ietf-netconf-monitoring where get-schema is unsupported...
             LOG.warn("{}: Netconf monitoring not supported on device, cannot detect provided schemas", id);
-            return EMPTY;
+            return Futures.immediateFuture(EMPTY);
         }
 
-        final DOMRpcResult schemasNodeResult;
+        final var future = SettableFuture.<NetconfStateSchemas>create();
+        Futures.addCallback(deviceRpc.invokeRpc(Get.QNAME, GET_SCHEMAS_RPC),
+            new FutureCallback<DOMRpcResult>() {
+                @Override
+                public void onSuccess(final DOMRpcResult result) {
+                    onGetSchemasResult(future, id, modelContext, result);
+                }
+
+                @Override
+                public void onFailure(final Throwable cause) {
+                    // debug, because we expect this error to be reported by caller
+                    LOG.debug("{}: Unable to detect available schemas", id, cause);
+                    future.setException(cause);
+                }
+            }, MoreExecutors.directExecutor());
+        return future;
+    }
+
+    private static void onGetSchemasResult(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
+            final EffectiveModelContext modelContext, final DOMRpcResult result) {
+        // Two-pass error reporting: first check if there is a hard error, then log any remaining warnings
+        final var errors = result.errors();
+        if (errors.stream().anyMatch(error -> error.getSeverity() == ErrorSeverity.ERROR)) {
+            // FIXME: a good exception, which can report the contents of errors?
+            future.setException(new OperationFailedException("Failed to get netconf-state", errors));
+            return;
+        }
+        for (var error : errors) {
+            LOG.info("{}: schema retrieval warning: {}", id, error);
+        }
+
+        final var value = result.value();
+        if (value == null) {
+            LOG.warn("{}: missing RPC output", id);
+            future.set(EMPTY);
+            return;
+        }
+        final var data = value.childByArg(NETCONF_DATA_NODEID);
+        if (data == null) {
+            LOG.warn("{}: missing RPC data", id);
+            future.set(EMPTY);
+            return;
+        }
+        if (!(data instanceof AnyxmlNode<?> anyxmlData)) {
+            future.setException(new VerifyException("Unexpected data " + data.prettyTree()));
+            return;
+        }
+        final var dataBody = anyxmlData.body();
+        if (!(dataBody instanceof DOMSource domDataBody)) {
+            future.setException(new VerifyException("Unexpected body " + dataBody));
+            return;
+        }
+
+        // Server may include additional data which we do not understand. Make sure we trim the input before we try
+        // to interpret it.
+        // FIXME: we should not be going to NormalizedNode at all. We are interpreting a very limited set of data
+        //        in the context of setting up the normalization schema. Everything we are dealing with are plain
+        //        strings for which yang-common provides everything we need -- with the notable exception of identityref
+        //        values. Those boil down into plain QNames -- so we can talk to XmlCodecs.identityRefCodec(). That
+        //        operation needs to also handle IAE and ignore unknown values.
+        final var filteredBody = ietfMonitoringCopy(domDataBody);
+
+        // Now normalize the anyxml content to the selected model context
+        final NormalizedNode normalizedData;
         try {
-            schemasNodeResult = deviceRpc.invokeRpc(Get.QNAME, GET_SCHEMAS_RPC).get();
-        } catch (final InterruptedException e) {
-            Thread.currentThread().interrupt();
-            throw new IllegalStateException(id
-                    + ": Interrupted while waiting for response to " + STATE_SCHEMAS_IDENTIFIER, e);
-        } catch (final ExecutionException e) {
-            LOG.warn("{}: Unable to detect available schemas, get to {} failed", id, STATE_SCHEMAS_IDENTIFIER, e);
-            return EMPTY;
+            normalizedData = NormalizedDataUtil.transformDOMSourceToNormalizedNode(modelContext, filteredBody)
+                .getResult().data();
+        } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
+            LOG.debug("{}: failed to transform {}", id, filteredBody, e);
+            future.setException(e);
+            return;
+        }
+
+        // The result should be the root of datastore, hence a DataContainerNode
+        if (!(normalizedData instanceof DataContainerNode root)) {
+            future.setException(new VerifyException("Unexpected normalized data " + normalizedData.prettyTree()));
+            return;
         }
 
-        if (!schemasNodeResult.errors().isEmpty()) {
-            LOG.warn("{}: Unable to detect available schemas, get to {} failed, {}",
-                    id, STATE_SCHEMAS_IDENTIFIER, schemasNodeResult.errors());
-            return EMPTY;
+        // container netconf-state
+        final var netconfState = root.childByArg(new NodeIdentifier(NetconfState.QNAME));
+        if (netconfState == null) {
+            LOG.warn("{}: missing netconf-state", id);
+            future.set(EMPTY);
+            return;
+        }
+        if (!(netconfState instanceof ContainerNode netconfStateCont)) {
+            future.setException(new VerifyException("Unexpected netconf-state " + netconfState.prettyTree()));
+            return;
         }
 
-        final Optional<? extends NormalizedNode> optSchemasNode = findSchemasNode(schemasNodeResult.value(),
-                schemaContext);
-        if (optSchemasNode.isEmpty()) {
-            LOG.warn("{}: Unable to detect available schemas, get to {} was empty", id, STATE_SCHEMAS_IDENTIFIER);
-            return EMPTY;
+        // container schemas
+        final var schemas = netconfStateCont.childByArg(new NodeIdentifier(Schemas.QNAME));
+        if (schemas == null) {
+            LOG.warn("{}: missing schemas", id);
+            future.set(EMPTY);
+            return;
+        }
+        if (!(schemas instanceof ContainerNode schemasNode)) {
+            future.setException(new VerifyException("Unexpected schemas " + schemas.prettyTree()));
+            return;
         }
 
-        final NormalizedNode schemasNode = optSchemasNode.orElseThrow();
-        checkState(schemasNode instanceof ContainerNode, "Expecting container containing schemas, but was %s",
-            schemasNode);
-        return create(id, (ContainerNode) schemasNode);
+        create(future, id, schemasNode);
     }
 
     /**
      * Parse response of get(netconf-state/schemas) to find all schemas under netconf-state/schemas.
      */
     @VisibleForTesting
-    protected static NetconfStateSchemas create(final RemoteDeviceId id, final ContainerNode schemasNode) {
-        final Set<RemoteYangSchema> availableYangSchemas = new HashSet<>();
-
-        final DataContainerChild child = schemasNode.childByArg(toId(Schema.QNAME));
-        checkState(child != null, "Unable to find list: %s in response: %s", Schema.QNAME.withoutRevision(),
-            schemasNode);
-        checkState(child instanceof MapNode,
-                "Unexpected structure for container: %s in response: %s. Expecting a list",
-                Schema.QNAME.withoutRevision(), schemasNode);
-
-        for (final MapEntryNode schemaNode : ((MapNode) child).body()) {
-            final Optional<RemoteYangSchema> fromCompositeNode =
-                    RemoteYangSchema.createFromNormalizedNode(id, schemaNode);
-            fromCompositeNode.ifPresent(availableYangSchemas::add);
+    static void create(final SettableFuture<NetconfStateSchemas> future, final RemoteDeviceId id,
+            final ContainerNode schemasNode) {
+        final var child = schemasNode.childByArg(new NodeIdentifier(Schema.QNAME));
+        if (child == null) {
+            LOG.warn("{}: missing schema", id);
+            future.set(EMPTY);
+            return;
+        }
+        if (!(child instanceof SystemMapNode schemaMap)) {
+            future.setException(new VerifyException("Unexpected schemas " + child.prettyTree()));
+            return;
         }
 
-        return new NetconfStateSchemas(availableYangSchemas);
+        // FIXME: we are producing the wrong thing here and simply not handling all the use cases
+        //        - instead of QName we want to say 'SourceIdentifier and XMLNamespace', because these are source files
+        //          and there is some namespace guidance -- which we do not really need (because localName+revision is
+        //          guaranteed to be unique and hence there cannot be a conflict on submodule names
+        //        - we handle on "NETCONF" and completely ignore the URI case -- which is something useful for
+        //          offloading model discovery
+        //
+        //        At the end of the day, all this information is going into yang-parser-impl, i.e. it will need to go
+        //        through SchemaSource and all the yang-repo-{api,spi} stuff. That implies policy and further control
+        //        point which needs to be customizable as we want to plug in various providers and differing policies.
+        //
+        //        A few examples:
+        //        - all URIs need to be resolved, which needs pluggable resolvers (https:// is obvious, but xri:// needs
+        //          to hand this off to a dedicated resolver
+        //        - we do not want to use URI.toURL().openConnection(), but leave it up to policy -- for example one
+        //          would want to use java.net.http.HttpClient, which means authentication and content negotiation.
+        //          Content negotiation is needed to establish byte stream encoding, plus
+        //        - all sources of schema are subject to caching, perhaps even in IRSource form
+        //
+        //        At the end of the day, we should just produce an instance of Schema.class and let others deal with
+        //        translating it to the real world -- for example turning a String into a XMLNamespace or a local name.
+        final var builder = ImmutableSet.<QName>builderWithExpectedSize(schemaMap.size());
+        for (var schemaNode : schemaMap.body()) {
+            final var qname = createFromNormalizedNode(id, schemaNode);
+            if (qname != null) {
+                builder.add(qname);
+            }
+        }
+        future.set(new NetconfStateSchemas(builder.build()));
     }
 
-    private static Optional<? extends NormalizedNode> findSchemasNode(final NormalizedNode result,
-            final EffectiveModelContext schemaContext) {
-        if (result == null) {
-            return Optional.empty();
+    private static @Nullable QName createFromNormalizedNode(final RemoteDeviceId id, final MapEntryNode schemaEntry) {
+        // These three are mandatory due to 'key "identifier version format"'
+        final var format = schemaEntry.getChildByArg(SCHEMA_FORMAT_NODEID).body();
+        // FIXME: we should support Yin as well
+        if (!Yang.QNAME.equals(format)) {
+            LOG.debug("{}: Ignoring schema due to unsupported format: {}", id, format);
+            return null;
         }
-        // FIXME: unchecked cast
-        final var rpcResult = ((ContainerNode) result).childByArg(NETCONF_DATA_NODEID);
-        if (rpcResult == null) {
-            return Optional.empty();
+        // Note: module name or submodule name
+        final var identifier = (String) schemaEntry.getChildByArg(SCHEMA_IDENTIFIER_NODEID).body();
+        // Note: revision
+        final var version = (String) schemaEntry.getChildByArg(SCHEMA_VERSION_NODEID).body();
+
+        // FIXME: we should be able to promote to 'getChildByArg()', IFF the normalizer is enforcing mandatory nodes
+        @SuppressWarnings("unchecked")
+        final var namespaceLeaf = (LeafNode<String>) schemaEntry.childByArg(SCHEMA_NAMESPACE_NODEID);
+        if (namespaceLeaf == null) {
+            LOG.warn("{}: Ignoring schema due to missing namespace", id);
+            return null;
         }
 
-        verify(rpcResult instanceof DOMSourceAnyxmlNode, "Unexpected result %s", rpcResult);
+        @SuppressWarnings("unchecked")
+        final var location = (SystemLeafSetNode<String>) schemaEntry.childByArg(SCHEMA_LOCATION_NODEID);
+        if (location == null) {
+            LOG.debug("{}: Ignoring schema due to missing location", id);
+            return null;
+        }
 
-        // Server may include additional data which we do not understand. Make sure we trim the input before we try
-        // to interpret it.
-        // FIXME: this is something NetconfUtil.transformDOMSourceToNormalizedNode(), and more generally, NormalizedNode
-        //        codecs should handle. We really want to a NormalizedNode tree which can be directly queried for known
-        //        things while completely ignoring XML content (and hence its semantics) of other elements.
-        final var filteredBody = ietfMonitoringCopy(((DOMSourceAnyxmlNode) rpcResult).body());
+        boolean foundNetconf = false;
+        for (var locEntry : location.body()) {
+            final var loc = locEntry.body();
+            if (NETCONF_LOCATION.equals(loc)) {
+                foundNetconf = true;
+                break;
+            }
 
-        final NormalizedNode dataNode;
-        try {
-            dataNode = NormalizedDataUtil.transformDOMSourceToNormalizedNode(schemaContext, filteredBody).getResult()
-                .data();
-        } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
-            LOG.warn("Failed to transform {}", rpcResult, e);
-            return Optional.empty();
+            // FIXME: the other string is an Uri, we should be exposing that as well
+            LOG.debug("{}: Ignoring schema due to unsupported location: {}", id, loc);
         }
 
-        // FIXME: unchecked cast
-        final var nStateNode = ((DataContainerNode) dataNode).childByArg(toId(NetconfState.QNAME));
-        if (nStateNode == null) {
-            return Optional.empty();
+        if (!foundNetconf) {
+            LOG.debug("{}: Ignoring schema due to no NETCONF location", id);
+            return null;
         }
 
-        // FIXME: unchecked cast
-        return ((DataContainerNode) nStateNode).findChildByArg(toId(Schemas.QNAME));
+        try {
+            final var namespace = XMLNamespace.of(namespaceLeaf.body());
+            final var revision = version.isEmpty() ? null : Revision.of(version);
+            return QName.create(namespace, revision, identifier);
+        } catch (DateTimeParseException | IllegalArgumentException e) {
+            LOG.warn("{}: Ignoring malformed schema {}", id, schemaEntry.prettyTree(), e);
+            return null;
+        }
     }
 
     @VisibleForTesting
@@ -252,108 +367,4 @@ public final class NetconfStateSchemas implements NetconfDeviceSchemas {
             }
         }
     }
-
-    public static final class RemoteYangSchema {
-        private final QName qname;
-
-        RemoteYangSchema(final QName qname) {
-            this.qname = qname;
-        }
-
-        public QName getQName() {
-            return qname;
-        }
-
-        static Optional<RemoteYangSchema> createFromNormalizedNode(final RemoteDeviceId id,
-                                                                   final MapEntryNode schemaNode) {
-            final QName schemaNodeId = schemaNode.name().getNodeType();
-            checkArgument(schemaNodeId.equals(Schema.QNAME), "Wrong QName %s", schemaNodeId);
-
-            QName childNode = NetconfMessageTransformUtil.IETF_NETCONF_MONITORING_SCHEMA_FORMAT;
-
-            final String formatAsString = getSingleChildNodeValue(schemaNode, childNode).orElseThrow();
-
-            if (!formatAsString.equals(Yang.QNAME.toString())) {
-                LOG.debug("{}: Ignoring schema due to unsupported format: {}", id, formatAsString);
-                return Optional.empty();
-            }
-
-            childNode = NetconfMessageTransformUtil.IETF_NETCONF_MONITORING_SCHEMA_LOCATION;
-            final Set<String> locationsAsString = getAllChildNodeValues(schemaNode, childNode);
-            if (!locationsAsString.contains(Schema.Location.Enumeration.NETCONF.toString())) {
-                LOG.debug("{}: Ignoring schema due to unsupported location: {}", id, locationsAsString);
-                return Optional.empty();
-            }
-
-            childNode = NetconfMessageTransformUtil.IETF_NETCONF_MONITORING_SCHEMA_NAMESPACE;
-            final Optional<String> namespaceValue = getSingleChildNodeValue(schemaNode, childNode);
-            if (namespaceValue.isEmpty()) {
-                LOG.warn("{}: Ignoring schema due to missing namespace", id);
-                return Optional.empty();
-            }
-            final String namespaceAsString = namespaceValue.orElseThrow();
-
-            childNode = NetconfMessageTransformUtil.IETF_NETCONF_MONITORING_SCHEMA_VERSION;
-            // Revision does not have to be filled
-            final Optional<String> revisionAsString = getSingleChildNodeValue(schemaNode, childNode);
-
-            childNode = NetconfMessageTransformUtil.IETF_NETCONF_MONITORING_SCHEMA_IDENTIFIER;
-            final String moduleNameAsString = getSingleChildNodeValue(schemaNode, childNode).orElseThrow();
-
-            final QName moduleQName = revisionAsString.isPresent()
-                    ? QName.create(namespaceAsString, revisionAsString.orElseThrow(), moduleNameAsString)
-                    : QName.create(XMLNamespace.of(namespaceAsString), moduleNameAsString);
-
-            return Optional.of(new RemoteYangSchema(moduleQName));
-        }
-
-        /**
-         * Extracts all values of a leaf-list node as a set of strings.
-         */
-        private static Set<String> getAllChildNodeValues(final DataContainerNode schemaNode,
-                                                         final QName childNodeQName) {
-            final Set<String> extractedValues = new HashSet<>();
-            final DataContainerChild child = schemaNode.childByArg(toId(childNodeQName));
-            checkArgument(child != null, "Child nodes %s not present", childNodeQName);
-            checkArgument(child instanceof LeafSetNode, "Child nodes %s not present", childNodeQName);
-            for (final LeafSetEntryNode<?> childNode : ((LeafSetNode<?>) child).body()) {
-                extractedValues.add(getValueOfSimpleNode(childNode).orElseThrow());
-            }
-            return extractedValues;
-        }
-
-        private static Optional<String> getSingleChildNodeValue(final DataContainerNode schemaNode,
-                                                                final QName childNode) {
-            final Optional<DataContainerChild> node = schemaNode.findChildByArg(toId(childNode));
-            if (node.isPresent()) {
-                return getValueOfSimpleNode(node.orElseThrow());
-            }
-            LOG.debug("Child node {} not present", childNode);
-            return Optional.empty();
-        }
-
-        private static Optional<String> getValueOfSimpleNode(final NormalizedNode node) {
-            final String valueStr = node.body().toString();
-            return Strings.isNullOrEmpty(valueStr) ? Optional.empty() : Optional.of(valueStr.trim());
-        }
-
-        @Override
-        public boolean equals(final Object obj) {
-            if (this == obj) {
-                return true;
-            }
-            if (obj == null || getClass() != obj.getClass()) {
-                return false;
-            }
-
-            final RemoteYangSchema that = (RemoteYangSchema) obj;
-
-            return qname.equals(that.qname);
-        }
-
-        @Override
-        public int hashCode() {
-            return qname.hashCode();
-        }
-    }
 }
index cc953c703cd91a8cb01f5aecb36c8f8ecf80d76b..cd6d77283b5f45d43421df227373786646fb7b98 100644 (file)
@@ -7,6 +7,8 @@
  */
 package org.opendaylight.netconf.client.mdsal;
 
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
 import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemas;
 import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemasResolver;
 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
@@ -27,18 +29,19 @@ public final class NetconfStateSchemasResolverImpl implements NetconfDeviceSchem
         .bindTo(QNameModule.create(RFC8525_YANG_LIBRARY_CAPABILITY.getNamespace(), Revision.of("2016-06-21"))).intern();
 
     @Override
-    public NetconfDeviceSchemas resolve(final NetconfDeviceRpc deviceRpc,
-            final NetconfSessionPreferences remoteSessionCapabilities,
-            final RemoteDeviceId id, final EffectiveModelContext schemaContext) {
+    public ListenableFuture<? extends NetconfDeviceSchemas> resolve(final NetconfDeviceRpc deviceRpc,
+            final NetconfSessionPreferences remoteSessionCapabilities, final RemoteDeviceId id,
+            final EffectiveModelContext schemaContext) {
         // FIXME: I think we should prefer YANG library here
         if (remoteSessionCapabilities.isMonitoringSupported()) {
-            return NetconfStateSchemas.create(deviceRpc.domRpcService(), remoteSessionCapabilities, id, schemaContext);
+            return NetconfStateSchemas.forDevice(deviceRpc.domRpcService(), remoteSessionCapabilities, id,
+                schemaContext);
         }
         if (remoteSessionCapabilities.containsModuleCapability(RFC8525_YANG_LIBRARY_CAPABILITY)
                 || remoteSessionCapabilities.containsModuleCapability(RFC7895_YANG_LIBRARY_CAPABILITY)) {
-            return LibraryModulesSchemas.create(deviceRpc, id);
+            return LibraryModulesSchemas.forDevice(deviceRpc, id);
         }
 
-        return NetconfStateSchemas.EMPTY;
+        return Futures.immediateFuture(NetconfStateSchemas.EMPTY);
     }
 }
index 1bc9ffc605cb56233fb52f2219b65f2a7c292b11..4142a666e928d011bc2e7ae7f6f083885e7001b1 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.netconf.client.mdsal.api;
 
+import com.google.common.util.concurrent.ListenableFuture;
 import org.opendaylight.netconf.client.mdsal.spi.NetconfDeviceRpc;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 
@@ -15,6 +16,6 @@ import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
  */
 public interface NetconfDeviceSchemasResolver {
     // FIXME: document this method
-    NetconfDeviceSchemas resolve(NetconfDeviceRpc deviceRpc, NetconfSessionPreferences remoteSessionCapabilities,
-        RemoteDeviceId id, EffectiveModelContext schemaContext);
+    ListenableFuture<? extends NetconfDeviceSchemas> resolve(NetconfDeviceRpc deviceRpc,
+        NetconfSessionPreferences remoteSessionCapabilities, RemoteDeviceId id, EffectiveModelContext schemaContext);
 }
index 68c2982342640e24ce71fa5c547fed7b3e94bfe2..fe0e269c72cb6b51d1b72e7c83fd602785b6e6d4 100644 (file)
@@ -96,16 +96,6 @@ public final class NetconfMessageTransformUtil {
 
     public static final @NonNull QName IETF_NETCONF_MONITORING =
             QName.create(NetconfState.QNAME, "ietf-netconf-monitoring").intern();
-    public static final @NonNull QName IETF_NETCONF_MONITORING_SCHEMA_FORMAT =
-            QName.create(IETF_NETCONF_MONITORING, "format").intern();
-    public static final @NonNull QName IETF_NETCONF_MONITORING_SCHEMA_LOCATION =
-            QName.create(IETF_NETCONF_MONITORING, "location").intern();
-    public static final @NonNull QName IETF_NETCONF_MONITORING_SCHEMA_IDENTIFIER =
-            QName.create(IETF_NETCONF_MONITORING, "identifier").intern();
-    public static final @NonNull QName IETF_NETCONF_MONITORING_SCHEMA_VERSION =
-            QName.create(IETF_NETCONF_MONITORING, "version").intern();
-    public static final @NonNull QName IETF_NETCONF_MONITORING_SCHEMA_NAMESPACE =
-            QName.create(IETF_NETCONF_MONITORING, "namespace").intern();
 
     public static final @NonNull NodeIdentifier NETCONF_DATA_NODEID = NodeIdentifier.create(Data.QNAME);
 
index a95eed54b784b1b8ef01ee7030ee60f5805df57e..ea53d20655f6a3a3f7114779cbf07467d1d9b67b 100644 (file)
@@ -25,7 +25,6 @@ import static org.mockito.Mockito.timeout;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 
-import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.MoreExecutors;
 import com.google.common.util.concurrent.SettableFuture;
@@ -57,7 +56,6 @@ import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.co
 import org.opendaylight.yang.gen.v1.urn.opendaylight.netconf.device.rev240120.connection.oper.available.capabilities.AvailableCapability.CapabilityOrigin;
 import org.opendaylight.yangtools.concepts.Registration;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.model.api.Module;
 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
 import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.api.source.YangTextSource;
@@ -81,7 +79,7 @@ public class NetconfDeviceTest extends AbstractTestModelTest {
             TEST_NAMESPACE + "?module=" + TEST_MODULE + "2" + "&amp;revision=" + TEST_REVISION;
 
     private static final NetconfDeviceSchemasResolver STATE_SCHEMAS_RESOLVER =
-        (deviceRpc, remoteSessionCapabilities, id, schemaContext) -> NetconfStateSchemas.EMPTY;
+        (deviceRpc, remoteSessionCapabilities, id, schemaContext) -> Futures.immediateFuture(NetconfStateSchemas.EMPTY);
 
     private static NetconfMessage NOTIFICATION;
 
@@ -112,15 +110,13 @@ public class NetconfDeviceTest extends AbstractTestModelTest {
             }
         }).when(schemaFactory).createEffectiveModelContext(anyCollection());
 
-        final NetconfDeviceSchemasResolver stateSchemasResolver = (deviceRpc, remoteSessionCapabilities, id,
-                schemaContext) -> {
-            final Module first = SCHEMA_CONTEXT.getModules().iterator().next();
-            final QName qName = QName.create(first.getQNameModule(), first.getName());
-            final NetconfStateSchemas.RemoteYangSchema source1 = new NetconfStateSchemas.RemoteYangSchema(qName);
-            final NetconfStateSchemas.RemoteYangSchema source2 =
-                    new NetconfStateSchemas.RemoteYangSchema(QName.create(first.getQNameModule(), "test-module2"));
-            return new NetconfStateSchemas(Sets.newHashSet(source1, source2));
-        };
+        final NetconfDeviceSchemasResolver stateSchemasResolver =
+            (deviceRpc, remoteSessionCapabilities, id, schemaContext) -> {
+                final var first = SCHEMA_CONTEXT.getModules().iterator().next();
+                final var qName = QName.create(first.getQNameModule(), first.getName());
+                return Futures.immediateFuture(new NetconfStateSchemas(
+                    Set.of(qName, QName.create(first.getQNameModule(), "test-module2"))));
+            };
 
         doReturn(mock(Registration.class)).when(schemaRegistry).registerSchemaSource(any(), any());
         final NetconfDevice.SchemaResourcesDTO schemaResourcesDTO = new NetconfDevice
@@ -201,15 +197,13 @@ public class NetconfDeviceTest extends AbstractTestModelTest {
             }
         }).when(schemaFactory).createEffectiveModelContext(anyCollection());
 
-        final NetconfDeviceSchemasResolver stateSchemasResolver = (deviceRpc, remoteSessionCapabilities, id,
-            schemaContext) -> {
-            final Module first = SCHEMA_CONTEXT.getModules().iterator().next();
-            final QName qName = QName.create(first.getQNameModule(), first.getName());
-            final NetconfStateSchemas.RemoteYangSchema source1 = new NetconfStateSchemas.RemoteYangSchema(qName);
-            final NetconfStateSchemas.RemoteYangSchema source2 =
-                    new NetconfStateSchemas.RemoteYangSchema(QName.create(first.getQNameModule(), "test-module2"));
-            return new NetconfStateSchemas(Sets.newHashSet(source1, source2));
-        };
+        final NetconfDeviceSchemasResolver stateSchemasResolver =
+            (deviceRpc, remoteSessionCapabilities, id, schemaContext) -> {
+                final var first = SCHEMA_CONTEXT.getModules().iterator().next();
+                final var qName = QName.create(first.getQNameModule(), first.getName());
+                return Futures.immediateFuture(new NetconfStateSchemas(
+                    Set.of(qName, QName.create(first.getQNameModule(), "test-module2"))));
+            };
 
         doReturn(mock(Registration.class)).when(schemaRegistry).registerSchemaSource(any(), any());
         final NetconfDevice.SchemaResourcesDTO schemaResourcesDTO = new NetconfDevice
index 0d091216207724f99ae4c36d994c14b2b8965bdf..5dfebde7919a77bcecf780716926aee7c0b88155 100644 (file)
@@ -8,32 +8,28 @@
 package org.opendaylight.netconf.client.mdsal;
 
 import static org.hamcrest.CoreMatchers.hasItem;
-import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.SettableFuture;
 import java.net.InetSocketAddress;
+import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
-import org.junit.Before;
+import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnitRunner;
 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.spi.DefaultDOMRpcResult;
 import org.opendaylight.netconf.client.mdsal.api.NetconfSessionPreferences;
@@ -45,10 +41,11 @@ import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.mon
 import org.opendaylight.yangtools.util.xml.UntrustedXML;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
+import org.opendaylight.yangtools.yang.common.OperationFailedException;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.RpcError;
 import org.opendaylight.yangtools.yang.common.RpcResultBuilder;
 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
@@ -56,71 +53,61 @@ import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolde
 import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 @RunWith(MockitoJUnitRunner.StrictStubs.class)
 public class NetconfStateSchemasTest extends AbstractBaseSchemasTest {
-    private static final Logger LOG = LoggerFactory.getLogger(NetconfStateSchemasTest.class);
     private static final NetconfSessionPreferences CAPS = NetconfSessionPreferences.fromStrings(Set.of(
         "urn:ietf:params:xml:ns:yang:ietf-netconf-monitoring?module=ietf-netconf-monitoring&amp;revision=2010-10-04"));
+    private static final RemoteDeviceId DEVICE_ID = new RemoteDeviceId("device", new InetSocketAddress(99));
 
-    private final RemoteDeviceId deviceId = new RemoteDeviceId("device", new InetSocketAddress(99));
-    private final int numberOfSchemas = 73;
-    private final int numberOfLegalSchemas = numberOfSchemas - 3;
-
-    private ContainerNode compositeNodeSchemas;
+    private static EffectiveModelContext MODEL_CONTEXT = BASE_SCHEMAS.baseSchemaForCapabilities(CAPS).modelContext();
+    private static ContainerNode SCHEMAS_PAYLOAD;
 
     @Mock
     private DOMRpcService rpc;
 
-    private EffectiveModelContext modelContext;
-
-    @Before
-    public void setUp() throws Exception {
-        modelContext = BASE_SCHEMAS.baseSchemaForCapabilities(CAPS).modelContext();
-
+    @BeforeClass
+    public static void setUp() throws Exception {
         final var resultHolder = new NormalizationResultHolder();
         final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
         final var xmlParser = XmlParserStream.create(writer,
-            SchemaInferenceStack.ofDataTreePath(modelContext, NetconfState.QNAME, Schemas.QNAME).toInference(), false);
+            SchemaInferenceStack.ofDataTreePath(MODEL_CONTEXT, NetconfState.QNAME, Schemas.QNAME).toInference(), false);
 
-        xmlParser.parse(UntrustedXML.createXMLStreamReader(getClass().getResourceAsStream(
+        xmlParser.parse(UntrustedXML.createXMLStreamReader(NetconfStateSchemasTest.class.getResourceAsStream(
                 "/netconf-state.schemas.payload.xml")));
-        compositeNodeSchemas = (ContainerNode) resultHolder.getResult().data();
+        SCHEMAS_PAYLOAD = (ContainerNode) resultHolder.getResult().data();
     }
 
     @Test
     public void testCreate() throws Exception {
-        final var schemas = NetconfStateSchemas.create(deviceId, compositeNodeSchemas);
+        final var future = SettableFuture.<NetconfStateSchemas>create();
+        NetconfStateSchemas.create(future, DEVICE_ID, SCHEMAS_PAYLOAD);
+        final var schemas = Futures.getDone(future);
 
         final var availableYangSchemasQNames = schemas.getAvailableYangSchemasQNames();
-        assertEquals(numberOfLegalSchemas, availableYangSchemasQNames.size());
+        assertEquals(69, availableYangSchemasQNames.size());
 
         assertThat(availableYangSchemasQNames,
                 hasItem(QName.create("urn:TBD:params:xml:ns:yang:network-topology", "2013-07-12", "network-topology")));
     }
 
-    @Ignore
     @Test
+    @Ignore("We cannot handle a container as data -- only anyxml")
     public void testCreate2() throws Exception {
-        final ContainerNode netconfState = ImmutableNodes.newContainerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NetconfState.QNAME))
-                .withChild(compositeNodeSchemas)
-                .build();
-        final ContainerNode data = ImmutableNodes.newContainerBuilder()
-                .withNodeIdentifier(NetconfMessageTransformUtil.NETCONF_DATA_NODEID)
-                .withChild(netconfState)
-                .build();
         final ContainerNode rpcReply = ImmutableNodes.newContainerBuilder()
-                .withNodeIdentifier(new YangInstanceIdentifier
-                        .NodeIdentifier(NetconfMessageTransformUtil.NETCONF_RPC_REPLY_QNAME))
-                .withChild(data)
+                .withNodeIdentifier(new NodeIdentifier(NetconfMessageTransformUtil.NETCONF_RPC_REPLY_QNAME))
+                .withChild(ImmutableNodes.newContainerBuilder()
+                    .withNodeIdentifier(NetconfMessageTransformUtil.NETCONF_DATA_NODEID)
+                    .withChild(ImmutableNodes.newContainerBuilder()
+                        .withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NetconfState.QNAME))
+                        .withChild(SCHEMAS_PAYLOAD)
+                        .build())
+                    .build())
                 .build();
         doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(rpcReply))).when(rpc).invokeRpc(eq(Get.QNAME), any());
-        final NetconfStateSchemas stateSchemas = NetconfStateSchemas.create(rpc, CAPS, deviceId, modelContext);
-        final Set<QName> availableYangSchemasQNames = stateSchemas.getAvailableYangSchemasQNames();
-        assertEquals(numberOfLegalSchemas, availableYangSchemasQNames.size());
+        final var stateSchemas = assertSchemas(CAPS);
+        final var availableYangSchemasQNames = stateSchemas.getAvailableYangSchemasQNames();
+        assertEquals(69, availableYangSchemasQNames.size());
 
         assertThat(availableYangSchemasQNames,
                 hasItem(QName.create("urn:TBD:params:xml:ns:yang:network-topology", "2013-07-12", "network-topology")));
@@ -128,57 +115,36 @@ public class NetconfStateSchemasTest extends AbstractBaseSchemasTest {
 
     @Test
     public void testCreateMonitoringNotSupported() throws Exception {
-        final var caps = NetconfSessionPreferences.fromStrings(Set.of());
-        final var stateSchemas = NetconfStateSchemas.create(rpc, caps, deviceId, modelContext);
+        final var stateSchemas = assertSchemas(NetconfSessionPreferences.fromStrings(Set.of()));
         assertEquals(Set.of(), stateSchemas.getAvailableYangSchemasQNames());
     }
 
     @Test
     public void testCreateFail() throws Exception {
-        when(rpc.invokeRpc(eq(Get.QNAME), any())).thenReturn(
-                Futures.immediateFailedFuture(new DOMRpcImplementationNotAvailableException("not available")));
-        final var stateSchemas = NetconfStateSchemas.create(rpc, CAPS, deviceId, modelContext);
-        assertEquals(Set.of(), stateSchemas.getAvailableYangSchemasQNames());
+        final var domEx = new DOMRpcImplementationNotAvailableException("not available");
+        doReturn(Futures.immediateFailedFuture(domEx)).when(rpc).invokeRpc(eq(Get.QNAME), any());
+        assertSame(domEx, assertSchemasFailure());
     }
 
     @Test
     public void testCreateRpcError() throws Exception {
-        final RpcError rpcError = RpcResultBuilder.newError(ErrorType.RPC, new ErrorTag("fail"), "fail");
-        doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(rpcError))).when(rpc)
-            .invokeRpc(eq(Get.QNAME), any());
-        final var stateSchemas = NetconfStateSchemas.create(rpc, CAPS, deviceId, modelContext);
-        assertEquals(Set.of(), stateSchemas.getAvailableYangSchemasQNames());
+        final var rpcError = RpcResultBuilder.newError(ErrorType.RPC, new ErrorTag("fail"), "fail");
+        doReturn(Futures.immediateFuture(new DefaultDOMRpcResult(rpcError))).when(rpc).invokeRpc(eq(Get.QNAME), any());
+
+        final var ex = assertInstanceOf(OperationFailedException.class, assertSchemasFailure());
+        assertEquals(List.of(rpcError), ex.getErrorList());
     }
 
-    @Test
-    public void testCreateInterrupted() {
-        //NetconfStateSchemas.create calls Thread.currentThread().interrupt(), so it must run in its own thread
-        final var testFuture = Executors.newSingleThreadExecutor().submit(() -> {
-            final ListenableFuture<DOMRpcResult> interruptedFuture = mock(ListenableFuture.class);
-            try {
-                when(interruptedFuture.get()).thenThrow(new InterruptedException("interrupted"));
-                doReturn(interruptedFuture).when(rpc).invokeRpc(eq(Get.QNAME), any());
-                NetconfStateSchemas.create(rpc, CAPS, deviceId, modelContext);
-            } catch (final InterruptedException | ExecutionException e) {
-                LOG.info("Operation failed.", e);
-            }
-        });
-
-        assertThat(assertThrows(ExecutionException.class, () -> testFuture.get(3, TimeUnit.SECONDS)).getCause(),
-            instanceOf(RuntimeException.class));
+    private NetconfStateSchemas assertSchemas(final NetconfSessionPreferences prefs) {
+        try {
+            return Futures.getDone(NetconfStateSchemas.forDevice(rpc, prefs, DEVICE_ID, MODEL_CONTEXT));
+        } catch (ExecutionException e) {
+            throw new AssertionError(e);
+        }
     }
 
-    @Test
-    public void testRemoteYangSchemaEquals() throws Exception {
-        final NetconfStateSchemas.RemoteYangSchema schema1 =
-                new NetconfStateSchemas.RemoteYangSchema(NetconfState.QNAME);
-        final NetconfStateSchemas.RemoteYangSchema schema2 =
-                new NetconfStateSchemas.RemoteYangSchema(NetconfState.QNAME);
-        final NetconfStateSchemas.RemoteYangSchema schema3 =
-                new NetconfStateSchemas.RemoteYangSchema(Schemas.QNAME);
-        assertEquals(schema1, schema2);
-        assertEquals(schema2, schema1);
-        assertNotEquals(schema1, schema3);
-        assertNotEquals(schema2, schema3);
+    private Throwable assertSchemasFailure() {
+        final var future = NetconfStateSchemas.forDevice(rpc, CAPS, DEVICE_ID, MODEL_CONTEXT);
+        return assertThrows(ExecutionException.class, () -> Futures.getDone(future)).getCause();
     }
 }