00455db3cd64f30ea4dd338c0bb5262d9dca695d
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / databind / XmlChildBody.java
1 /*
2  * Copyright (c) 2023 PANTHEON.tech, s.r.o. 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.restconf.nb.rfc8040.databind;
9
10 import static com.google.common.base.Preconditions.checkState;
11
12 import com.google.common.collect.ImmutableList;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.net.URISyntaxException;
16 import javax.xml.stream.XMLStreamException;
17 import javax.xml.transform.dom.DOMSource;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
20 import org.opendaylight.restconf.server.api.DataPostPath;
21 import org.opendaylight.yangtools.util.xml.UntrustedXML;
22 import org.opendaylight.yangtools.yang.common.ErrorTag;
23 import org.opendaylight.yangtools.yang.common.ErrorType;
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.common.XMLNamespace;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
27 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
28 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
29 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
30 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
31 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
32 import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
33 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
35 import org.slf4j.Logger;
36 import org.slf4j.LoggerFactory;
37 import org.w3c.dom.Document;
38 import org.xml.sax.SAXException;
39
40 public final class XmlChildBody extends ChildBody {
41     private static final Logger LOG = LoggerFactory.getLogger(XmlChildBody.class);
42
43     public XmlChildBody(final InputStream inputStream) {
44         super(inputStream);
45     }
46
47     @Override
48     @SuppressWarnings("checkstyle:illegalCatch")
49     PrefixAndBody toPayload(final DataPostPath path, final InputStream inputStream) {
50         try {
51             return parse(path, UntrustedXML.newDocumentBuilder().parse(inputStream));
52         } catch (final RestconfDocumentedException e) {
53             throw e;
54         } catch (final Exception e) {
55             LOG.debug("Error parsing xml input", e);
56             throwIfYangError(e);
57             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
58                     ErrorTag.MALFORMED_MESSAGE, e);
59         }
60     }
61
62     private static @NonNull PrefixAndBody parse(final DataPostPath path, final Document doc)
63             throws XMLStreamException, IOException, SAXException, URISyntaxException {
64         final var pathInference = path.inference();
65
66         final DataSchemaNode parentNode;
67         if (pathInference.isEmpty()) {
68             parentNode = pathInference.modelContext();
69         } else {
70             final var hackStack = pathInference.toSchemaInferenceStack();
71             final var hackStmt = hackStack.currentStatement();
72             if (hackStmt instanceof DataSchemaNode data) {
73                 parentNode = data;
74             } else {
75                 throw new IllegalStateException("Unknown SchemaNode " + hackStmt);
76             }
77         }
78
79         var schemaNode = parentNode;
80         final String docRootElm = doc.getDocumentElement().getLocalName();
81         final XMLNamespace docRootNamespace = XMLNamespace.of(doc.getDocumentElement().getNamespaceURI());
82         final var context = pathInference.modelContext();
83         final var it = context.findModuleStatements(docRootNamespace).iterator();
84         checkState(it.hasNext(), "Failed to find module for %s", docRootNamespace);
85         final var qname = QName.create(it.next().localQNameModule(), docRootElm);
86
87         final var iiToDataList = ImmutableList.<PathArgument>builder();
88         // FIXME: we should have this readily available: it is the last node the ApiPath->YangInstanceIdentifier parser
89         //        has seen (and it should have the nodeAndStack handy
90         final var nodeAndStack = path.databind().schemaTree().enterPath(path.instance()).orElseThrow();
91         final var stack = nodeAndStack.stack();
92         var current = nodeAndStack.node();
93         do {
94             final var next = current instanceof DataSchemaContext.Composite compositeCurrent
95                 ? compositeCurrent.enterChild(stack, qname) : null;
96             if (next == null) {
97                 throw new IllegalStateException(
98                     "Child \"" + qname + "\" was not found in parent schema node \"" + schemaNode + "\"");
99             }
100
101             // Careful about steps: for keyed list items the individual item does not have a PathArgument step,
102             // as we do not know the key values -- we supply that later
103             final var step = next.pathStep();
104             if (step != null) {
105                 iiToDataList.add(step);
106             }
107             schemaNode = next.dataSchemaNode();
108             current = next;
109         } while (current instanceof PathMixin);
110
111         final var resultHolder = new NormalizationResultHolder();
112         final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
113
114         final var xmlParser = XmlParserStream.create(writer, path.databind().xmlCodecs(), stack.toInference());
115         xmlParser.traverse(new DOMSource(doc.getDocumentElement()));
116         var parsed = resultHolder.getResult().data();
117
118         // When parsing an XML source with a list root node
119         // the new XML parser always returns a MapNode with one MapEntryNode inside.
120         // However, the old XML parser returned a MapEntryNode directly in this place.
121         // Therefore we now have to extract the MapEntryNode from the parsed MapNode.
122         if (parsed instanceof MapNode mapNode) {
123             // extracting the MapEntryNode
124             parsed = mapNode.body().iterator().next();
125         }
126
127         if (schemaNode instanceof ListSchemaNode) {
128             // Supply the last item
129             iiToDataList.add(parsed.name());
130         }
131
132         return new PrefixAndBody(iiToDataList.build(), parsed);
133     }
134 }