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