Merge "BUG 1082 Migrate sal-rest-connector to Async Data Broker API"
[controller.git] / opendaylight / md-sal / sal-rest-connector / src / main / java / org / opendaylight / controller / sal / rest / impl / XmlToNormalizedNodeReaderWithSchema.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.controller.sal.rest.impl;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import java.io.BufferedInputStream;
13 import java.io.IOException;
14 import java.io.InputStream;
15 import java.net.URI;
16 import java.util.HashMap;
17 import java.util.Map;
18 import java.util.Stack;
19 import javax.xml.stream.XMLEventReader;
20 import javax.xml.stream.XMLInputFactory;
21 import javax.xml.stream.XMLStreamConstants;
22 import javax.xml.stream.XMLStreamException;
23 import javax.xml.stream.events.Characters;
24 import javax.xml.stream.events.StartElement;
25 import javax.xml.stream.events.XMLEvent;
26 import org.opendaylight.controller.sal.restconf.impl.CompositeNodeWrapper;
27 import org.opendaylight.controller.sal.restconf.impl.IdentityValuesDTO;
28 import org.opendaylight.controller.sal.restconf.impl.InstanceIdWithSchemaNode;
29 import org.opendaylight.controller.sal.restconf.impl.NodeWrapper;
30 import org.opendaylight.controller.sal.restconf.impl.RestCodec;
31 import org.opendaylight.controller.sal.restconf.impl.RestconfDocumentedException;
32 import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorTag;
33 import org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType;
34 import org.opendaylight.controller.sal.restconf.impl.SimpleNodeWrapper;
35 import org.opendaylight.yangtools.yang.common.QName;
36 import org.opendaylight.yangtools.yang.data.api.Node;
37 import org.opendaylight.yangtools.yang.model.api.ContainerSchemaNode;
38 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
39 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
40 import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
41 import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
42 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
43 import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
44
45 public class XmlToNormalizedNodeReaderWithSchema {
46
47     private final static XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
48     private XMLEventReader eventReader;
49     private InstanceIdWithSchemaNode iiWithSchema;
50
51     public XmlToNormalizedNodeReaderWithSchema(final InstanceIdWithSchemaNode iiWithSchema) {
52         this.iiWithSchema = iiWithSchema;
53     }
54
55     public Node<?> read(InputStream entityStream) throws XMLStreamException, UnsupportedFormatException, IOException {
56         // Get an XML stream which can be marked, and reset, so we can check and see if there is
57         // any content being provided.
58         entityStream = getMarkableStream(entityStream);
59
60         if (isInputStreamEmpty(entityStream)) {
61             return null;
62         }
63
64         eventReader = xmlInputFactory.createXMLEventReader(entityStream);
65         if (eventReader.hasNext()) {
66             XMLEvent element = eventReader.peek();
67             if (element.isStartDocument()) {
68                 eventReader.nextEvent();
69             }
70         }
71
72         final Stack<NodeWrapper<?>> processingQueue = new Stack<>();
73         NodeWrapper<?> root = null;
74         NodeWrapper<?> element = null;
75         Stack<DataSchemaNode> processingQueueSchema = new Stack<>();
76
77         while (eventReader.hasNext()) {
78             final XMLEvent event = eventReader.nextEvent();
79
80             if (event.isStartElement()) {
81                 final StartElement startElement = event.asStartElement();
82                 CompositeNodeWrapper compParentNode = null;
83                 if (!processingQueue.isEmpty() && processingQueue.peek() instanceof CompositeNodeWrapper) {
84                     compParentNode = (CompositeNodeWrapper) processingQueue.peek();
85                     findSchemaNodeForElement(startElement, processingQueueSchema);
86                 } else {
87                     processingQueueSchema = checkElementAndSchemaNodeNameAndNamespace(startElement,
88                             iiWithSchema.getSchemaNode());
89                     DataSchemaNode currentSchemaNode = processingQueueSchema.peek();
90                     if (!(currentSchemaNode instanceof ListSchemaNode)
91                             && !(currentSchemaNode instanceof ContainerSchemaNode)) {
92                         throw new UnsupportedFormatException(
93                                 "Top level element has to be of type list or container schema node.");
94                     }
95                 }
96
97                 NodeWrapper<?> newNode = null;
98                 if (isCompositeNodeEvent(event)) {
99                     newNode = resolveCompositeNodeFromStartElement(processingQueueSchema.peek().getQName());
100                     if (root == null) {
101                         root = newNode;
102                     }
103                 } else if (isSimpleNodeEvent(event)) {
104                     newNode = resolveSimpleNodeFromStartElement(processingQueueSchema.peek(), getValueOf(startElement));
105                     if (root == null) {
106                         root = newNode;
107                     }
108                 }
109
110                 if (newNode != null) {
111                     processingQueue.push(newNode);
112                     if (compParentNode != null) {
113                         compParentNode.addValue(newNode);
114                     }
115                 }
116             } else if (event.isEndElement()) {
117                 element = processingQueue.pop();
118 //                if(((EndElement)event).getName().getLocalPart().equals
119                 processingQueueSchema.pop();
120             }
121         }
122
123         if (!root.getLocalName().equals(element.getLocalName())) {
124             throw new UnsupportedFormatException("XML should contain only one root element");
125         }
126
127         return root.unwrap();
128     }
129
130     private void findSchemaNodeForElement(StartElement element, Stack<DataSchemaNode> processingQueueSchema) {
131         DataSchemaNode currentSchemaNode = processingQueueSchema.peek();
132         if (currentSchemaNode instanceof DataNodeContainer) {
133             final URI realNamespace = getNamespaceFor(element);
134             final String realName = getLocalNameFor(element);
135             Map<URI, DataSchemaNode> childNamesakes = resolveChildsWithNameAsElement(
136                     ((DataNodeContainer) currentSchemaNode), realName);
137             DataSchemaNode childDataSchemaNode = childNamesakes.get(realNamespace);
138             if (childDataSchemaNode == null) {
139                 throw new RestconfDocumentedException("Element " + realName + " has namespace " + realNamespace
140                         + ". Available namespaces are: " + childNamesakes.keySet(), ErrorType.APPLICATION,
141                         ErrorTag.INVALID_VALUE);
142             }
143             processingQueueSchema.push(childDataSchemaNode);
144         } else {
145             throw new RestconfDocumentedException("Element " + processingQueueSchema.peek().getQName().getLocalName()
146                     + " should be data node container .", ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
147         }
148
149     }
150
151     /**
152      * Returns map of data schema node which are accesible by URI which have equal name
153      */
154     private Map<URI, DataSchemaNode> resolveChildsWithNameAsElement(final DataNodeContainer dataNodeContainer,
155             final String realName) {
156         final Map<URI, DataSchemaNode> namespaceToDataSchemaNode = new HashMap<URI, DataSchemaNode>();
157         for (DataSchemaNode dataSchemaNode : dataNodeContainer.getChildNodes()) {
158             if (dataSchemaNode.equals(realName)) {
159                 namespaceToDataSchemaNode.put(dataSchemaNode.getQName().getNamespace(), dataSchemaNode);
160             }
161         }
162         return namespaceToDataSchemaNode;
163     }
164
165     private final Stack<DataSchemaNode> checkElementAndSchemaNodeNameAndNamespace(final StartElement startElement,
166             final DataSchemaNode node) {
167         checkArgument(startElement != null, "Start Element cannot be NULL!");
168         final String expectedName = node.getQName().getLocalName();
169         final String xmlName = getLocalNameFor(startElement);
170         final URI expectedNamespace = node.getQName().getNamespace();
171         final URI xmlNamespace = getNamespaceFor(startElement);
172         if (!expectedName.equals(xmlName)) {
173             throw new RestconfDocumentedException("Xml element name: " + xmlName + "\nSchema node name: "
174                     + expectedName, org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType.APPLICATION,
175                     ErrorTag.INVALID_VALUE);
176         }
177
178         if (xmlNamespace != null && !expectedNamespace.equals(xmlNamespace)) {
179             throw new RestconfDocumentedException("Xml element ns: " + xmlNamespace + "\nSchema node ns: "
180                     + expectedNamespace,
181                     org.opendaylight.controller.sal.restconf.impl.RestconfError.ErrorType.APPLICATION,
182                     ErrorTag.INVALID_VALUE);
183         }
184         Stack<DataSchemaNode> processingQueueSchema = new Stack<>();
185         processingQueueSchema.push(node);
186         return processingQueueSchema;
187     }
188
189     /**
190      * If the input stream is not markable, then it wraps the input stream with a buffered stream, which is mark able.
191      * That way we can check if the stream is empty safely.
192      *
193      * @param entityStream
194      * @return
195      */
196     private InputStream getMarkableStream(InputStream entityStream) {
197         if (!entityStream.markSupported()) {
198             entityStream = new BufferedInputStream(entityStream);
199         }
200         return entityStream;
201     }
202
203     private boolean isInputStreamEmpty(final InputStream entityStream) throws IOException {
204         boolean isEmpty = false;
205         entityStream.mark(1);
206         if (entityStream.read() == -1) {
207             isEmpty = true;
208         }
209         entityStream.reset();
210         return isEmpty;
211     }
212
213     private boolean isSimpleNodeEvent(final XMLEvent event) throws XMLStreamException {
214         checkArgument(event != null, "XML Event cannot be NULL!");
215         if (event.isStartElement()) {
216             XMLEvent innerEvent = skipCommentsAndWhitespace();
217             if (innerEvent != null && (innerEvent.isCharacters() || innerEvent.isEndElement())) {
218                 return true;
219             }
220         }
221         return false;
222     }
223
224     private boolean isCompositeNodeEvent(final XMLEvent event) throws XMLStreamException {
225         checkArgument(event != null, "XML Event cannot be NULL!");
226         if (event.isStartElement()) {
227             XMLEvent innerEvent = skipCommentsAndWhitespace();
228             if (innerEvent != null) {
229                 if (innerEvent.isStartElement()) {
230                     return true;
231                 }
232             }
233         }
234         return false;
235     }
236
237     private XMLEvent skipCommentsAndWhitespace() throws XMLStreamException {
238         while (eventReader.hasNext()) {
239             XMLEvent event = eventReader.peek();
240             if (event.getEventType() == XMLStreamConstants.COMMENT) {
241                 eventReader.nextEvent();
242                 continue;
243             }
244
245             if (event.isCharacters()) {
246                 Characters chars = event.asCharacters();
247                 if (chars.isWhiteSpace()) {
248                     eventReader.nextEvent();
249                     continue;
250                 }
251             }
252             return event;
253         }
254         return null;
255     }
256
257     private CompositeNodeWrapper resolveCompositeNodeFromStartElement(final QName qName) {
258         // checkArgument(startElement != null, "Start Element cannot be NULL!");
259         CompositeNodeWrapper compositeNodeWrapper = new CompositeNodeWrapper("dummy");
260         compositeNodeWrapper.setQname(qName);
261         return compositeNodeWrapper;
262
263     }
264
265     private SimpleNodeWrapper resolveSimpleNodeFromStartElement(final DataSchemaNode node, final String value)
266             throws XMLStreamException {
267         // checkArgument(startElement != null, "Start Element cannot be NULL!");
268         Object deserializedValue = null;
269
270         if (node instanceof LeafSchemaNode) {
271             TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(((LeafSchemaNode) node).getType());
272             deserializedValue = RestCodec.from(baseType, iiWithSchema.getMountPoint()).deserialize(value);
273         } else if (node instanceof LeafListSchemaNode) {
274             TypeDefinition<?> baseType = RestUtil.resolveBaseTypeFrom(((LeafListSchemaNode) node).getType());
275             deserializedValue = RestCodec.from(baseType, iiWithSchema.getMountPoint()).deserialize(value);
276         }
277         // String data;
278         // if (data == null) {
279         // return new EmptyNodeWrapper(getNamespaceFor(startElement), getLocalNameFor(startElement));
280         // }
281         SimpleNodeWrapper simpleNodeWrapper = new SimpleNodeWrapper("dummy", deserializedValue);
282         simpleNodeWrapper.setQname(node.getQName());
283         return simpleNodeWrapper;
284     }
285
286     private String getValueOf(final StartElement startElement) throws XMLStreamException {
287         String data = null;
288         if (eventReader.hasNext()) {
289             final XMLEvent innerEvent = eventReader.peek();
290             if (innerEvent.isCharacters()) {
291                 final Characters chars = innerEvent.asCharacters();
292                 if (!chars.isWhiteSpace()) {
293                     data = innerEvent.asCharacters().getData();
294                     data = data + getAdditionalData(eventReader.nextEvent());
295                 }
296             } else if (innerEvent.isEndElement()) {
297                 if (startElement.getLocation().getCharacterOffset() == innerEvent.getLocation().getCharacterOffset()) {
298                     data = null;
299                 } else {
300                     data = "";
301                 }
302             }
303         }
304         return data == null ? null : data.trim();
305     }
306
307     private String getAdditionalData(final XMLEvent event) throws XMLStreamException {
308         String data = "";
309         if (eventReader.hasNext()) {
310             final XMLEvent innerEvent = eventReader.peek();
311             if (innerEvent.isCharacters() && !innerEvent.isEndElement()) {
312                 final Characters chars = innerEvent.asCharacters();
313                 if (!chars.isWhiteSpace()) {
314                     data = innerEvent.asCharacters().getData();
315                     data = data + getAdditionalData(eventReader.nextEvent());
316                 }
317             }
318         }
319         return data;
320     }
321
322     private String getLocalNameFor(final StartElement startElement) {
323         return startElement.getName().getLocalPart();
324     }
325
326     private URI getNamespaceFor(final StartElement startElement) {
327         String namespaceURI = startElement.getName().getNamespaceURI();
328         return namespaceURI.isEmpty() ? null : URI.create(namespaceURI);
329     }
330
331     private Object resolveValueOfElement(final String value, final StartElement startElement) {
332         // it could be instance-identifier Built-In Type
333         if (value.startsWith("/")) {
334             IdentityValuesDTO iiValue = RestUtil.asInstanceIdentifier(value, new RestUtil.PrefixMapingFromXml(
335                     startElement));
336             if (iiValue != null) {
337                 return iiValue;
338             }
339         }
340         // it could be identityref Built-In Type
341         String[] namespaceAndValue = value.split(":");
342         if (namespaceAndValue.length == 2) {
343             String namespace = startElement.getNamespaceContext().getNamespaceURI(namespaceAndValue[0]);
344             if (namespace != null && !namespace.isEmpty()) {
345                 return new IdentityValuesDTO(namespace, namespaceAndValue[1], namespaceAndValue[0], value);
346             }
347         }
348         // it is not "prefix:value" but just "value"
349         return value;
350     }
351
352 }