Bug 7176 - Netconf MD-SAL connector is treating prefixed XML as invalid
[netconf.git] / netconf / mdsal-netconf-connector / src / main / java / org / opendaylight / netconf / mdsal / connector / ops / EditConfig.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
9 package org.opendaylight.netconf.mdsal.connector.ops;
10
11 import com.google.common.annotations.VisibleForTesting;
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 java.net.URI;
17 import java.net.URISyntaxException;
18 import java.util.List;
19 import java.util.ListIterator;
20 import java.util.Map;
21 import java.util.stream.Collectors;
22 import javax.xml.transform.dom.DOMSource;
23 import org.opendaylight.controller.config.util.xml.DocumentedException;
24 import org.opendaylight.controller.config.util.xml.DocumentedException.ErrorSeverity;
25 import org.opendaylight.controller.config.util.xml.DocumentedException.ErrorTag;
26 import org.opendaylight.controller.config.util.xml.DocumentedException.ErrorType;
27 import org.opendaylight.controller.config.util.xml.XmlElement;
28 import org.opendaylight.controller.config.util.xml.XmlUtil;
29 import org.opendaylight.controller.md.sal.common.api.data.LogicalDatastoreType;
30 import org.opendaylight.controller.md.sal.common.api.data.ReadFailedException;
31 import org.opendaylight.controller.md.sal.dom.api.DOMDataReadWriteTransaction;
32 import org.opendaylight.netconf.api.NetconfDocumentedException;
33 import org.opendaylight.netconf.api.xml.XmlNetconfConstants;
34 import org.opendaylight.netconf.mdsal.connector.CurrentSchemaContext;
35 import org.opendaylight.netconf.mdsal.connector.TransactionProvider;
36 import org.opendaylight.netconf.mdsal.connector.ops.DataTreeChangeTracker.DataTreeChange;
37 import org.opendaylight.netconf.util.mapping.AbstractSingletonNetconfOperation;
38 import org.opendaylight.yangtools.yang.common.QName;
39 import org.opendaylight.yangtools.yang.data.api.ModifyAction;
40 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
41 import org.opendaylight.yangtools.yang.data.api.schema.AugmentationNode;
42 import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
43 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
44 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
45 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
46 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
47 import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
48 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
49 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
51 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.Module;
53 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
54 import org.opendaylight.yangtools.yang.model.util.SchemaContextUtil;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57 import org.w3c.dom.Document;
58 import org.w3c.dom.Element;
59 import org.w3c.dom.NodeList;
60
61 public class EditConfig extends AbstractSingletonNetconfOperation {
62
63     private static final Logger LOG = LoggerFactory.getLogger(EditConfig.class);
64
65     private static final String OPERATION_NAME = "edit-config";
66     private static final String CONFIG_KEY = "config";
67     private static final String TARGET_KEY = "target";
68     private static final String DEFAULT_OPERATION_KEY = "default-operation";
69     private final CurrentSchemaContext schemaContext;
70     private final TransactionProvider transactionProvider;
71
72     public EditConfig(final String netconfSessionIdForReporting, final CurrentSchemaContext schemaContext,
73             final TransactionProvider transactionProvider) {
74         super(netconfSessionIdForReporting);
75         this.schemaContext = schemaContext;
76         this.transactionProvider = transactionProvider;
77     }
78
79     @Override
80     protected Element handleWithNoSubsequentOperations(final Document document, final XmlElement operationElement)
81             throws DocumentedException {
82         final Datastore targetDatastore = extractTargetParameter(operationElement);
83         if (targetDatastore == Datastore.running) {
84             throw new DocumentedException("edit-config on running datastore is not supported",
85                     ErrorType.PROTOCOL,
86                     ErrorTag.OPERATION_NOT_SUPPORTED,
87                     ErrorSeverity.ERROR);
88         }
89
90         final ModifyAction defaultAction = getDefaultOperation(operationElement);
91
92         final XmlElement configElement = getElement(operationElement, CONFIG_KEY);
93
94         for (final XmlElement element : configElement.getChildElements()) {
95             final String ns = element.getNamespace();
96             final DataSchemaNode schemaNode = getSchemaNodeFromNamespace(ns, element).get();
97
98             final DataTreeChangeTracker changeTracker = new DataTreeChangeTracker(defaultAction);
99
100             parseIntoNormalizedNode(schemaNode, element, changeTracker);
101             executeOperations(changeTracker);
102         }
103
104         return XmlUtil.createElement(document, XmlNetconfConstants.OK, Optional.absent());
105     }
106
107     private void executeOperations(final DataTreeChangeTracker changeTracker) throws DocumentedException {
108         final DOMDataReadWriteTransaction rwTx = transactionProvider.getOrCreateTransaction();
109         final List<DataTreeChange> aa = changeTracker.getDataTreeChanges();
110         final ListIterator<DataTreeChange> iterator = aa.listIterator(aa.size());
111
112         while (iterator.hasPrevious()) {
113             final DataTreeChange dtc = iterator.previous();
114             executeChange(rwTx, dtc);
115         }
116     }
117
118     private void executeChange(final DOMDataReadWriteTransaction rwtx, final DataTreeChange change)
119             throws DocumentedException {
120         final YangInstanceIdentifier path = YangInstanceIdentifier.create(change.getPath());
121         final NormalizedNode<?, ?> changeData = change.getChangeRoot();
122         switch (change.getAction()) {
123             case NONE:
124                 return;
125             case MERGE:
126                 mergeParentMixin(rwtx, path, changeData);
127                 rwtx.merge(LogicalDatastoreType.CONFIGURATION, path, changeData);
128                 break;
129             case CREATE:
130                 try {
131                     final Optional<NormalizedNode<?, ?>> readResult =
132                             rwtx.read(LogicalDatastoreType.CONFIGURATION, path).checkedGet();
133                     if (readResult.isPresent()) {
134                         throw new DocumentedException("Data already exists, cannot execute CREATE operation",
135                             ErrorType.PROTOCOL, ErrorTag.DATA_EXISTS, ErrorSeverity.ERROR);
136                     }
137                     mergeParentMixin(rwtx, path, changeData);
138                     rwtx.put(LogicalDatastoreType.CONFIGURATION, path, changeData);
139                 } catch (final ReadFailedException e) {
140                     LOG.warn("Read from datastore failed when trying to read data for create operation", change, e);
141                 }
142                 break;
143             case REPLACE:
144                 mergeParentMixin(rwtx, path, changeData);
145                 rwtx.put(LogicalDatastoreType.CONFIGURATION, path, changeData);
146                 break;
147             case DELETE:
148                 try {
149                     final Optional<NormalizedNode<?, ?>> readResult =
150                             rwtx.read(LogicalDatastoreType.CONFIGURATION, path).checkedGet();
151                     if (!readResult.isPresent()) {
152                         throw new DocumentedException("Data is missing, cannot execute DELETE operation",
153                             ErrorType.PROTOCOL, ErrorTag.DATA_MISSING, ErrorSeverity.ERROR);
154                     }
155                     rwtx.delete(LogicalDatastoreType.CONFIGURATION, path);
156                 } catch (final ReadFailedException e) {
157                     LOG.warn("Read from datastore failed when trying to read data for delete operation", change, e);
158                 }
159                 break;
160             case REMOVE:
161                 rwtx.delete(LogicalDatastoreType.CONFIGURATION, path);
162                 break;
163             default:
164                 LOG.warn("Unknown/not implemented operation, not executing");
165         }
166     }
167
168     private void mergeParentMixin(final DOMDataReadWriteTransaction rwtx, final YangInstanceIdentifier path,
169                                 final NormalizedNode change) {
170         final YangInstanceIdentifier parentNodeYid = path.getParent();
171         if (change instanceof MapEntryNode) {
172             final SchemaNode schemaNode = SchemaContextUtil.findNodeInSchemaContext(
173                     schemaContext.getCurrentContext(),
174                     parentNodeYid.getPathArguments().stream()
175                             // filter out identifiers not present in the schema tree
176                             .filter(arg -> !(arg instanceof YangInstanceIdentifier.NodeIdentifierWithPredicates))
177                             .filter(arg -> !(arg instanceof YangInstanceIdentifier.AugmentationIdentifier))
178                             .map(YangInstanceIdentifier.PathArgument::getNodeType).collect(Collectors.toList()));
179
180             // we should have the schema node that points to the parent list now, enforce it
181             Preconditions.checkState(schemaNode instanceof ListSchemaNode, "Schema node is not pointing to a list.");
182
183             //merge empty ordered or unordered map
184             if (((ListSchemaNode) schemaNode).isUserOrdered()) {
185                 final MapNode mixinNode = Builders.orderedMapBuilder()
186                         .withNodeIdentifier(
187                                 new YangInstanceIdentifier.NodeIdentifier(
188                                         parentNodeYid.getLastPathArgument().getNodeType()))
189                         .build();
190                 rwtx.merge(LogicalDatastoreType.CONFIGURATION, parentNodeYid, mixinNode);
191                 return;
192             }
193
194             final MapNode mixinNode = Builders.mapBuilder()
195                     .withNodeIdentifier(
196                             new YangInstanceIdentifier.NodeIdentifier(
197                                         parentNodeYid.getLastPathArgument().getNodeType()))
198                     .build();
199             rwtx.merge(LogicalDatastoreType.CONFIGURATION, parentNodeYid, mixinNode);
200         } else if (parentNodeYid.getLastPathArgument() instanceof YangInstanceIdentifier.AugmentationIdentifier) {
201             // merge empty augmentation node
202             final YangInstanceIdentifier.AugmentationIdentifier augmentationYid =
203                 (YangInstanceIdentifier.AugmentationIdentifier) parentNodeYid.getLastPathArgument();
204             final AugmentationNode augmentationNode = Builders.augmentationBuilder()
205                 .withNodeIdentifier(augmentationYid).build();
206             rwtx.merge(LogicalDatastoreType.CONFIGURATION, parentNodeYid, augmentationNode);
207         }
208     }
209
210     @SuppressWarnings("checkstyle:IllegalCatch")
211     private NormalizedNode<?, ?> parseIntoNormalizedNode(final DataSchemaNode schemaNode, final XmlElement element,
212             final DataTreeChangeTracker changeTracker) throws DocumentedException {
213         if (!(schemaNode instanceof ContainerSchemaNode) && !(schemaNode instanceof ListSchemaNode)) {
214             //this should never happen since edit-config on any other node type should not be possible nor makes sense
215             LOG.debug("DataNode from module is not ContainerSchemaNode nor ListSchemaNode, aborting..");
216             throw new UnsupportedOperationException("implement exception if parse fails");
217         }
218
219         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
220         final NormalizedNodeStreamWriter writer = new EditOperationNormalizedNodeStreamWriter(resultHolder,
221                 changeTracker);
222         final XmlParserStream xmlParser = XmlParserStream.create(writer, schemaContext.getCurrentContext(), schemaNode);
223         try {
224             xmlParser.traverse(new DOMSource(element.getDomElement()));
225         } catch (final Exception ex) {
226             throw new NetconfDocumentedException("Error parsing input: " + ex.getMessage(), ex, ErrorType.PROTOCOL,
227                     ErrorTag.MALFORMED_MESSAGE, ErrorSeverity.ERROR);
228         }
229
230         return resultHolder.getResult();
231     }
232
233     private Optional<DataSchemaNode> getSchemaNodeFromNamespace(final String namespace, final XmlElement element)
234             throws DocumentedException {
235         Optional<DataSchemaNode> dataSchemaNode = Optional.absent();
236         try {
237             // returns module with newest revision since findModuleByNamespace returns a set of modules and we only
238             // need the newest one
239             final Module module =
240                     schemaContext.getCurrentContext().findModuleByNamespaceAndRevision(new URI(namespace), null);
241             if (module == null) {
242                 // no module is present with this namespace
243                 throw new NetconfDocumentedException("Unable to find module by namespace: " + namespace,
244                         ErrorType.APPLICATION, ErrorTag.UNKNOWN_NAMESPACE, ErrorSeverity.ERROR);
245             }
246             final DataSchemaNode schemaNode =
247                     module.getDataChildByName(QName.create(module.getQNameModule(), element.getName()));
248             if (schemaNode != null) {
249                 dataSchemaNode = Optional.of(schemaNode);
250             } else {
251                 throw new DocumentedException(
252                         "Unable to find node with namespace: " + namespace + "in module: " + module.toString(),
253                         ErrorType.APPLICATION,
254                         ErrorTag.UNKNOWN_NAMESPACE,
255                         ErrorSeverity.ERROR);
256             }
257         } catch (final URISyntaxException e) {
258             LOG.debug("Unable to create URI for namespace : {}", namespace);
259         }
260
261         return dataSchemaNode;
262     }
263
264     private static Datastore extractTargetParameter(final XmlElement operationElement) throws DocumentedException {
265         final NodeList elementsByTagName = getElementsByTagName(operationElement, TARGET_KEY);
266         // Direct lookup instead of using XmlElement class due to performance
267         if (elementsByTagName.getLength() == 0) {
268             final Map<String, String> errorInfo = ImmutableMap.of("bad-attribute", TARGET_KEY, "bad-element",
269                 OPERATION_NAME);
270             throw new DocumentedException("Missing target element", ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE,
271                 ErrorSeverity.ERROR, errorInfo);
272         } else if (elementsByTagName.getLength() > 1) {
273             throw new DocumentedException("Multiple target elements", ErrorType.RPC, ErrorTag.UNKNOWN_ATTRIBUTE,
274                 ErrorSeverity.ERROR);
275         } else {
276             final XmlElement targetChildNode =
277                     XmlElement.fromDomElement((Element) elementsByTagName.item(0)).getOnlyChildElement();
278             return Datastore.valueOf(targetChildNode.getName());
279         }
280     }
281
282     private static ModifyAction getDefaultOperation(final XmlElement operationElement) throws DocumentedException {
283         final NodeList elementsByTagName = getElementsByTagName(operationElement, DEFAULT_OPERATION_KEY);
284         if (elementsByTagName.getLength() == 0) {
285             return ModifyAction.MERGE;
286         } else if (elementsByTagName.getLength() > 1) {
287             throw new DocumentedException("Multiple " + DEFAULT_OPERATION_KEY + " elements", ErrorType.RPC,
288                 ErrorTag.UNKNOWN_ATTRIBUTE, ErrorSeverity.ERROR);
289         } else {
290             return ModifyAction.fromXmlValue(elementsByTagName.item(0).getTextContent());
291         }
292
293     }
294
295     private static XmlElement getElement(final XmlElement operationElement, final String elementName)
296             throws DocumentedException {
297         final Optional<XmlElement> childNode = operationElement.getOnlyChildElementOptionally(elementName);
298         if (!childNode.isPresent()) {
299             throw new DocumentedException(elementName + " element is missing",
300                     ErrorType.PROTOCOL,
301                     ErrorTag.MISSING_ELEMENT,
302                     ErrorSeverity.ERROR);
303         }
304
305         return childNode.get();
306     }
307
308     @VisibleForTesting
309     static NodeList getElementsByTagName(final XmlElement operationElement, final String key) throws
310             DocumentedException {
311         final Element element = operationElement.getDomElement();
312         final NodeList elementsByTagName;
313
314         if (Strings.isNullOrEmpty(element.getPrefix())) {
315             elementsByTagName = element.getElementsByTagName(key);
316         } else {
317             elementsByTagName = element.getElementsByTagNameNS(operationElement.getNamespace(), key);
318         }
319
320         return elementsByTagName;
321     }
322
323     @Override
324     protected String getOperationName() {
325         return OPERATION_NAME;
326     }
327
328 }