Optimize sal-netconf-connector translation
[netconf.git] / netconf / sal-netconf-connector / src / main / java / org / opendaylight / netconf / sal / connect / netconf / LibraryModulesSchemas.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.netconf.sal.connect.netconf;
9
10 import static javax.xml.bind.DatatypeConverter.printBase64Binary;
11 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_DATA_NODEID;
12 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_NODEID;
13 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_GET_QNAME;
14 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toId;
15 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toPath;
16
17 import com.google.common.base.Preconditions;
18 import com.google.common.base.Strings;
19 import com.google.common.collect.ImmutableMap;
20 import com.google.common.collect.Maps;
21 import com.google.gson.stream.JsonReader;
22 import java.io.IOException;
23 import java.io.InputStream;
24 import java.io.InputStreamReader;
25 import java.net.HttpURLConnection;
26 import java.net.MalformedURLException;
27 import java.net.URI;
28 import java.net.URISyntaxException;
29 import java.net.URL;
30 import java.net.URLConnection;
31 import java.nio.charset.Charset;
32 import java.nio.charset.StandardCharsets;
33 import java.util.AbstractMap;
34 import java.util.Collections;
35 import java.util.Locale;
36 import java.util.Map;
37 import java.util.Optional;
38 import java.util.Set;
39 import java.util.concurrent.ExecutionException;
40 import java.util.regex.Pattern;
41 import javax.xml.parsers.DocumentBuilder;
42 import javax.xml.parsers.ParserConfigurationException;
43 import javax.xml.stream.XMLStreamException;
44 import javax.xml.transform.dom.DOMSource;
45 import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult;
46 import org.opendaylight.mdsal.binding.generator.impl.ModuleInfoBackedContext;
47 import org.opendaylight.netconf.sal.connect.api.NetconfDeviceSchemas;
48 import org.opendaylight.netconf.sal.connect.netconf.sal.NetconfDeviceRpc;
49 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil;
50 import org.opendaylight.netconf.sal.connect.util.RemoteDeviceId;
51 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160621.ModulesState;
52 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160621.module.list.Module;
53 import org.opendaylight.yangtools.util.xml.UntrustedXML;
54 import org.opendaylight.yangtools.yang.common.QName;
55 import org.opendaylight.yangtools.yang.common.Revision;
56 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
57 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
58 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
59 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
60 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
61 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
62 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
63 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
64 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
65 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
66 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
67 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
68 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
69 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
70 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
71 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
74 import org.w3c.dom.Document;
75 import org.w3c.dom.Element;
76 import org.w3c.dom.Node;
77 import org.xml.sax.SAXException;
78
79 /**
80  * Holds URLs with YANG schema resources for all yang modules reported in
81  * ietf-netconf-yang-library/modules-state/modules node.
82  */
83 public final class LibraryModulesSchemas implements NetconfDeviceSchemas {
84
85     private static final Logger LOG = LoggerFactory.getLogger(LibraryModulesSchemas.class);
86     private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
87     private static final SchemaContext LIBRARY_CONTEXT;
88
89     static {
90         final ModuleInfoBackedContext moduleInfoBackedContext = ModuleInfoBackedContext.create();
91         moduleInfoBackedContext.registerModuleInfo(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang
92                 .library.rev160621.$YangModuleInfoImpl.getInstance());
93         LIBRARY_CONTEXT = moduleInfoBackedContext.tryToCreateSchemaContext().get();
94     }
95
96     private final Map<QName, URL> availableModels;
97
98     private static final YangInstanceIdentifier MODULES_STATE_MODULE_LIST =
99             YangInstanceIdentifier.builder().node(ModulesState.QNAME).node(Module.QNAME).build();
100
101     private static final ContainerNode GET_MODULES_STATE_MODULE_LIST_RPC;
102
103     static {
104         final DataContainerChild<?, ?> filter =
105                 NetconfMessageTransformUtil.toFilterStructure(MODULES_STATE_MODULE_LIST, LIBRARY_CONTEXT);
106         GET_MODULES_STATE_MODULE_LIST_RPC =
107                 Builders.containerBuilder().withNodeIdentifier(NETCONF_GET_NODEID).withChild(filter).build();
108     }
109
110     private LibraryModulesSchemas(final Map<QName, URL> availableModels) {
111         this.availableModels = availableModels;
112     }
113
114     public Map<SourceIdentifier, URL> getAvailableModels() {
115         final Map<SourceIdentifier, URL> result = Maps.newHashMap();
116         for (final Map.Entry<QName, URL> entry : availableModels.entrySet()) {
117             final SourceIdentifier sId = RevisionSourceIdentifier
118                 .create(entry.getKey().getLocalName(), entry.getKey().getRevision());
119             result.put(sId, entry.getValue());
120         }
121
122         return result;
123     }
124
125
126     /**
127      * Resolves URLs with YANG schema resources from modules-state. Uses basic http authenticaiton
128      *
129      * @param url URL pointing to yang library
130      * @return Resolved URLs with YANG schema resources for all yang modules from yang library
131      */
132     public static LibraryModulesSchemas create(final String url, final String username, final String password) {
133         Preconditions.checkNotNull(url);
134         try {
135             final URL urlConnection = new URL(url);
136             final URLConnection connection = urlConnection.openConnection();
137
138             if (connection instanceof HttpURLConnection) {
139                 connection.setRequestProperty("Accept", "application/xml");
140                 final String userpass = username + ":" + password;
141                 final String basicAuth = "Basic " + printBase64Binary(userpass.getBytes(StandardCharsets.UTF_8));
142
143                 connection.setRequestProperty("Authorization", basicAuth);
144             }
145
146             return createFromURLConnection(connection);
147
148         } catch (final IOException e) {
149             LOG.warn("Unable to download yang library from {}", url, e);
150             return new LibraryModulesSchemas(Collections.emptyMap());
151         }
152     }
153
154
155     public static LibraryModulesSchemas create(final NetconfDeviceRpc deviceRpc, final RemoteDeviceId deviceId) {
156         final DOMRpcResult moduleListNodeResult;
157         try {
158             moduleListNodeResult =
159                     deviceRpc.invokeRpc(toPath(NETCONF_GET_QNAME), GET_MODULES_STATE_MODULE_LIST_RPC).get();
160         } catch (final InterruptedException e) {
161             Thread.currentThread().interrupt();
162             throw new RuntimeException(deviceId + ": Interrupted while waiting for response to "
163                     + MODULES_STATE_MODULE_LIST, e);
164         } catch (final ExecutionException e) {
165             LOG.warn("{}: Unable to detect available schemas, get to {} failed", deviceId,
166                     MODULES_STATE_MODULE_LIST, e);
167             return new LibraryModulesSchemas(Collections.emptyMap());
168         }
169
170         if (moduleListNodeResult.getErrors().isEmpty() == false) {
171             LOG.warn("{}: Unable to detect available schemas, get to {} failed, {}",
172                     deviceId, MODULES_STATE_MODULE_LIST, moduleListNodeResult.getErrors());
173             return new LibraryModulesSchemas(Collections.emptyMap());
174         }
175
176
177         final Optional<? extends NormalizedNode<?, ?>> modulesStateNode =
178                 findModulesStateNode(moduleListNodeResult.getResult());
179         if (modulesStateNode.isPresent()) {
180             Preconditions.checkState(modulesStateNode.get() instanceof ContainerNode,
181                     "Expecting container containing schemas, but was %s", modulesStateNode.get());
182             return create((ContainerNode) modulesStateNode.get());
183         }
184
185         LOG.warn("{}: Unable to detect available schemas, get to {} was empty", deviceId, toId(ModulesState.QNAME));
186         return new LibraryModulesSchemas(Collections.emptyMap());
187     }
188
189     private static LibraryModulesSchemas create(final ContainerNode modulesStateNode) {
190         final YangInstanceIdentifier.NodeIdentifier moduleListNodeId =
191                 new YangInstanceIdentifier.NodeIdentifier(Module.QNAME);
192         final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> moduleListNode =
193                 modulesStateNode.getChild(moduleListNodeId);
194         Preconditions.checkState(moduleListNode.isPresent(),
195                 "Unable to find list: %s in %s", moduleListNodeId, modulesStateNode);
196         Preconditions.checkState(moduleListNode.get() instanceof MapNode,
197                 "Unexpected structure for container: %s in : %s. Expecting a list",
198                 moduleListNodeId, modulesStateNode);
199
200         final ImmutableMap.Builder<QName, URL> schemasMapping = new ImmutableMap.Builder<>();
201         for (final MapEntryNode moduleNode : ((MapNode) moduleListNode.get()).getValue()) {
202             final Optional<Map.Entry<QName, URL>> schemaMappingEntry = createFromEntry(moduleNode);
203             if (schemaMappingEntry.isPresent()) {
204                 schemasMapping.put(createFromEntry(moduleNode).get());
205             }
206         }
207
208         return new LibraryModulesSchemas(schemasMapping.build());
209     }
210
211     /**
212      * Resolves URLs with YANG schema resources from modules-state.
213      * @param url URL pointing to yang library
214      * @return Resolved URLs with YANG schema resources for all yang modules from yang library
215      */
216     public static LibraryModulesSchemas create(final String url) {
217         Preconditions.checkNotNull(url);
218         try {
219             final URL urlConnection = new URL(url);
220             final URLConnection connection = urlConnection.openConnection();
221
222             if (connection instanceof HttpURLConnection) {
223                 connection.setRequestProperty("Accept", "application/xml");
224             }
225
226             return createFromURLConnection(connection);
227
228         } catch (final IOException e) {
229             LOG.warn("Unable to download yang library from {}", url, e);
230             return new LibraryModulesSchemas(Collections.emptyMap());
231         }
232     }
233
234     private static Optional<? extends NormalizedNode<?, ?>> findModulesStateNode(final NormalizedNode<?, ?> result) {
235         if (result == null) {
236             return Optional.empty();
237         }
238         final Optional<DataContainerChild<?, ?>> dataNode =
239                 ((DataContainerNode<?>) result).getChild(NETCONF_DATA_NODEID);
240         if (dataNode.isPresent() == false) {
241             return Optional.empty();
242         }
243
244         return ((DataContainerNode<?>) dataNode.get()).getChild(toId(ModulesState.QNAME));
245     }
246
247     private static LibraryModulesSchemas createFromURLConnection(final URLConnection connection) {
248
249         String contentType = connection.getContentType();
250
251         // TODO try to guess Json also from intput stream
252         if (guessJsonFromFileName(connection.getURL().getFile())) {
253             contentType = "application/json";
254         }
255
256         Preconditions.checkNotNull(contentType, "Content type unknown");
257         Preconditions.checkState(contentType.equals("application/json") || contentType.equals("application/xml"),
258                 "Only XML and JSON types are supported.");
259         try (InputStream in = connection.getInputStream()) {
260             final Optional<NormalizedNode<?, ?>> optionalModulesStateNode =
261                     contentType.equals("application/json") ? readJson(in) : readXml(in);
262
263             if (!optionalModulesStateNode.isPresent()) {
264                 return new LibraryModulesSchemas(Collections.emptyMap());
265             }
266
267             final NormalizedNode<?, ?> modulesStateNode = optionalModulesStateNode.get();
268             Preconditions.checkState(modulesStateNode.getNodeType().equals(ModulesState.QNAME),
269                     "Wrong QName %s", modulesStateNode.getNodeType());
270             Preconditions.checkState(modulesStateNode instanceof ContainerNode,
271                     "Expecting container containing module list, but was %s", modulesStateNode);
272
273             final YangInstanceIdentifier.NodeIdentifier moduleListNodeId =
274                     new YangInstanceIdentifier.NodeIdentifier(Module.QNAME);
275             final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> moduleListNode =
276                     ((ContainerNode) modulesStateNode).getChild(moduleListNodeId);
277             Preconditions.checkState(moduleListNode.isPresent(),
278                     "Unable to find list: %s in %s", moduleListNodeId, modulesStateNode);
279             Preconditions.checkState(moduleListNode.get() instanceof MapNode,
280                     "Unexpected structure for container: %s in : %s. Expecting a list",
281                     moduleListNodeId, modulesStateNode);
282
283             final ImmutableMap.Builder<QName, URL> schemasMapping = new ImmutableMap.Builder<>();
284             for (final MapEntryNode moduleNode : ((MapNode) moduleListNode.get()).getValue()) {
285                 final Optional<Map.Entry<QName, URL>> schemaMappingEntry = createFromEntry(moduleNode);
286                 if (schemaMappingEntry.isPresent()) {
287                     schemasMapping.put(createFromEntry(moduleNode).get());
288                 }
289             }
290
291             return new LibraryModulesSchemas(schemasMapping.build());
292         } catch (final IOException e) {
293             LOG.warn("Unable to download yang library from {}", connection.getURL(), e);
294             return new LibraryModulesSchemas(Collections.emptyMap());
295         }
296     }
297
298     private static boolean guessJsonFromFileName(final String fileName) {
299         String extension = "";
300         final int i = fileName.lastIndexOf(46);
301         if (i != -1) {
302             extension = fileName.substring(i).toLowerCase(Locale.ROOT);
303         }
304
305         return extension.equals(".json");
306     }
307
308     private static Optional<NormalizedNode<?, ?>> readJson(final InputStream in) {
309         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
310         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
311
312         final JsonParserStream jsonParser = JsonParserStream.create(writer, LIBRARY_CONTEXT);
313         final JsonReader reader = new JsonReader(new InputStreamReader(in, Charset.defaultCharset()));
314
315         jsonParser.parse(reader);
316
317         return resultHolder.isFinished() ? Optional.of(resultHolder.getResult()) : Optional.empty();
318     }
319
320     private static Optional<NormalizedNode<?, ?>> readXml(final InputStream in) {
321         try {
322             final DocumentBuilder docBuilder = UntrustedXML.newDocumentBuilder();
323
324             final Document read = docBuilder.parse(in);
325             final Document doc = docBuilder.newDocument();
326             final Element rootElement = doc.createElementNS("urn:ietf:params:xml:ns:yang:ietf-yang-library",
327                     "modules");
328             doc.appendChild(rootElement);
329
330             for (int i = 0; i < read.getElementsByTagName("revision").getLength(); i++) {
331                 final String revision = read.getElementsByTagName("revision").item(i).getTextContent();
332                 if (DATE_PATTERN.matcher(revision).find() || revision.isEmpty()) {
333                     final Node module = doc.importNode(read.getElementsByTagName("module").item(i), true);
334                     rootElement.appendChild(module);
335                 } else {
336                     LOG.warn("Xml contains wrong revision - {} - on module {}", revision,
337                             read.getElementsByTagName("module").item(i).getTextContent());
338                 }
339             }
340
341             final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
342             final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
343             final XmlParserStream xmlParser = XmlParserStream.create(writer, LIBRARY_CONTEXT,
344                     LIBRARY_CONTEXT.getDataChildByName(ModulesState.QNAME));
345             xmlParser.traverse(new DOMSource(doc.getDocumentElement()));
346             final NormalizedNode<?, ?> parsed = resultHolder.getResult();
347             return Optional.of(parsed);
348         } catch (XMLStreamException | URISyntaxException | IOException | ParserConfigurationException
349                 | SAXException e) {
350             LOG.warn("Unable to parse yang library xml content", e);
351         }
352
353         return Optional.empty();
354     }
355
356     private static Optional<Map.Entry<QName, URL>> createFromEntry(final MapEntryNode moduleNode) {
357         Preconditions.checkArgument(
358                 moduleNode.getNodeType().equals(Module.QNAME), "Wrong QName %s", moduleNode.getNodeType());
359
360         YangInstanceIdentifier.NodeIdentifier childNodeId =
361                 new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "name"));
362         final String moduleName = getSingleChildNodeValue(moduleNode, childNodeId).get();
363
364         childNodeId = new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "revision"));
365         final Optional<String> revision = getSingleChildNodeValue(moduleNode, childNodeId);
366         if (revision.isPresent()) {
367             if (!Revision.STRING_FORMAT_PATTERN.matcher(revision.get()).matches()) {
368                 LOG.warn("Skipping library schema for {}. Revision {} is in wrong format.", moduleNode, revision.get());
369                 return Optional.empty();
370             }
371         }
372
373         // FIXME leaf schema with url that represents the yang schema resource for this module is not mandatory
374         // don't fail if schema node is not present, just skip the entry or add some default URL
375         childNodeId = new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "schema"));
376         final Optional<String> schemaUriAsString = getSingleChildNodeValue(moduleNode, childNodeId);
377
378         childNodeId = new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "namespace"));
379         final String moduleNameSpace = getSingleChildNodeValue(moduleNode, childNodeId).get();
380
381         final QName moduleQName = revision.isPresent()
382                 ? QName.create(moduleNameSpace, revision.get(), moduleName)
383                 : QName.create(URI.create(moduleNameSpace), moduleName);
384
385         try {
386             return Optional.of(new AbstractMap.SimpleImmutableEntry<>(
387                     moduleQName, new URL(schemaUriAsString.get())));
388         } catch (final MalformedURLException e) {
389             LOG.warn("Skipping library schema for {}. URL {} representing yang schema resource is not valid",
390                     moduleNode, schemaUriAsString.get());
391             return Optional.empty();
392         }
393     }
394
395     private static Optional<String> getSingleChildNodeValue(final DataContainerNode<?> schemaNode,
396                                                             final YangInstanceIdentifier.NodeIdentifier childNodeId) {
397         final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> node =
398                 schemaNode.getChild(childNodeId);
399         Preconditions.checkArgument(node.isPresent(), "Child node %s not present", childNodeId.getNodeType());
400         return getValueOfSimpleNode(node.get());
401     }
402
403     private static Optional<String> getValueOfSimpleNode(
404             final NormalizedNode<? extends YangInstanceIdentifier.PathArgument, ?> node) {
405         final String valueStr = node.getValue().toString();
406         return Strings.isNullOrEmpty(valueStr) ? Optional.empty() : Optional.of(valueStr.trim());
407     }
408
409     @Override
410     public Set<QName> getAvailableYangSchemasQNames() {
411         return null;
412     }
413 }