View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.internal.xml;
20  
21  import javax.xml.stream.XMLStreamException;
22  import javax.xml.stream.XMLStreamReader;
23  
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.Reader;
27  import java.util.ArrayList;
28  import java.util.HashMap;
29  import java.util.List;
30  import java.util.Map;
31  
32  import com.ctc.wstx.stax.WstxInputFactory;
33  import org.apache.maven.api.xml.XmlNode;
34  import org.codehaus.plexus.util.xml.pull.MXParser;
35  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
36  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
37  
38  /**
39   * All methods in this class attempt to fully parse the XML.
40   * The caller is responsible for closing {@code InputStream} and {@code Reader} arguments.
41   */
42  public class XmlNodeBuilder {
43      private static final boolean DEFAULT_TRIM = true;
44  
45      public static XmlNodeImpl build(Reader reader) throws XmlPullParserException, IOException {
46          return build(reader, (InputLocationBuilder) null);
47      }
48  
49      /**
50       * @param reader the reader
51       * @param locationBuilder the builder
52       * @since 3.2.0
53       * @return DOM
54       * @throws XmlPullParserException xml exception
55       * @throws IOException io
56       */
57      public static XmlNodeImpl build(Reader reader, InputLocationBuilder locationBuilder)
58              throws XmlPullParserException, IOException {
59          return build(reader, DEFAULT_TRIM, locationBuilder);
60      }
61  
62      public static XmlNodeImpl build(InputStream is, String encoding) throws XmlPullParserException, IOException {
63          return build(is, encoding, DEFAULT_TRIM);
64      }
65  
66      public static XmlNodeImpl build(InputStream is, String encoding, boolean trim)
67              throws XmlPullParserException, IOException {
68          XmlPullParser parser = new MXParser();
69          parser.setInput(is, encoding);
70          return build(parser, trim);
71      }
72  
73      public static XmlNodeImpl build(Reader reader, boolean trim) throws XmlPullParserException, IOException {
74          return build(reader, trim, null);
75      }
76  
77      /**
78       * @param reader the reader
79       * @param trim to trim
80       * @param locationBuilder the builder
81       * @since 3.2.0
82       * @return DOM
83       * @throws XmlPullParserException xml exception
84       * @throws IOException io
85       */
86      public static XmlNodeImpl build(Reader reader, boolean trim, InputLocationBuilder locationBuilder)
87              throws XmlPullParserException, IOException {
88          XmlPullParser parser = new MXParser();
89          parser.setInput(reader);
90          return build(parser, trim, locationBuilder);
91      }
92  
93      public static XmlNodeImpl build(XmlPullParser parser) throws XmlPullParserException, IOException {
94          return build(parser, DEFAULT_TRIM);
95      }
96  
97      public static XmlNodeImpl build(XmlPullParser parser, boolean trim) throws XmlPullParserException, IOException {
98          return build(parser, trim, null);
99      }
100 
101     /**
102      * @since 3.2.0
103      * @param locationBuilder builder
104      * @param parser the parser
105      * @param trim do trim
106      * @return DOM
107      * @throws XmlPullParserException xml exception
108      * @throws IOException io
109      */
110     public static XmlNodeImpl build(XmlPullParser parser, boolean trim, InputLocationBuilder locationBuilder)
111             throws XmlPullParserException, IOException {
112         boolean spacePreserve = false;
113         String name = null;
114         String value = null;
115         Object location = null;
116         Map<String, String> attrs = null;
117         List<XmlNode> children = null;
118         int eventType = parser.getEventType();
119         boolean emptyTag = false;
120         while (eventType != XmlPullParser.END_DOCUMENT) {
121             if (eventType == XmlPullParser.START_TAG) {
122                 emptyTag = parser.isEmptyElementTag();
123                 if (name == null) {
124                     name = parser.getName();
125                     location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null;
126                     int attributesSize = parser.getAttributeCount();
127                     if (attributesSize > 0) {
128                         attrs = new HashMap<>();
129                         for (int i = 0; i < attributesSize; i++) {
130                             String aname = parser.getAttributeName(i);
131                             String avalue = parser.getAttributeValue(i);
132                             attrs.put(aname, avalue);
133                             spacePreserve = spacePreserve || ("xml:space".equals(aname) && "preserve".equals(avalue));
134                         }
135                     }
136                 } else {
137                     if (children == null) {
138                         children = new ArrayList<>();
139                     }
140                     XmlNode child = build(parser, trim, locationBuilder);
141                     children.add(child);
142                 }
143             } else if (eventType == XmlPullParser.TEXT) {
144                 String text = parser.getText();
145                 if (trim && !spacePreserve) {
146                     text = text.trim();
147                 }
148                 value = value != null ? value + text : text;
149             } else if (eventType == XmlPullParser.END_TAG) {
150                 return new XmlNodeImpl(
151                         name,
152                         children == null ? (value != null ? value : emptyTag ? null : "") : null,
153                         attrs,
154                         children,
155                         location);
156             }
157             eventType = parser.next();
158         }
159         throw new IllegalStateException("End of document found before returning to 0 depth");
160     }
161 
162     public static XmlNodeImpl build(Reader reader, InputLocationBuilderStax locationBuilder) throws XMLStreamException {
163         XMLStreamReader parser = WstxInputFactory.newFactory().createXMLStreamReader(reader);
164         return build(parser, DEFAULT_TRIM, locationBuilder);
165     }
166 
167     public static XmlNodeImpl build(XMLStreamReader parser) throws XMLStreamException {
168         return build(parser, DEFAULT_TRIM, null);
169     }
170 
171     public static XmlNodeImpl build(XMLStreamReader parser, InputLocationBuilderStax locationBuilder)
172             throws XMLStreamException {
173         return build(parser, DEFAULT_TRIM, locationBuilder);
174     }
175 
176     public static XmlNodeImpl build(XMLStreamReader parser, boolean trim, InputLocationBuilderStax locationBuilder)
177             throws XMLStreamException {
178         boolean spacePreserve = false;
179         String name = null;
180         String value = null;
181         Object location = null;
182         Map<String, String> attrs = null;
183         List<XmlNode> children = null;
184         int eventType = parser.getEventType();
185         int lastStartTag = -1;
186         while (eventType != XMLStreamReader.END_DOCUMENT) {
187             if (eventType == XMLStreamReader.START_ELEMENT) {
188                 lastStartTag = parser.getLocation().getLineNumber() * 1000
189                         + parser.getLocation().getColumnNumber();
190                 if (name == null) {
191                     int namespacesSize = parser.getNamespaceCount();
192                     name = parser.getLocalName();
193                     String pfx = parser.getPrefix();
194                     if (pfx != null && !pfx.isEmpty()) {
195                         name = pfx + ":" + name;
196                     }
197                     location = locationBuilder != null ? locationBuilder.toInputLocation(parser) : null;
198                     int attributesSize = parser.getAttributeCount();
199                     if (attributesSize > 0 || namespacesSize > 0) {
200                         attrs = new HashMap<>();
201                         for (int i = 0; i < namespacesSize; i++) {
202                             String prefix = parser.getNamespacePrefix(i);
203                             String namespace = parser.getNamespaceURI(i);
204                             attrs.put("xmlns:" + prefix, namespace);
205                         }
206                         for (int i = 0; i < attributesSize; i++) {
207                             String aname = parser.getAttributeLocalName(i);
208                             String avalue = parser.getAttributeValue(i);
209                             String apfx = parser.getAttributePrefix(i);
210                             if (apfx != null && !apfx.isEmpty()) {
211                                 aname = apfx + ":" + aname;
212                             }
213                             attrs.put(aname, avalue);
214                             spacePreserve = spacePreserve || ("xml:space".equals(aname) && "preserve".equals(avalue));
215                         }
216                     }
217                 } else {
218                     if (children == null) {
219                         children = new ArrayList<>();
220                     }
221                     XmlNode child = build(parser, trim, locationBuilder);
222                     children.add(child);
223                 }
224             } else if (eventType == XMLStreamReader.CHARACTERS || eventType == XMLStreamReader.CDATA) {
225                 String text = parser.getText();
226                 value = value != null ? value + text : text;
227             } else if (eventType == XMLStreamReader.END_ELEMENT) {
228                 boolean emptyTag = lastStartTag
229                         == parser.getLocation().getLineNumber() * 1000
230                                 + parser.getLocation().getColumnNumber();
231                 if (value != null && trim && !spacePreserve) {
232                     value = value.trim();
233                 }
234                 return new XmlNodeImpl(
235                         name,
236                         children == null ? (value != null ? value : emptyTag ? null : "") : null,
237                         attrs,
238                         children,
239                         location);
240             }
241             eventType = parser.next();
242         }
243         throw new IllegalStateException("End of document found before returning to 0 depth");
244     }
245 
246     /**
247      * Input location builder interface, to be implemented to choose how to store data.
248      *
249      * @since 3.2.0
250      */
251     public interface InputLocationBuilder {
252         Object toInputLocation(XmlPullParser parser);
253     }
254 
255     public interface InputLocationBuilderStax {
256         Object toInputLocation(XMLStreamReader parser);
257     }
258 }