Schema determination is asynchronous
[netconf.git] / plugins / netconf-client-mdsal / src / main / java / org / opendaylight / netconf / client / mdsal / LibraryModulesSchemas.java
index afd506e2e27627f734cc304ec3c7b88076ca4c13..afbfb8325442bbff4192cb3deda3fb08dc7c3b52 100644 (file)
@@ -12,29 +12,31 @@ 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.netconf.client.mdsal.impl.NetconfMessageTransformUtil.NETCONF_GET_QNAME;
+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;
 import java.io.InputStreamReader;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
-import java.net.URISyntaxException;
 import java.net.URL;
 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;
@@ -47,10 +49,13 @@ import org.opendaylight.netconf.client.mdsal.api.NetconfDeviceSchemas;
 import org.opendaylight.netconf.client.mdsal.api.RemoteDeviceId;
 import org.opendaylight.netconf.client.mdsal.impl.NetconfMessageTransformUtil;
 import org.opendaylight.netconf.client.mdsal.spi.NetconfDeviceRpc;
+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.yang.ietf.yang.library.rev190104.ModulesState;
 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;
@@ -62,16 +67,17 @@ import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
 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.stream.NormalizationResult;
 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
 import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactorySupplier;
 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
-import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
-import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
+import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
+import org.opendaylight.yangtools.yang.data.spi.node.ImmutableNodes;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
-import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
+import org.opendaylight.yangtools.yang.model.api.source.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
 import org.slf4j.Logger;
@@ -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,24 +102,22 @@ 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);
 
     private static final YangInstanceIdentifier MODULES_STATE_MODULE_LIST =
-            YangInstanceIdentifier.create(MODULES_STATE_NID, MODULE_NID);
+            YangInstanceIdentifier.of(MODULES_STATE_NID, MODULE_NID);
 
-    private static final @NonNull ContainerNode GET_MODULES_STATE_MODULE_LIST_RPC = Builders.containerBuilder()
+    private static final @NonNull ContainerNode GET_MODULES_STATE_MODULE_LIST_RPC = ImmutableNodes.newContainerBuilder()
             .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.invokeRpc(NETCONF_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();
@@ -273,7 +280,7 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
         checkState(modulesStateNode instanceof ContainerNode, "Expecting container containing module list, but was %s",
             modulesStateNode);
         final ContainerNode modulesState = (ContainerNode) modulesStateNode;
-        final NodeIdentifier nodeName = modulesState.getIdentifier();
+        final NodeIdentifier nodeName = modulesState.name();
         checkState(MODULES_STATE_NID.equals(nodeName), "Wrong container identifier %s", nodeName);
 
         return create((ContainerNode) modulesStateNode);
@@ -285,7 +292,7 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
     }
 
     private static Optional<NormalizedNode> readJson(final InputStream in) {
-        final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+        final NormalizationResultHolder resultHolder = new NormalizationResultHolder();
         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
 
         final JsonParserStream jsonParser = JsonParserStream.create(writer, JSON_CODECS);
@@ -293,7 +300,8 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
 
         jsonParser.parse(reader);
 
-        return resultHolder.isFinished() ? Optional.of(resultHolder.getResult()) : Optional.empty();
+        final NormalizationResult<?> result = resultHolder.result();
+        return result == null ? Optional.empty() : Optional.of(result.data());
     }
 
     private static Optional<NormalizedNode> readXml(final InputStream in) {
@@ -320,12 +328,12 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
                 }
             }
 
-            final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
+            final NormalizationResultHolder resultHolder = new NormalizationResultHolder();
             final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
             final XmlParserStream xmlParser = XmlParserStream.create(writer, MODULES_STATE_INFERENCE);
             xmlParser.traverse(new DOMSource(doc.getDocumentElement()));
-            return Optional.of(resultHolder.getResult());
-        } catch (XMLStreamException | URISyntaxException | IOException | SAXException e) {
+            return Optional.of(resultHolder.getResult().data());
+        } catch (XMLStreamException | IOException | SAXException e) {
             LOG.warn("Unable to parse yang library xml content", e);
         }
 
@@ -333,7 +341,7 @@ public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
     }
 
     private static @Nullable Entry<QName, URL> createFromEntry(final MapEntryNode moduleNode) {
-        final QName moduleNodeId = moduleNode.getIdentifier().getNodeType();
+        final QName moduleNodeId = moduleNode.name().getNodeType();
         checkArgument(moduleNodeId.equals(Module.QNAME), "Wrong QName %s", moduleNodeId);
 
         final String moduleName = getSingleChildNodeValue(moduleNode, NAME_NID).orElseThrow();