Hide NormalizedNodeContext and WriterParameters
[netconf.git] / restconf / restconf-nb-bierman02 / src / main / java / org / opendaylight / netconf / sal / rest / impl / XmlNormalizedNodeBodyReader.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.rest.impl;
9
10 import com.google.common.base.Preconditions;
11 import com.google.common.collect.Iterables;
12 import java.io.IOException;
13 import java.io.InputStream;
14 import java.lang.annotation.Annotation;
15 import java.lang.reflect.Type;
16 import java.net.URISyntaxException;
17 import java.util.ArrayDeque;
18 import java.util.ArrayList;
19 import java.util.Deque;
20 import java.util.List;
21 import java.util.Optional;
22 import javax.ws.rs.Consumes;
23 import javax.ws.rs.WebApplicationException;
24 import javax.ws.rs.core.MediaType;
25 import javax.ws.rs.core.MultivaluedMap;
26 import javax.ws.rs.ext.MessageBodyReader;
27 import javax.ws.rs.ext.Provider;
28 import javax.xml.parsers.ParserConfigurationException;
29 import javax.xml.stream.XMLStreamException;
30 import javax.xml.transform.dom.DOMSource;
31 import org.opendaylight.netconf.sal.rest.api.Draft02;
32 import org.opendaylight.netconf.sal.rest.api.RestconfService;
33 import org.opendaylight.netconf.sal.restconf.impl.ControllerContext;
34 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
35 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
36 import org.opendaylight.restconf.common.util.RestUtil;
37 import org.opendaylight.yangtools.util.xml.UntrustedXML;
38 import org.opendaylight.yangtools.yang.common.ErrorTag;
39 import org.opendaylight.yangtools.yang.common.ErrorType;
40 import org.opendaylight.yangtools.yang.common.QName;
41 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
42 import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
43 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
44 import org.opendaylight.yangtools.yang.data.api.schema.stream.NormalizedNodeStreamWriter;
45 import org.opendaylight.yangtools.yang.data.codec.xml.XmlParserStream;
46 import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
47 import org.opendaylight.yangtools.yang.data.impl.schema.NormalizedNodeResult;
48 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
49 import org.opendaylight.yangtools.yang.model.api.AugmentationSchemaNode;
50 import org.opendaylight.yangtools.yang.model.api.AugmentationTarget;
51 import org.opendaylight.yangtools.yang.model.api.CaseSchemaNode;
52 import org.opendaylight.yangtools.yang.model.api.ChoiceSchemaNode;
53 import org.opendaylight.yangtools.yang.model.api.ContainerLike;
54 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
55 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
56 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
57 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
58 import org.opendaylight.yangtools.yang.model.api.RpcDefinition;
59 import org.opendaylight.yangtools.yang.model.api.SchemaNode;
60 import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
63 import org.w3c.dom.Document;
64 import org.xml.sax.SAXException;
65
66 @Provider
67 @Consumes({
68     Draft02.MediaTypes.DATA + RestconfService.XML,
69     Draft02.MediaTypes.OPERATION + RestconfService.XML,
70     MediaType.APPLICATION_XML,
71     MediaType.TEXT_XML
72 })
73 public class XmlNormalizedNodeBodyReader extends AbstractIdentifierAwareJaxRsProvider
74         implements MessageBodyReader<NormalizedNodeContext> {
75
76     private static final Logger LOG = LoggerFactory.getLogger(XmlNormalizedNodeBodyReader.class);
77
78     public XmlNormalizedNodeBodyReader(final ControllerContext controllerContext) {
79         super(controllerContext);
80     }
81
82     @Override
83     public boolean isReadable(final Class<?> type, final Type genericType, final Annotation[] annotations,
84             final MediaType mediaType) {
85         return true;
86     }
87
88     @SuppressWarnings("checkstyle:IllegalCatch")
89     @Override
90     public NormalizedNodeContext readFrom(final Class<NormalizedNodeContext> type, final Type genericType,
91             final Annotation[] annotations, final MediaType mediaType,
92             final MultivaluedMap<String, String> httpHeaders, final InputStream entityStream) throws
93         WebApplicationException {
94         try {
95             return readFrom(entityStream);
96         } catch (final RestconfDocumentedException e) {
97             throw e;
98         } catch (final Exception e) {
99             LOG.debug("Error parsing xml input", e);
100             RestconfDocumentedException.throwIfYangError(e);
101             throw new RestconfDocumentedException("Error parsing input: " + e.getMessage(), ErrorType.PROTOCOL,
102                     ErrorTag.MALFORMED_MESSAGE, e);
103         }
104     }
105
106     private NormalizedNodeContext readFrom(final InputStream entityStream) throws IOException, SAXException,
107             XMLStreamException, ParserConfigurationException, URISyntaxException {
108         final InstanceIdentifierContext<?> path = getInstanceIdentifierContext();
109         final Optional<InputStream> nonEmptyInputStreamOptional = RestUtil.isInputStreamEmpty(entityStream);
110         if (nonEmptyInputStreamOptional.isEmpty()) {
111             // represent empty nopayload input
112             return new NormalizedNodeContext(path, null);
113         }
114
115         final Document doc = UntrustedXML.newDocumentBuilder().parse(nonEmptyInputStreamOptional.get());
116         return parse(path, doc);
117     }
118
119     private NormalizedNodeContext parse(final InstanceIdentifierContext<?> pathContext,final Document doc)
120             throws XMLStreamException, IOException, ParserConfigurationException, SAXException, URISyntaxException {
121         final SchemaNode schemaNodeContext = pathContext.getSchemaNode();
122         DataSchemaNode schemaNode;
123         boolean isRpc = false;
124         if (schemaNodeContext instanceof RpcDefinition) {
125             schemaNode = ((RpcDefinition) schemaNodeContext).getInput();
126             isRpc = true;
127         } else if (schemaNodeContext instanceof DataSchemaNode) {
128             schemaNode = (DataSchemaNode) schemaNodeContext;
129         } else {
130             throw new IllegalStateException("Unknown SchemaNode");
131         }
132
133         final String docRootElm = doc.getDocumentElement().getLocalName();
134         final String docRootNamespace = doc.getDocumentElement().getNamespaceURI();
135         final List<YangInstanceIdentifier.PathArgument> iiToDataList = new ArrayList<>();
136
137         if (isPost() && !isRpc) {
138             final Deque<Object> foundSchemaNodes = findPathToSchemaNodeByName(schemaNode, docRootElm, docRootNamespace);
139             if (foundSchemaNodes.isEmpty()) {
140                 throw new IllegalStateException(String.format("Child \"%s\" was not found in parent schema node \"%s\"",
141                         docRootElm, schemaNode.getQName()));
142             }
143             while (!foundSchemaNodes.isEmpty()) {
144                 final Object child = foundSchemaNodes.pop();
145                 if (child instanceof AugmentationSchemaNode) {
146                     final AugmentationSchemaNode augmentSchemaNode = (AugmentationSchemaNode) child;
147                     iiToDataList.add(DataSchemaContextNode.augmentationIdentifierFrom(augmentSchemaNode));
148                 } else if (child instanceof DataSchemaNode) {
149                     schemaNode = (DataSchemaNode) child;
150                     iiToDataList.add(new YangInstanceIdentifier.NodeIdentifier(schemaNode.getQName()));
151                 }
152             }
153         // PUT
154         } else if (!isRpc) {
155             final QName scQName = schemaNode.getQName();
156             Preconditions.checkState(
157                     docRootElm.equals(scQName.getLocalName())
158                             && docRootNamespace.equals(scQName.getNamespace().toString()),
159                     String.format("Not correct message root element \"%s\", should be \"%s\"",
160                             docRootElm, scQName));
161         }
162
163         NormalizedNode parsed;
164         final NormalizedNodeResult resultHolder = new NormalizedNodeResult();
165         final NormalizedNodeStreamWriter writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
166
167         if (schemaNode instanceof ContainerLike || schemaNode instanceof ListSchemaNode
168                 || schemaNode instanceof LeafSchemaNode) {
169             final XmlParserStream xmlParser = XmlParserStream.create(writer, SchemaInferenceStack.ofSchemaPath(
170                 pathContext.getSchemaContext(), schemaNode.getPath()).toInference());
171             xmlParser.traverse(new DOMSource(doc.getDocumentElement()));
172             parsed = resultHolder.getResult();
173
174             // When parsing an XML source with a list root node
175             // the new XML parser always returns a MapNode with one MapEntryNode inside.
176             // However, the old XML parser returned a MapEntryNode directly in this place.
177             // Therefore we now have to extract the MapEntryNode from the parsed MapNode.
178             if (parsed instanceof MapNode) {
179                 final MapNode mapNode = (MapNode) parsed;
180                 // extracting the MapEntryNode
181                 parsed = mapNode.body().iterator().next();
182             }
183
184             if (schemaNode instanceof  ListSchemaNode && isPost()) {
185                 iiToDataList.add(parsed.getIdentifier());
186             }
187         } else {
188             LOG.warn("Unknown schema node extension {} was not parsed", schemaNode.getClass());
189             parsed = null;
190         }
191
192         final YangInstanceIdentifier fullIIToData = YangInstanceIdentifier.create(Iterables.concat(
193                 pathContext.getInstanceIdentifier().getPathArguments(), iiToDataList));
194
195         final InstanceIdentifierContext<? extends SchemaNode> outIIContext = new InstanceIdentifierContext<>(
196                 fullIIToData, pathContext.getSchemaNode(), pathContext.getMountPoint(), pathContext.getSchemaContext());
197
198         return new NormalizedNodeContext(outIIContext, parsed);
199     }
200
201     private static Deque<Object> findPathToSchemaNodeByName(final DataSchemaNode schemaNode, final String elementName,
202                                                             final String namespace) {
203         final Deque<Object> result = new ArrayDeque<>();
204         final ArrayList<ChoiceSchemaNode> choiceSchemaNodes = new ArrayList<>();
205         for (final DataSchemaNode child : ((DataNodeContainer) schemaNode).getChildNodes()) {
206             if (child instanceof ChoiceSchemaNode) {
207                 choiceSchemaNodes.add((ChoiceSchemaNode) child);
208             } else if (child.getQName().getLocalName().equalsIgnoreCase(elementName)
209                     && child.getQName().getNamespace().toString().equalsIgnoreCase(namespace)) {
210                 // add child to result
211                 result.push(child);
212
213                 // find augmentation
214                 if (child.isAugmenting()) {
215                     final AugmentationSchemaNode augment = findCorrespondingAugment(schemaNode, child);
216                     if (augment != null) {
217                         result.push(augment);
218                     }
219                 }
220
221                 // return result
222                 return result;
223             }
224         }
225
226         for (final ChoiceSchemaNode choiceNode : choiceSchemaNodes) {
227             for (final CaseSchemaNode caseNode : choiceNode.getCases()) {
228                 final Deque<Object> resultFromRecursion = findPathToSchemaNodeByName(caseNode, elementName, namespace);
229                 if (!resultFromRecursion.isEmpty()) {
230                     resultFromRecursion.push(choiceNode);
231                     if (choiceNode.isAugmenting()) {
232                         final AugmentationSchemaNode augment = findCorrespondingAugment(schemaNode, choiceNode);
233                         if (augment != null) {
234                             resultFromRecursion.push(augment);
235                         }
236                     }
237                     return resultFromRecursion;
238                 }
239             }
240         }
241         return result;
242     }
243
244     private static AugmentationSchemaNode findCorrespondingAugment(final DataSchemaNode parent,
245                                                                    final DataSchemaNode child) {
246         if (parent instanceof AugmentationTarget && !(parent instanceof ChoiceSchemaNode)) {
247             for (AugmentationSchemaNode augmentation : ((AugmentationTarget) parent).getAvailableAugmentations()) {
248                 if (augmentation.dataChildByName(child.getQName()) != null) {
249                     return augmentation;
250                 }
251             }
252         }
253         return null;
254     }
255 }
256