Merge "Bug 5708: Schemaless netconf mount point"
[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
12 import com.google.common.base.Optional;
13 import com.google.common.base.Preconditions;
14 import com.google.common.base.Strings;
15 import com.google.common.collect.ImmutableMap;
16 import com.google.gson.stream.JsonReader;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.io.InputStreamReader;
20 import java.net.HttpURLConnection;
21 import java.net.MalformedURLException;
22 import java.net.URL;
23 import java.net.URLConnection;
24 import java.util.AbstractMap;
25 import java.util.Collections;
26 import java.util.Map;
27 import org.opendaylight.controller.config.util.xml.XmlUtil;
28 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160409.ModulesState;
29 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160409.module.list.Module;
30 import org.opendaylight.yangtools.sal.binding.generator.impl.ModuleInfoBackedContext;
31 import org.opendaylight.yangtools.yang.common.QName;
32 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
33 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
34 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
35 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
36 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
37 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
38 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
39 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
40 import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
41 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
42 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
43 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
44 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
45 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
46 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
47 import org.opendaylight.yangtools.yang.model.repo.api.RevisionSourceIdentifier;
48 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51 import org.xml.sax.SAXException;
52
53 /**
54  * Holds URLs with YANG schema resources for all yang modules reported in
55  * ietf-netconf-yang-library/modules-state/modules node
56  */
57 public class LibraryModulesSchemas {
58
59     private static final Logger LOG = LoggerFactory.getLogger(LibraryModulesSchemas.class);
60
61     private static SchemaContext libraryContext;
62
63     private final Map<SourceIdentifier, URL> availableModels;
64
65     static {
66         final ModuleInfoBackedContext moduleInfoBackedContext = ModuleInfoBackedContext.create();
67         moduleInfoBackedContext.registerModuleInfo(
68                 org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.library.rev160409.
69                         $YangModuleInfoImpl.getInstance());
70         libraryContext = moduleInfoBackedContext.tryToCreateSchemaContext().get();
71     }
72
73     private LibraryModulesSchemas(final Map<SourceIdentifier, URL> availableModels) {
74         this.availableModels = availableModels;
75     }
76
77     public Map<SourceIdentifier, URL> getAvailableModels() {
78         return availableModels;
79     }
80
81
82     /**
83      * Resolves URLs with YANG schema resources from modules-state. Uses basic http authenticaiton
84      * @param url URL pointing to yang library
85      * @return Resolved URLs with YANG schema resources for all yang modules from yang library
86      */
87     public static LibraryModulesSchemas create(final String url, final String username, final String password) {
88         Preconditions.checkNotNull(url);
89         try {
90             final URL urlConnection = new URL(url);
91             final URLConnection connection = urlConnection.openConnection();
92
93             if(connection instanceof HttpURLConnection) {
94                 connection.setRequestProperty("Accept", "application/xml");
95                 String userpass = username + ":" + password;
96                 String basicAuth = "Basic " + printBase64Binary(userpass.getBytes());
97
98                 connection.setRequestProperty("Authorization", basicAuth);
99             }
100
101             return createFromURLConnection(connection);
102
103         } catch (IOException e) {
104             LOG.warn("Unable to download yang library from {}", url, e);
105             return new LibraryModulesSchemas(Collections.<SourceIdentifier, URL>emptyMap());
106         }
107     }
108
109
110     private static LibraryModulesSchemas createFromURLConnection(URLConnection connection) {
111
112         String contentType = connection.getContentType();
113
114         // TODO try to guess Json also from intput stream
115         if (guessJsonFromFileName(connection.getURL().getFile())) {
116             contentType = "application/json";
117         }
118
119         Preconditions.checkNotNull(contentType, "Content type unknown");
120         Preconditions.checkState(contentType.equals("application/json") || contentType.equals("application/xml"),
121                 "Only XML and JSON types are supported.");
122         try (final InputStream in = connection.getInputStream()) {
123             final Optional<NormalizedNode<?, ?>> optionalModulesStateNode =
124                     contentType.equals("application/json") ? readJson(in) : readXml(in);
125
126             if (!optionalModulesStateNode.isPresent()) {
127                 return new LibraryModulesSchemas(Collections.<SourceIdentifier, URL>emptyMap());
128             }
129
130             final NormalizedNode<?, ?> modulesStateNode = optionalModulesStateNode.get();
131             Preconditions.checkState(modulesStateNode.getNodeType().equals(ModulesState.QNAME),
132                     "Wrong QName %s", modulesStateNode.getNodeType());
133             Preconditions.checkState(modulesStateNode instanceof ContainerNode,
134                     "Expecting container containing module list, but was %s", modulesStateNode);
135
136             final YangInstanceIdentifier.NodeIdentifier moduleListNodeId =
137                     new YangInstanceIdentifier.NodeIdentifier(Module.QNAME);
138             final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> moduleListNode =
139                     ((ContainerNode) modulesStateNode).getChild(moduleListNodeId);
140             Preconditions.checkState(moduleListNode.isPresent(),
141                     "Unable to find list: %s in %s", moduleListNodeId, modulesStateNode);
142             Preconditions.checkState(moduleListNode.get() instanceof MapNode,
143                     "Unexpected structure for container: %s in : %s. Expecting a list",
144                     moduleListNodeId, modulesStateNode);
145
146             final ImmutableMap.Builder<SourceIdentifier, URL> schemasMapping = new ImmutableMap.Builder<>();
147             for (final MapEntryNode moduleNode : ((MapNode) moduleListNode.get()).getValue()) {
148                 final Optional<Map.Entry<SourceIdentifier, URL>> schemaMappingEntry = createFromEntry(moduleNode);
149                 if (schemaMappingEntry.isPresent()) {
150                     schemasMapping.put(createFromEntry(moduleNode).get());
151                 }
152             }
153
154             return new LibraryModulesSchemas(schemasMapping.build());
155         } catch (IOException e) {
156             LOG.warn("Unable to download yang library from {}", connection.getURL(), e);
157             return new LibraryModulesSchemas(Collections.<SourceIdentifier, URL>emptyMap());
158         }
159     }
160
161     /**
162      * Resolves URLs with YANG schema resources from modules-state
163      * @param url URL pointing to yang library
164      * @return Resolved URLs with YANG schema resources for all yang modules from yang library
165      */
166     public static LibraryModulesSchemas create(final String url) {
167         Preconditions.checkNotNull(url);
168         try {
169             final URL urlConnection = new URL(url);
170             final URLConnection connection = urlConnection.openConnection();
171
172             if(connection instanceof HttpURLConnection) {
173                 connection.setRequestProperty("Accept", "application/xml");
174             }
175
176             return createFromURLConnection(connection);
177
178         } catch (IOException e) {
179             LOG.warn("Unable to download yang library from {}", url, e);
180             return new LibraryModulesSchemas(Collections.<SourceIdentifier, URL>emptyMap());
181         }
182     }
183
184     private static boolean guessJsonFromFileName(final String fileName) {
185         String extension = "";
186         final int i = fileName.lastIndexOf(46);
187         if(i != -1) {
188             extension = fileName.substring(i).toLowerCase();
189         }
190
191         return extension.equals(".json");
192     }
193
194     private static Optional<NormalizedNode<?, ?>> readJson(final InputStream in) {
195         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
196         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
197
198         final JsonParserStream jsonParser = JsonParserStream.create(writer, libraryContext);
199         final JsonReader reader = new JsonReader(new InputStreamReader(in));
200
201         jsonParser.parse(reader);
202
203         return resultHolder.isFinished() ?
204                 Optional.of(resultHolder.getResult()) :
205                 Optional.<NormalizedNode<?, ?>>absent();
206     }
207
208     private static Optional<NormalizedNode<?, ?>> readXml(final InputStream in) {
209         final DomToNormalizedNodeParserFactory parserFactory =
210                 DomToNormalizedNodeParserFactory.getInstance(XmlUtils.DEFAULT_XML_CODEC_PROVIDER, libraryContext);
211
212         try {
213             final NormalizedNode<?, ?> parsed =
214                     parserFactory.getContainerNodeParser().parse(Collections.singleton(XmlUtil.readXmlToElement(in)),
215                             (ContainerSchemaNode) libraryContext.getDataChildByName(ModulesState.QNAME));
216             return Optional.of(parsed);
217         } catch (IOException|SAXException e) {
218             LOG.warn("Unable to parse yang library xml content", e);
219         }
220
221         return Optional.<NormalizedNode<?, ?>>absent();
222     }
223
224     private static Optional<Map.Entry<SourceIdentifier, URL>> createFromEntry(final MapEntryNode moduleNode) {
225         Preconditions.checkArgument(
226                 moduleNode.getNodeType().equals(Module.QNAME), "Wrong QName %s", moduleNode.getNodeType());
227
228         YangInstanceIdentifier.NodeIdentifier childNodeId =
229                 new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "name"));
230         final String moduleName = getSingleChildNodeValue(moduleNode, childNodeId).get();
231
232         childNodeId = new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "revision"));
233         final Optional<String> revision = getSingleChildNodeValue(moduleNode, childNodeId);
234         if(revision.isPresent()) {
235             if(!SourceIdentifier.REVISION_PATTERN.matcher(revision.get()).matches()) {
236                 LOG.warn("Skipping library schema for {}. Revision {} is in wrong format.", moduleNode, revision.get());
237                 return Optional.<Map.Entry<SourceIdentifier, URL>>absent();
238             }
239         }
240
241         // FIXME leaf schema with url that represents the yang schema resource for this module is not mandatory
242         // don't fail if schema node is not present, just skip the entry or add some default URL
243         childNodeId = new YangInstanceIdentifier.NodeIdentifier(QName.create(Module.QNAME, "schema"));
244         final Optional<String> schemaUriAsString = getSingleChildNodeValue(moduleNode, childNodeId);
245
246
247         final SourceIdentifier sourceId = revision.isPresent()
248                 ? RevisionSourceIdentifier.create(moduleName, revision.get())
249                 : RevisionSourceIdentifier.create(moduleName);
250
251         try {
252             return Optional.<Map.Entry<SourceIdentifier, URL>>of(new AbstractMap.SimpleImmutableEntry<>(
253                     sourceId, new URL(schemaUriAsString.get())));
254         } catch (MalformedURLException e) {
255             LOG.warn("Skipping library schema for {}. URL {} representing yang schema resource is not valid",
256                     moduleNode, schemaUriAsString.get());
257             return Optional.<Map.Entry<SourceIdentifier, URL>>absent();
258         }
259     }
260
261     private static Optional<String> getSingleChildNodeValue(final DataContainerNode<?> schemaNode,
262                                                             final YangInstanceIdentifier.NodeIdentifier childNodeId) {
263         final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> node =
264                 schemaNode.getChild(childNodeId);
265         Preconditions.checkArgument(node.isPresent(), "Child node %s not present", childNodeId.getNodeType());
266         return getValueOfSimpleNode(node.get());
267     }
268
269     private static Optional<String> getValueOfSimpleNode(
270             final NormalizedNode<? extends YangInstanceIdentifier.PathArgument, ?> node) {
271         final Object value = node.getValue();
272         return value == null || Strings.isNullOrEmpty(value.toString())
273                 ? Optional.<String>absent() : Optional.of(value.toString().trim());
274     }
275
276 }