Prepare netconf to support YANG 1.1 actions
[netconf.git] / netconf / sal-netconf-connector / src / main / java / org / opendaylight / netconf / 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.netconf.sal.connect.netconf.schema.mapping;
9
10 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.IETF_NETCONF_NOTIFICATIONS;
11 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.NETCONF_URI;
12 import static org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil.toPath;
13
14 import com.google.common.annotations.VisibleForTesting;
15 import com.google.common.base.Preconditions;
16 import com.google.common.collect.ImmutableSet;
17 import com.google.common.collect.ImmutableSet.Builder;
18 import com.google.common.collect.Maps;
19 import com.google.common.collect.Multimap;
20 import com.google.common.collect.Multimaps;
21 import java.io.IOException;
22 import java.net.URISyntaxException;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Date;
26 import java.util.Map;
27 import java.util.Set;
28 import javax.annotation.Nonnull;
29 import javax.xml.parsers.ParserConfigurationException;
30 import javax.xml.stream.XMLStreamException;
31 import javax.xml.transform.dom.DOMResult;
32 import javax.xml.transform.dom.DOMSource;
33 import org.opendaylight.controller.md.sal.dom.api.DOMEvent;
34 import org.opendaylight.controller.md.sal.dom.api.DOMNotification;
35 import org.opendaylight.controller.md.sal.dom.api.DOMRpcResult;
36 import org.opendaylight.controller.md.sal.dom.spi.DefaultDOMRpcResult;
37 import org.opendaylight.mdsal.dom.api.DOMActionResult;
38 import org.opendaylight.mdsal.dom.api.DOMDataTreeIdentifier;
39 import org.opendaylight.mdsal.dom.spi.SimpleDOMActionResult;
40 import org.opendaylight.netconf.api.NetconfMessage;
41 import org.opendaylight.netconf.api.xml.MissingNameSpaceException;
42 import org.opendaylight.netconf.api.xml.XmlElement;
43 import org.opendaylight.netconf.sal.connect.api.MessageTransformer;
44 import org.opendaylight.netconf.sal.connect.netconf.util.NetconfMessageTransformUtil;
45 import org.opendaylight.netconf.sal.connect.util.MessageCounter;
46 import org.opendaylight.yangtools.yang.common.QName;
47 import org.opendaylight.yangtools.yang.common.Revision;
48 import org.opendaylight.yangtools.yang.common.RpcError;
49 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
50 import org.opendaylight.yangtools.yang.data.api.schema.ContainerNode;
51 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
52 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
53 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
54 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
55 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
56 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
57 import org.opendaylight.yangtools.yang.model.api.ActionDefinition;
58 import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
59 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
60 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
61 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
62 import org.opendaylight.yangtools.yang.model.api.NotificationDefinition;
63 import org.opendaylight.yangtools.yang.model.api.OperationDefinition;
64 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
65 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
66 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
67 import org.opendaylight.yangtools.yang.model.api.SchemaPath;
68 import org.slf4j.Logger;
69 import org.slf4j.LoggerFactory;
70 import org.w3c.dom.Document;
71 import org.w3c.dom.Element;
72 import org.xml.sax.SAXException;
73
74 public class NetconfMessageTransformer implements MessageTransformer<NetconfMessage> {
75
76     private static final Logger LOG = LoggerFactory.getLogger(NetconfMessageTransformer.class);
77
78     private final SchemaContext schemaContext;
79     private final BaseSchema baseSchema;
80     private final MessageCounter counter;
81     private final Map<QName, RpcDefinition> mappedRpcs;
82     private final Multimap<QName, NotificationDefinition> mappedNotifications;
83     private final boolean strictParsing;
84     private final Set<ActionDefinition> actions;
85
86     public NetconfMessageTransformer(final SchemaContext schemaContext, final boolean strictParsing) {
87         this(schemaContext, strictParsing, BaseSchema.BASE_NETCONF_CTX);
88     }
89
90     public NetconfMessageTransformer(final SchemaContext schemaContext, final boolean strictParsing,
91                                      final BaseSchema baseSchema) {
92         this.counter = new MessageCounter();
93         this.schemaContext = schemaContext;
94         this.mappedRpcs = Maps.uniqueIndex(schemaContext.getOperations(), SchemaNode::getQName);
95         this.actions = getActions();
96         this.mappedNotifications = Multimaps.index(schemaContext.getNotifications(),
97             node -> node.getQName().withoutRevision());
98         this.baseSchema = baseSchema;
99         this.strictParsing = strictParsing;
100     }
101
102     @VisibleForTesting
103     Set<ActionDefinition> getActions() {
104         Builder<ActionDefinition> builder = ImmutableSet.builder();
105         for (DataSchemaNode dataSchemaNode : schemaContext.getChildNodes()) {
106             if (dataSchemaNode instanceof ActionNodeContainer) {
107                 findAction(dataSchemaNode, builder);
108             }
109         }
110         return builder.build();
111     }
112
113     private void findAction(DataSchemaNode dataSchemaNode, Builder<ActionDefinition> builder) {
114         if (dataSchemaNode instanceof ActionNodeContainer) {
115             final ActionNodeContainer containerSchemaNode = (ActionNodeContainer) dataSchemaNode;
116             for (ActionDefinition actionDefinition : containerSchemaNode.getActions()) {
117                 builder.add(actionDefinition);
118             }
119         }
120         if (dataSchemaNode instanceof DataNodeContainer) {
121             for (DataSchemaNode innerDataSchemaNode : ((DataNodeContainer) dataSchemaNode).getChildNodes()) {
122                 findAction(innerDataSchemaNode, builder);
123             }
124         }
125     }
126
127     @Override
128     public synchronized DOMNotification toNotification(final NetconfMessage message) {
129         final Map.Entry<Date, XmlElement> stripped = NetconfMessageTransformUtil.stripNotification(message);
130         final QName notificationNoRev;
131         try {
132             notificationNoRev = QName.create(
133                     stripped.getValue().getNamespace(), stripped.getValue().getName()).withoutRevision();
134         } catch (final MissingNameSpaceException e) {
135             throw new IllegalArgumentException(
136                     "Unable to parse notification " + message + ", cannot find namespace", e);
137         }
138         final Collection<NotificationDefinition> notificationDefinitions = mappedNotifications.get(notificationNoRev);
139         Preconditions.checkArgument(notificationDefinitions.size() > 0,
140                 "Unable to parse notification %s, unknown notification. Available notifications: %s",
141                 notificationDefinitions, mappedNotifications.keySet());
142
143         final NotificationDefinition mostRecentNotification = getMostRecentNotification(notificationDefinitions);
144
145         final ContainerSchemaNode notificationAsContainerSchemaNode =
146                 NetconfMessageTransformUtil.createSchemaForNotification(mostRecentNotification);
147
148         final Element element = stripped.getValue().getDomElement();
149         final ContainerNode content;
150         try {
151             final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
152             final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
153             final XmlParserStream xmlParser = XmlParserStream.create(writer, schemaContext,
154                     notificationAsContainerSchemaNode, strictParsing);
155             xmlParser.traverse(new DOMSource(element));
156             content = (ContainerNode) resultHolder.getResult();
157         } catch (XMLStreamException | URISyntaxException | IOException | ParserConfigurationException
158                 | SAXException | UnsupportedOperationException e) {
159             throw new IllegalArgumentException(String.format("Failed to parse notification %s", element), e);
160         }
161         return new NetconfDeviceNotification(content, stripped.getKey());
162     }
163
164     private static NotificationDefinition getMostRecentNotification(
165             final Collection<NotificationDefinition> notificationDefinitions) {
166         return Collections.max(notificationDefinitions, (o1, o2) ->
167             Revision.compare(o1.getQName().getRevision(), o2.getQName().getRevision()));
168     }
169
170     @Override
171     public NetconfMessage toRpcRequest(SchemaPath rpc, final NormalizedNode<?, ?> payload) {
172         // In case no input for rpc is defined, we can simply construct the payload here
173         final QName rpcQName = rpc.getLastComponent();
174         Map<QName, RpcDefinition> currentMappedRpcs = mappedRpcs;
175
176         // Determine whether a base netconf operation is being invoked
177         // and also check if the device exposed model for base netconf.
178         // If no, use pre built base netconf operations model
179         final boolean needToUseBaseCtx = mappedRpcs.get(rpcQName) == null && isBaseOrNotificationRpc(rpcQName);
180         if (needToUseBaseCtx) {
181             currentMappedRpcs = baseSchema.getMappedRpcs();
182         }
183
184         Preconditions.checkNotNull(currentMappedRpcs.get(rpcQName),
185                 "Unknown rpc %s, available rpcs: %s", rpcQName, currentMappedRpcs.keySet());
186         if (currentMappedRpcs.get(rpcQName).getInput().getChildNodes().isEmpty()) {
187             return new NetconfMessage(NetconfMessageTransformUtil
188                     .prepareDomResultForRpcRequest(rpcQName, counter).getNode().getOwnerDocument());
189         }
190
191         Preconditions.checkNotNull(payload, "Transforming an rpc with input: %s, payload cannot be null", rpcQName);
192
193         Preconditions.checkArgument(payload instanceof ContainerNode,
194                 "Transforming an rpc with input: %s, payload has to be a container, but was: %s", rpcQName, payload);
195         // Set the path to the input of rpc for the node stream writer
196         rpc = rpc.createChild(QName.create(rpcQName, "input").intern());
197         final DOMResult result = NetconfMessageTransformUtil.prepareDomResultForRpcRequest(rpcQName, counter);
198
199         try {
200             // If the schema context for netconf device does not contain model for base netconf operations,
201             // use default pre build context with just the base model
202             // This way operations like lock/unlock are supported even if the source for base model was not provided
203             SchemaContext ctx = needToUseBaseCtx ? baseSchema.getSchemaContext() : schemaContext;
204             NetconfMessageTransformUtil.writeNormalizedRpc((ContainerNode) payload, result, rpc, ctx);
205         } catch (final XMLStreamException | IOException | IllegalStateException e) {
206             throw new IllegalStateException("Unable to serialize " + rpc, e);
207         }
208
209         final Document node = result.getNode().getOwnerDocument();
210
211         return new NetconfMessage(node);
212     }
213
214     @Override
215     public NetconfMessage toActionRequest(SchemaPath action, DOMDataTreeIdentifier domDataTreeIdentifier,
216             final NormalizedNode<?, ?> payload) {
217         ActionDefinition actionDefinition = null;
218         SchemaPath schemaPath = action;
219         for (ActionDefinition actionDef : actions) {
220             if (actionDef.getPath().getLastComponent().equals(action.getLastComponent())) {
221                 actionDefinition = actionDef;
222                 schemaPath = actionDef.getPath();
223             }
224         }
225         Preconditions.checkNotNull(actionDefinition, "Action does not exist: %s", action.getLastComponent());
226
227         if (actionDefinition.getInput().getChildNodes().isEmpty()) {
228             return new NetconfMessage(NetconfMessageTransformUtil.prepareDomResultForActionRequest(
229                     domDataTreeIdentifier, action, counter, actionDefinition.getQName().getLocalName())
230                     .getNode().getOwnerDocument());
231         }
232
233         Preconditions.checkNotNull(payload, "Transforming an action with input: %s, payload cannot be null",
234                 action.getLastComponent());
235         Preconditions.checkArgument(payload instanceof ContainerNode,
236                 "Transforming an rpc with input: %s, payload has to be a container, but was: %s",
237                 action.getLastComponent(), payload);
238         // Set the path to the input of rpc for the node stream writer
239         action = action.createChild(QName.create(action.getLastComponent(), "input").intern());
240         final DOMResult result = NetconfMessageTransformUtil.prepareDomResultForActionRequest(
241                 domDataTreeIdentifier, action, counter, actionDefinition.getQName().getLocalName());
242
243         try {
244             NetconfMessageTransformUtil.writeNormalizedRpc((ContainerNode) payload, result,
245                     schemaPath.createChild(QName.create(action.getLastComponent(), "input").intern()), schemaContext);
246         } catch (final XMLStreamException | IOException | IllegalStateException e) {
247             throw new IllegalStateException("Unable to serialize " + action, e);
248         }
249
250         final Document node = result.getNode().getOwnerDocument();
251
252         return new NetconfMessage(node);
253     }
254
255     private static boolean isBaseOrNotificationRpc(final QName rpc) {
256         return rpc.getNamespace().equals(NETCONF_URI)
257                 || rpc.getNamespace().equals(IETF_NETCONF_NOTIFICATIONS.getNamespace())
258                 || rpc.getNamespace().equals(NetconfMessageTransformUtil.CREATE_SUBSCRIPTION_RPC_QNAME.getNamespace());
259     }
260
261     @Override
262     public synchronized DOMRpcResult toRpcResult(final NetconfMessage message, final SchemaPath rpc) {
263         final NormalizedNode<?, ?> normalizedNode;
264         final QName rpcQName = rpc.getLastComponent();
265         if (NetconfMessageTransformUtil.isDataRetrievalOperation(rpcQName)) {
266             final Element xmlData = NetconfMessageTransformUtil.getDataSubtree(message.getDocument());
267             final ContainerSchemaNode schemaForDataRead =
268                     NetconfMessageTransformUtil.createSchemaForDataRead(schemaContext);
269             final ContainerNode dataNode;
270
271             try {
272                 final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
273                 final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
274                 final XmlParserStream xmlParser = XmlParserStream.create(writer, schemaContext, schemaForDataRead,
275                         strictParsing);
276                 xmlParser.traverse(new DOMSource(xmlData));
277                 dataNode = (ContainerNode) resultHolder.getResult();
278             } catch (XMLStreamException | URISyntaxException | IOException | ParserConfigurationException
279                     | SAXException e) {
280                 throw new IllegalArgumentException(String.format("Failed to parse data response %s", xmlData), e);
281             }
282
283             normalizedNode = Builders.containerBuilder()
284                     .withNodeIdentifier(new YangInstanceIdentifier
285                             .NodeIdentifier(NetconfMessageTransformUtil.NETCONF_RPC_REPLY_QNAME))
286                     .withChild(dataNode).build();
287         } else {
288
289             Map<QName, RpcDefinition> currentMappedRpcs = mappedRpcs;
290
291             // Determine whether a base netconf operation is being invoked
292             // and also check if the device exposed model for base netconf.
293             // If no, use pre built base netconf operations model
294             final boolean needToUseBaseCtx = mappedRpcs.get(rpcQName) == null && isBaseOrNotificationRpc(rpcQName);
295             if (needToUseBaseCtx) {
296                 currentMappedRpcs = baseSchema.getMappedRpcs();
297             }
298
299             final RpcDefinition rpcDefinition = currentMappedRpcs.get(rpcQName);
300             Preconditions.checkArgument(rpcDefinition != null,
301                     "Unable to parse response of %s, the rpc is unknown", rpcQName);
302
303             // In case no input for rpc is defined, we can simply construct the payload here
304             normalizedNode = parseResult(message, rpcDefinition);
305         }
306         return new DefaultDOMRpcResult(normalizedNode);
307     }
308
309     @Override
310     public DOMActionResult toActionResult(SchemaPath action, NetconfMessage message) {
311         ActionDefinition actionDefinition = null;
312         for (ActionDefinition actionDef : actions) {
313             if (actionDef.getPath().getLastComponent().equals(action.getLastComponent())) {
314                 actionDefinition = actionDef;
315             }
316         }
317         Preconditions.checkNotNull(actionDefinition, "Action does not exist: %s", action);
318         ContainerNode normalizedNode = (ContainerNode) parseResult(message, actionDefinition);
319
320         return new SimpleDOMActionResult(normalizedNode, Collections.<RpcError>emptyList());
321     }
322
323     private NormalizedNode<?, ?> parseResult(final NetconfMessage message,
324             final OperationDefinition operationDefinition) {
325         if (operationDefinition.getOutput().getChildNodes().isEmpty()) {
326             Preconditions.checkArgument(XmlElement.fromDomDocument(
327                 message.getDocument()).getOnlyChildElementWithSameNamespaceOptionally("ok").isPresent(),
328                 "Unexpected content in response of rpc: %s, %s", operationDefinition.getQName(), message);
329             return null;
330         } else {
331             final Element element = message.getDocument().getDocumentElement();
332             try {
333                 final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
334                 final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
335                 final XmlParserStream xmlParser = XmlParserStream.create(writer, schemaContext,
336                         operationDefinition.getOutput(), strictParsing);
337                 xmlParser.traverse(new DOMSource(element));
338                 return resultHolder.getResult();
339             } catch (XMLStreamException | URISyntaxException | IOException | ParserConfigurationException
340                     | SAXException e) {
341                 throw new IllegalArgumentException(String.format("Failed to parse RPC response %s", element), e);
342             }
343         }
344     }
345
346     static class NetconfDeviceNotification implements DOMNotification, DOMEvent {
347         private final ContainerNode content;
348         private final SchemaPath schemaPath;
349         private final Date eventTime;
350
351         NetconfDeviceNotification(final ContainerNode content, final Date eventTime) {
352             this.content = content;
353             this.eventTime = eventTime;
354             this.schemaPath = toPath(content.getNodeType());
355         }
356
357         @Nonnull
358         @Override
359         public SchemaPath getType() {
360             return schemaPath;
361
362         }
363
364         @Nonnull
365         @Override
366         public ContainerNode getBody() {
367             return content;
368         }
369
370         @Override
371         public Date getEventTime() {
372             return eventTime;
373         }
374     }
375 }