Added XmlTreeBuilder into yang-data-impl
[yangtools.git] / yang / yang-data-impl / src / main / java / org / opendaylight / yangtools / yang / data / impl / XmlTreeBuilder.java
1 /*
2  * Copyright (c) 2013 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.yangtools.yang.data.impl;
9
10 import static com.google.common.base.Preconditions.checkArgument;
11
12 import java.io.InputStream;
13 import java.net.URI;
14 import java.util.ArrayList;
15 import java.util.Stack;
16
17 import javax.xml.stream.XMLEventReader;
18 import javax.xml.stream.XMLInputFactory;
19 import javax.xml.stream.XMLStreamException;
20 import javax.xml.stream.events.Characters;
21 import javax.xml.stream.events.StartElement;
22 import javax.xml.stream.events.XMLEvent;
23
24 import org.opendaylight.yangtools.yang.common.QName;
25 import org.opendaylight.yangtools.yang.data.api.CompositeNode;
26 import org.opendaylight.yangtools.yang.data.api.ModifyAction;
27 import org.opendaylight.yangtools.yang.data.api.MutableCompositeNode;
28 import org.opendaylight.yangtools.yang.data.api.Node;
29 import org.opendaylight.yangtools.yang.data.api.SimpleNode;
30
31 /**
32  * The XML Tree Builder is builder utility class designed to facilitate of
33  * loading, reading and parsing XML documents into YANG DOM represented by
34  * yang-data-api. <br>
35  * The method {@link #buildDataTree(InputStream)} is designed to take and parse
36  * XML as {@link InputStream}. The output of the operation SHOULD be root
37  * <code>CompositeNode</code> or <code>SimpleElement</code> depends by which
38  * element XML begins. The XML header is omitted by XML parser.
39  * 
40  * @author Lukas Sedlak
41  * 
42  * @see CompositeNode
43  * @see SimpleNode
44  * @see Node
45  */
46 public final class XmlTreeBuilder {
47
48         private final static XMLInputFactory xmlInputFactory = XMLInputFactory
49                         .newInstance();
50         private static XMLEventReader eventReader;
51
52         private XmlTreeBuilder() {
53         }
54
55         /**
56          * The method is designed to take and parse XML as {@link InputStream}. The
57          * output of the operation SHOULD be root <code>CompositeNode</code> or
58          * <code>SimpleElement</code> depends on element that XML document begins.
59          * The XML header is omitted by XML parser.
60          * 
61          * @param inputStream
62          *            XML Input Stream
63          * @return root <code>Node</code> element conformant to XML start element in
64          *         most cases it will be CompositeNode which contains child Nodes
65          * @throws XMLStreamException
66          */
67         public static Node<?> buildDataTree(final InputStream inputStream)
68                         throws XMLStreamException {
69                 eventReader = xmlInputFactory.createXMLEventReader(inputStream);
70
71                 final Stack<Node<?>> processingQueue = new Stack<>();
72                 Node<?> parentNode = null;
73                 Node<?> root = null;
74                 while (eventReader.hasNext()) {
75                         final XMLEvent event = eventReader.nextEvent();
76
77                         if (event.isStartElement()) {
78                                 final StartElement startElement = event.asStartElement();
79                                 if (!processingQueue.isEmpty()) {
80                                         parentNode = processingQueue.peek();
81                                 }
82                                 CompositeNode compParentNode = null;
83                                 if (parentNode instanceof CompositeNode) {
84                                         compParentNode = (CompositeNode) parentNode;
85                                 }
86                                 Node<?> newNode = null;
87                                 if (isCompositeNodeEvent(event)) {
88                                         newNode = resolveCompositeNodeFromStartElement(
89                                                         startElement, compParentNode);
90                                 } else if (isSimpleNodeEvent(event)) {
91                                         newNode = resolveSimpleNodeFromStartElement(startElement,
92                                                         compParentNode);
93                                 }
94
95                                 if (newNode != null) {
96                                         processingQueue.push(newNode);
97                                         if (compParentNode != null) {
98                                                 compParentNode.getChildren().add(newNode);
99                                         }
100                                 }
101                         } else if (event.isEndElement()) {
102                                 root = processingQueue.pop();
103                         }
104                 }
105                 return root;
106         }
107
108         /**
109          * Checks if the XMLEvent is compliant to SimpleNode tag that contains only
110          * characters value. If the SimpleNode is composed only by empty XML tag
111          * (i.e. {@code <emptyTag />} or {@code<emptyTag></emptyTag>}) the result
112          * will be also <code>true</code>.
113          * 
114          * @param event
115          *            actual XMLEvent that is processed
116          * @return <code>true</code> only and only if the XMLEvent Start Element is
117          *         Simple element tag and contains character values or is empty XML
118          *         tag.
119          * 
120          * @see SimpleNode
121          */
122         private static boolean isSimpleNodeEvent(final XMLEvent event) {
123                 checkArgument(event != null, "XML Event cannot be NULL!");
124                 if (event.isStartElement()) {
125                         if (eventReader.hasNext()) {
126                                 final XMLEvent innerEvent;
127                                 try {
128                                         innerEvent = eventReader.peek();
129                                         if (innerEvent.isCharacters()) {
130                                                 final Characters chars = innerEvent.asCharacters();
131                                                 if (!chars.isWhiteSpace()) {
132                                                         return true;
133                                                 }
134                                         } else if (innerEvent.isEndElement()) {
135                                                 return true;
136                                         }
137                                 } catch (XMLStreamException e) {
138                                         e.printStackTrace();
139                                 }
140                         }
141                 }
142                 return false;
143         }
144
145         /**
146          * Checks if XMLEvent is equivalent to CompositeNode Event. The
147          * CompositeNode Event is XML element that conforms to the XML element that
148          * contains 1..N XML child elements. (i.e. {@code <compositeNode>
149          *      <simpleNode>data</simpleNode>
150          * </compositeNode>})
151          * 
152          * @param event
153          *            actual XMLEvent that is processed
154          * @return <code>true</code> only if XML Element contains 1..N child
155          *         elements, otherwise returns <code>false</code>
156          * 
157          * @see CompositeNode
158          */
159         private static boolean isCompositeNodeEvent(final XMLEvent event) {
160                 checkArgument(event != null, "XML Event cannot be NULL!");
161                 if (event.isStartElement()) {
162                         if (eventReader.hasNext()) {
163                                 XMLEvent innerEvent;
164                                 try {
165                                         innerEvent = eventReader.peek();
166                                         if (innerEvent.isCharacters()) {
167                                                 Characters chars = innerEvent.asCharacters();
168                                                 if (chars.isWhiteSpace()) {
169                                                         eventReader.nextEvent();
170                                                         innerEvent = eventReader.peek();
171                                                 }
172                                         }
173                                         if (innerEvent.isStartElement()) {
174                                                 return true;
175                                         }
176                                 } catch (XMLStreamException e) {
177                                         e.printStackTrace();
178                                 }
179                         }
180                 }
181                 return false;
182         }
183
184         /**
185          * Creates and returns <code>SimpleNode</code> instance from actually
186          * processed XML Start Element.
187          * 
188          * @param startElement
189          *            actual XML Start Element that is processed
190          * @param parent
191          *            Parent CompositeNode
192          * @return <code>new SimpleNode</code> instance from actually processed XML
193          *         Start Element
194          * @throws XMLStreamException
195          * 
196          * @see SimpleNode
197          */
198         private static SimpleNode<String> resolveSimpleNodeFromStartElement(
199                         final StartElement startElement, CompositeNode parent)
200                         throws XMLStreamException {
201                 checkArgument(startElement != null, "Start Element cannot be NULL!");
202                 String data = null;
203
204                 if (eventReader.hasNext()) {
205                         final XMLEvent innerEvent = eventReader.peek();
206                         if (innerEvent.isCharacters()) {
207                                 final Characters chars = innerEvent.asCharacters();
208                                 if (!chars.isWhiteSpace()) {
209                                         data = innerEvent.asCharacters().getData();
210                                 }
211                         } else if (innerEvent.isEndElement()) {
212                                 data = "";
213                         }
214                 }
215                 return NodeFactory.createImmutableSimpleNode(
216                                 resolveElementQName(startElement), parent, data);
217         }
218
219         /**
220          * Creates and returns <code>MutableCompositeNode</code> instance from
221          * actually processed XML Start Element.
222          * 
223          * @param startElement
224          *            actual XML Start Element that is processed
225          * @param parent
226          *            Parent CompositeNode
227          * @return <code>new MutableCompositeNode</code> instance from actually
228          *         processed XML Start Element
229          * 
230          * @see CompositeNode
231          * @see MutableCompositeNode
232          */
233         private static CompositeNode resolveCompositeNodeFromStartElement(
234                         final StartElement startElement, CompositeNode parent) {
235                 checkArgument(startElement != null, "Start Element cannot be NULL!");
236
237                 return NodeFactory.createMutableCompositeNode(
238                                 resolveElementQName(startElement), parent,
239                                 new ArrayList<Node<?>>(), ModifyAction.CREATE, null);
240         }
241
242         /**
243          * Extract and retrieve XML Element QName to OpenDaylight QName.
244          * 
245          * @param element
246          *            Start Element
247          * @return QName instance composed of <code>elements</code> Namespace and
248          *         Local Part.
249          * 
250          * @see QName
251          */
252         private static QName resolveElementQName(final StartElement element) {
253                 checkArgument(element != null, "Start Element cannot be NULL!");
254
255                 final String nsURI = element.getName().getNamespaceURI();
256                 final String localName = element.getName().getLocalPart();
257                 return new QName(URI.create(nsURI), localName);
258         }
259 }