Merge changes I4ec69712,I8348002a,I7e6abbf1,Idc2294da
[controller.git] / opendaylight / md-sal / sal-netconf-connector / src / main / java / org / opendaylight / controller / sal / connect / netconf / schema / mapping / NetconfMessageTransformer.java
1 /*
2  * Copyright (c) 2014 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.controller.sal.connect.netconf.schema.mapping;
9
10 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_CONFIG_QNAME;
11 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_FILTER_QNAME;
12 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_RPC_QNAME;
13 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_TYPE_QNAME;
14 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_URI;
15 import static org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil.toId;
16
17 import com.google.common.base.Function;
18 import com.google.common.base.Optional;
19 import com.google.common.base.Preconditions;
20 import com.google.common.base.Predicate;
21 import com.google.common.collect.Iterables;
22 import com.google.common.collect.Lists;
23 import com.google.common.collect.Maps;
24 import com.google.common.collect.Multimap;
25 import com.google.common.collect.Multimaps;
26 import java.io.IOException;
27 import java.util.Collection;
28 import java.util.Collections;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.NoSuchElementException;
32 import java.util.Set;
33 import javax.xml.stream.XMLOutputFactory;
34 import javax.xml.stream.XMLStreamException;
35 import javax.xml.stream.XMLStreamWriter;
36 import javax.xml.transform.dom.DOMResult;
37 import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult;
38 import org.opendaylight.controller.md.sal.dom.spi.DefaultDOMRpcResult;
39 import org.opendaylight.controller.netconf.api.NetconfMessage;
40 import org.opendaylight.controller.netconf.util.exception.MissingNameSpaceException;
41 import org.opendaylight.controller.netconf.util.xml.XmlElement;
42 import org.opendaylight.controller.netconf.util.xml.XmlUtil;
43 import org.opendaylight.controller.sal.connect.api.MessageTransformer;
44 import org.opendaylight.controller.sal.connect.netconf.util.NetconfMessageTransformUtil;
45 import org.opendaylight.controller.sal.connect.util.MessageCounter;
46 import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.edit.config.input.EditContent;
47 import org.opendaylight.yangtools.sal.binding.generator.impl.ModuleInfoBackedContext;
48 import org.opendaylight.yangtools.yang.common.QName;
49 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
50 import org.opendaylight.yangtools.yang.data.api.schema.ChoiceNode;
51 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
52 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
53 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
54 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
55 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeWriter;
56 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XMLStreamNormalizedNodeStreamWriter;
57 import org.opendaylight.yangtools.yang.data.impl.codec.xml.XmlUtils;
58 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
59 import org.opendaylight.yangtools.yang.data.impl.schema.transform.dom.parser.DomToNormalizedNodeParserFactory;
60 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
61 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
62 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
63 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
64 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
65 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
66 import org.slf4j.Logger;
67 import org.slf4j.LoggerFactory;
68 import org.w3c.dom.Document;
69 import org.w3c.dom.Element;
70 import org.w3c.dom.Node;
71
72 public class NetconfMessageTransformer implements MessageTransformer<NetconfMessage> {
73
74     public static final String MESSAGE_ID_PREFIX = "m";
75
76     private static final Logger LOG= LoggerFactory.getLogger(NetconfMessageTransformer.class);
77
78     private static final DomToNormalizedNodeParserFactory NORMALIZED_NODE_PARSER_FACTORY = DomToNormalizedNodeParserFactory.getInstance(XmlUtils.DEFAULT_XML_CODEC_PROVIDER);
79
80     private static final Function<SchemaNode, QName> QNAME_FUNCTION = new Function<SchemaNode, QName>() {
81         @Override
82         public QName apply(final SchemaNode rpcDefinition) {
83             return rpcDefinition.getQName();
84         }
85     };
86
87     private static final Function<SchemaNode, QName> QNAME_NOREV_FUNCTION = new Function<SchemaNode, QName>() {
88         @Override
89         public QName apply(final SchemaNode notification) {
90             return QNAME_FUNCTION.apply(notification).withoutRevision();
91         }
92     };
93     private static final SchemaContext BASE_NETCONF_CTX;
94
95     static {
96         try {
97             final ModuleInfoBackedContext moduleInfoBackedContext = ModuleInfoBackedContext.create();
98             // TODO this should be used only if the base is not present
99             moduleInfoBackedContext.addModuleInfos(
100                     Lists.newArrayList(org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.netconf.base._1._0.rev110601.$YangModuleInfoImpl.getInstance()));
101             BASE_NETCONF_CTX = moduleInfoBackedContext.tryToCreateSchemaContext().get();
102         } catch (final RuntimeException e) {
103             LOG.error("Unable to prepare schema context for base netconf ops", e);
104             throw new ExceptionInInitializerError(e);
105         }
106     }
107
108     private final SchemaContext schemaContext;
109     private final MessageCounter counter;
110     private final Map<QName, RpcDefinition> mappedRpcs;
111     private final Multimap<QName, NotificationDefinition> mappedNotifications;
112
113     public NetconfMessageTransformer(final SchemaContext schemaContext) {
114         this.counter = new MessageCounter();
115         this.schemaContext = schemaContext;
116
117         mappedRpcs = Maps.uniqueIndex(schemaContext.getOperations(), QNAME_FUNCTION);
118         mappedNotifications = Multimaps.index(schemaContext.getNotifications(), QNAME_NOREV_FUNCTION);
119     }
120
121     @Override
122     public synchronized ContainerNode toNotification(final NetconfMessage message) {
123         final XmlElement stripped = stripNotification(message);
124         final QName notificationNoRev;
125         try {
126             // How to construct QName with no revision ?
127             notificationNoRev = QName.cachedReference(QName.create(stripped.getNamespace(), "0000-00-00", stripped.getName()).withoutRevision());
128         } catch (final MissingNameSpaceException e) {
129             throw new IllegalArgumentException("Unable to parse notification " + message + ", cannot find namespace", e);
130         }
131
132         final Collection<NotificationDefinition> notificationDefinitions = mappedNotifications.get(notificationNoRev);
133         Preconditions.checkArgument(notificationDefinitions.size() > 0,
134                 "Unable to parse notification %s, unknown notification. Available notifications: %s", notificationDefinitions, mappedNotifications.keySet());
135
136         // FIXME if multiple revisions for same notifications are present, we should pick the most recent. Or ?
137         // We should probably just put the most recent notification versions into our map. We can expect that the device sends the data according to the latest available revision of a model.
138         final NotificationDefinition next = notificationDefinitions.iterator().next();
139
140         // We wrap the notification as a container node in order to reuse the parsers and builders for container node
141         final ContainerSchemaNode notificationAsContainerSchemaNode = NetconfMessageTransformUtil.createSchemaForNotification(next);
142         return NORMALIZED_NODE_PARSER_FACTORY.getContainerNodeParser().parse(Collections.singleton(stripped.getDomElement()), notificationAsContainerSchemaNode);
143     }
144
145     // FIXME move somewhere to util
146     private static XmlElement stripNotification(final NetconfMessage message) {
147         final XmlElement xmlElement = XmlElement.fromDomDocument(message.getDocument());
148         final List<XmlElement> childElements = xmlElement.getChildElements();
149         Preconditions.checkArgument(childElements.size() == 2, "Unable to parse notification %s, unexpected format", message);
150         try {
151             return Iterables.find(childElements, new Predicate<XmlElement>() {
152                 @Override
153                 public boolean apply(final XmlElement xmlElement) {
154                     return !xmlElement.getName().equals("eventTime");
155                 }
156             });
157         } catch (final NoSuchElementException e) {
158             throw new IllegalArgumentException("Unable to parse notification " + message + ", cannot strip notification metadata", e);
159         }
160     }
161
162     @Override
163     public NetconfMessage toRpcRequest(SchemaPath rpc, final ContainerNode payload) {
164         // In case no input for rpc is defined, we can simply construct the payload here
165         final QName rpcQName = rpc.getLastComponent();
166         Preconditions.checkNotNull(mappedRpcs.get(rpcQName), "Unknown rpc %s, available rpcs: %s", rpcQName, mappedRpcs.keySet());
167         if(mappedRpcs.get(rpcQName).getInput() == null) {
168             final Document document = XmlUtil.newDocument();
169             final Element elementNS = document.createElementNS(rpcQName.getNamespace().toString(), rpcQName.getLocalName());
170             document.appendChild(elementNS);
171             return new NetconfMessage(document);
172         }
173
174         // Set the path to the input of rpc for the node stream writer
175         rpc = rpc.createChild(QName.cachedReference(QName.create(rpcQName, "input")));
176         final DOMResult result = prepareDomResultForRpcRequest(rpcQName);
177
178         try {
179             final SchemaContext baseNetconfCtx = schemaContext.findModuleByNamespace(NETCONF_URI).isEmpty() ? BASE_NETCONF_CTX : schemaContext;
180             if(NetconfMessageTransformUtil.isDataEditOperation(rpcQName)) {
181                 writeNormalizedEdit(payload, result, rpc, baseNetconfCtx);
182             } else if(NetconfMessageTransformUtil.isDataRetrievalOperation(rpcQName)) {
183                 writeNormalizedGet(payload, result, rpc, baseNetconfCtx);
184             } else {
185                 writeNormalizedRpc(payload, result, rpc, schemaContext);
186             }
187         } catch (final XMLStreamException | IOException | IllegalStateException e) {
188             throw new IllegalStateException("Unable to serialize " + rpc, e);
189         }
190
191         final Document node = result.getNode().getOwnerDocument();
192
193         node.getDocumentElement().setAttribute(NetconfMessageTransformUtil.MESSAGE_ID_ATTR, counter.getNewMessageId(MESSAGE_ID_PREFIX));
194         return new NetconfMessage(node);
195     }
196
197     private DOMResult prepareDomResultForRpcRequest(final QName rpcQName) {
198         final Document document = XmlUtil.newDocument();
199         final Element rpcNS = document.createElementNS(NETCONF_RPC_QNAME.getNamespace().toString(), NETCONF_RPC_QNAME.getLocalName());
200         final Element elementNS = document.createElementNS(rpcQName.getNamespace().toString(), rpcQName.getLocalName());
201         rpcNS.appendChild(elementNS);
202         document.appendChild(rpcNS);
203         return new DOMResult(elementNS);
204     }
205
206     static final XMLOutputFactory XML_FACTORY;
207     static {
208         XML_FACTORY = XMLOutputFactory.newFactory();
209         XML_FACTORY.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);
210     }
211
212     // FIXME similar code is in netconf-notifications-impl , DRY
213     private void writeNormalizedNode(final NormalizedNode<?, ?> normalized, final DOMResult result, final SchemaPath schemaPath, final SchemaContext context)
214             throws IOException, XMLStreamException {
215         NormalizedNodeWriter normalizedNodeWriter = null;
216         NormalizedNodeStreamWriter normalizedNodeStreamWriter = null;
217         XMLStreamWriter writer = null;
218         try {
219             writer = XML_FACTORY.createXMLStreamWriter(result);
220             normalizedNodeStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(writer, context, schemaPath);
221             normalizedNodeWriter = NormalizedNodeWriter.forStreamWriter(normalizedNodeStreamWriter);
222
223             normalizedNodeWriter.write(normalized);
224
225             normalizedNodeWriter.flush();
226         } finally {
227             try {
228                 if(normalizedNodeWriter != null) {
229                     normalizedNodeWriter.close();
230                 }
231                 if(normalizedNodeStreamWriter != null) {
232                     normalizedNodeStreamWriter.close();
233                 }
234                 if(writer != null) {
235                     writer.close();
236                 }
237             } catch (final Exception e) {
238                 LOG.warn("Unable to close resource properly", e);
239             }
240         }
241     }
242
243     private void writeNormalizedEdit(final ContainerNode normalized, final DOMResult result, final SchemaPath schemaPath, final SchemaContext baseNetconfCtx) throws IOException, XMLStreamException {
244         final NormalizedNodeWriter normalizedNodeWriter;
245         NormalizedNodeStreamWriter normalizedNodeStreamWriter = null;
246         XMLStreamWriter writer = null;
247         try {
248             writer = XML_FACTORY.createXMLStreamWriter(result);
249             normalizedNodeStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(writer, baseNetconfCtx, schemaPath);
250             normalizedNodeWriter = NormalizedNodeWriter.forStreamWriter(normalizedNodeStreamWriter);
251
252             Optional<Iterable<Element>> editDataElements = Optional.absent();
253             for (final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> editElement : normalized.getValue()) {
254                 if(editElement.getNodeType().getLocalName().equals(EditContent.QNAME.getLocalName())) {
255                     Preconditions.checkState(editElement instanceof ChoiceNode,
256                             "Edit content element is expected to be %s, not %s", ChoiceNode.class, editElement);
257                     final Optional<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> configContentHolder =
258                             ((ChoiceNode) editElement).getChild(toId(NETCONF_CONFIG_QNAME));
259                     // TODO The config element inside the EditContent should be AnyXml not Container, but AnyXml is based on outdated API
260                     Preconditions.checkState(configContentHolder.isPresent() && configContentHolder.get() instanceof ContainerNode,
261                             "Edit content/config element is expected to be present as a container node");
262                     normalizedNodeStreamWriter.startChoiceNode(toId(editElement.getNodeType()), 1);
263                     normalizedNodeStreamWriter.anyxmlNode(toId(NETCONF_CONFIG_QNAME), null);
264                     normalizedNodeStreamWriter.endNode();
265
266                     editDataElements = Optional.of(serializeAnyXmlAccordingToSchema(((ContainerNode) configContentHolder.get()).getValue()));
267                 } else {
268                     normalizedNodeWriter.write(editElement);
269                 }
270             }
271
272             normalizedNodeWriter.flush();
273
274             // FIXME this is a workaround for filter content serialization
275             // Any xml is not supported properly by the stream writer
276             if(editDataElements.isPresent()) {
277                 appendEditData(result, editDataElements.get());
278             }
279         } finally {
280             try {
281                 if(normalizedNodeStreamWriter != null) {
282                     normalizedNodeStreamWriter.close();
283                 }
284                 if(writer != null) {
285                     writer.close();
286                 }
287             } catch (final Exception e) {
288                 LOG.warn("Unable to close resource properly", e);
289             }
290         }
291     }
292
293     private void writeNormalizedRpc(final ContainerNode normalized, final DOMResult result, final SchemaPath schemaPath, final SchemaContext baseNetconfCtx) throws IOException, XMLStreamException {
294         final NormalizedNodeWriter normalizedNodeWriter;
295         NormalizedNodeStreamWriter normalizedNodeStreamWriter = null;
296         XMLStreamWriter writer = null;
297         try {
298             writer = XML_FACTORY.createXMLStreamWriter(result);
299             normalizedNodeStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(writer, baseNetconfCtx, schemaPath);
300             normalizedNodeWriter = NormalizedNodeWriter.forStreamWriter(normalizedNodeStreamWriter);
301
302             for (final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> editElement : normalized.getValue()) {
303                 normalizedNodeWriter.write(editElement);
304             }
305             normalizedNodeWriter.flush();
306         } finally {
307             try {
308                 if(normalizedNodeStreamWriter != null) {
309                     normalizedNodeStreamWriter.close();
310                 }
311                 if(writer != null) {
312                     writer.close();
313                 }
314             } catch (final Exception e) {
315                 LOG.warn("Unable to close resource properly", e);
316             }
317         }
318     }
319
320     private void writeNormalizedGet(final ContainerNode normalized, final DOMResult result, final SchemaPath schemaPath, final SchemaContext baseNetconfCtx) throws IOException, XMLStreamException {
321         final NormalizedNodeWriter normalizedNodeWriter;
322         NormalizedNodeStreamWriter normalizedNodeStreamWriter = null;
323         XMLStreamWriter writer = null;
324         try {
325             writer = XML_FACTORY.createXMLStreamWriter(result);
326             normalizedNodeStreamWriter = XMLStreamNormalizedNodeStreamWriter.create(writer, baseNetconfCtx, schemaPath);
327             normalizedNodeWriter = NormalizedNodeWriter.forStreamWriter(normalizedNodeStreamWriter);
328
329             Optional<Iterable<Element>> filterElements = Optional.absent();
330
331             for (final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> editElement : normalized.getValue()) {
332                 Preconditions.checkState(editElement instanceof ContainerNode);
333                 if(editElement.getNodeType().getLocalName().equals(NETCONF_FILTER_QNAME.getLocalName())) {
334                     Preconditions.checkState(editElement instanceof ContainerNode,
335                             "Filter element is expected to be %s, not %s", ContainerNode.class, editElement);
336                     normalizedNodeStreamWriter.anyxmlNode(toId(editElement.getNodeType()), null);
337                     filterElements = Optional.of(serializeAnyXmlAccordingToSchema(((ContainerNode) editElement).getValue()));
338                 } else {
339                     normalizedNodeWriter.write(editElement);
340                 }
341             }
342
343             normalizedNodeWriter.flush();
344
345             // FIXME this is a workaround for filter content serialization
346             // Any xml is not supported properly by the stream writer
347             if(filterElements.isPresent()) {
348                 appendFilter(result, filterElements.get());
349             }
350         } finally {
351             try {
352                 if(normalizedNodeStreamWriter != null) {
353                     normalizedNodeStreamWriter.close();
354                 }
355                 if(writer != null) {
356                     writer.close();
357                 }
358             } catch (final Exception e) {
359                 LOG.warn("Unable to close resource properly", e);
360             }
361         }
362     }
363
364     private void appendFilter(final DOMResult result, final Iterable<Element> filterElements) {
365         final Element rpcElement = ((Element) result.getNode());
366         final Node filterParent = rpcElement.getElementsByTagNameNS(NETCONF_FILTER_QNAME.getNamespace().toString(), NETCONF_FILTER_QNAME.getLocalName()).item(0);
367         final Document ownerDocument = rpcElement.getOwnerDocument();
368         // TODO workaround, add subtree attribute, since it is not serialized by the caller of this method
369         ((Element) filterParent).setAttributeNS(NETCONF_TYPE_QNAME.getNamespace().toString(), NETCONF_TYPE_QNAME.getLocalName(), "subtree");
370         for (final Element element : filterElements) {
371             filterParent.appendChild(ownerDocument.importNode(element, true));
372         }
373     }
374
375     private void appendEditData(final DOMResult result, final Iterable<Element> filterElements) {
376         final Element rpcElement = ((Element) result.getNode());
377         final Node configParent = rpcElement.getElementsByTagNameNS(NETCONF_CONFIG_QNAME.getNamespace().toString(), NETCONF_CONFIG_QNAME.getLocalName()).item(0);
378         for (final Element element : filterElements) {
379             configParent.appendChild(rpcElement.getOwnerDocument().importNode(element, true));
380         }
381     }
382
383     private Iterable<Element> serializeAnyXmlAccordingToSchema(final Iterable<DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?>> values) throws IOException, XMLStreamException {
384         return Iterables.transform(values, new Function<DataContainerChild<? extends YangInstanceIdentifier.PathArgument,?>, Element>() {
385             @Override
386             public Element apply(final DataContainerChild<? extends YangInstanceIdentifier.PathArgument, ?> input) {
387                 final DOMResult domResult = new DOMResult(XmlUtil.newDocument());
388                 try {
389                     writeNormalizedNode(input, domResult, SchemaPath.ROOT, schemaContext);
390                 } catch (IOException | XMLStreamException e) {
391                     throw new IllegalStateException(e);
392                 }
393                 return ((Document) domResult.getNode()).getDocumentElement();
394             }
395         });
396     }
397
398     @Override
399     public synchronized DOMRpcResult toRpcResult(final NetconfMessage message, final SchemaPath rpc) {
400         final NormalizedNode<?, ?> normalizedNode;
401         if (NetconfMessageTransformUtil.isDataRetrievalOperation(rpc.getLastComponent())) {
402             final Element xmlData = NetconfMessageTransformUtil.getDataSubtree(message.getDocument());
403             final ContainerSchemaNode schemaForDataRead = NetconfMessageTransformUtil.createSchemaForDataRead(schemaContext);
404             final ContainerNode dataNode = NORMALIZED_NODE_PARSER_FACTORY.getContainerNodeParser().parse(Collections.singleton(xmlData), schemaForDataRead);
405
406             // TODO check if the response is wrapper correctly
407             normalizedNode = Builders.containerBuilder().withNodeIdentifier(new YangInstanceIdentifier.NodeIdentifier(NetconfMessageTransformUtil.NETCONF_RPC_REPLY_QNAME))
408                     .withChild(dataNode).build();
409         } else {
410             final Set<Element> documentElement = Collections.singleton(message.getDocument().getDocumentElement());
411             final RpcDefinition rpcDefinition = mappedRpcs.get(rpc.getLastComponent());
412             Preconditions.checkArgument(rpcDefinition != null, "Unable to parse response of %s, the rpc is unknown", rpc.getLastComponent());
413
414             // In case no input for rpc is defined, we can simply construct the payload here
415             if(rpcDefinition.getOutput() == null) {
416                 Preconditions.checkArgument(XmlElement.fromDomDocument(message.getDocument()).getOnlyChildElementWithSameNamespaceOptionally("ok").isPresent(),
417                         "Unexpected content in response of rpc: %s, %s", rpcDefinition.getQName(), message);
418                 normalizedNode = null;
419             } else {
420                 normalizedNode = NORMALIZED_NODE_PARSER_FACTORY.getContainerNodeParser().parse(documentElement, rpcDefinition.getOutput());
421             }
422         }
423         return new DefaultDOMRpcResult(normalizedNode);
424     }
425
426 }