1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.util;
16
17 import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
18
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.InputStreamReader;
22 import java.io.Reader;
23 import java.io.StringReader;
24 import java.nio.charset.Charset;
25 import java.util.Locale;
26
27 import javax.xml.parsers.DocumentBuilder;
28 import javax.xml.parsers.DocumentBuilderFactory;
29 import javax.xml.parsers.ParserConfigurationException;
30
31 import org.apache.commons.io.input.BOMInputStream;
32 import org.apache.commons.logging.Log;
33 import org.apache.commons.logging.LogFactory;
34 import org.htmlunit.SgmlPage;
35 import org.htmlunit.WebResponse;
36 import org.htmlunit.html.DomAttr;
37 import org.htmlunit.html.DomCDataSection;
38 import org.htmlunit.html.DomComment;
39 import org.htmlunit.html.DomDocumentType;
40 import org.htmlunit.html.DomElement;
41 import org.htmlunit.html.DomNode;
42 import org.htmlunit.html.DomProcessingInstruction;
43 import org.htmlunit.html.DomText;
44 import org.htmlunit.html.ElementFactory;
45 import org.htmlunit.html.Html;
46 import org.htmlunit.xml.XmlPage;
47 import org.w3c.dom.Attr;
48 import org.w3c.dom.Document;
49 import org.w3c.dom.DocumentType;
50 import org.w3c.dom.NamedNodeMap;
51 import org.w3c.dom.Node;
52 import org.w3c.dom.NodeList;
53 import org.xml.sax.Attributes;
54 import org.xml.sax.ErrorHandler;
55 import org.xml.sax.InputSource;
56 import org.xml.sax.SAXException;
57 import org.xml.sax.SAXParseException;
58 import org.xml.sax.helpers.AttributesImpl;
59
60
61
62
63
64
65
66
67
68
69
70
71
72 public final class XmlUtils {
73
74 private static final Log LOG = LogFactory.getLog(XmlUtils.class);
75
76 private static final ErrorHandler DISCARD_MESSAGES_HANDLER = new ErrorHandler() {
77
78
79
80 @Override
81 public void error(final SAXParseException exception) {
82
83 }
84
85
86
87
88 @Override
89 public void fatalError(final SAXParseException exception) {
90
91 }
92
93
94
95
96 @Override
97 public void warning(final SAXParseException exception) {
98
99 }
100 };
101
102
103
104
105 private XmlUtils() {
106
107 }
108
109
110
111
112
113
114
115
116
117
118
119
120 public static Document buildDocument(final WebResponse webResponse)
121 throws IOException, SAXException, ParserConfigurationException {
122
123 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
124
125 if (webResponse == null) {
126 return factory.newDocumentBuilder().newDocument();
127 }
128
129 factory.setNamespaceAware(true);
130
131 Charset charset = webResponse.getContentCharset();
132 try (InputStream is = webResponse.getContentAsStreamWithBomIfApplicable()) {
133 if (is instanceof BOMInputStream stream) {
134 final String bomCharsetName = stream.getBOMCharsetName();
135 if (bomCharsetName != null) {
136 charset = Charset.forName(bomCharsetName);
137 }
138 }
139
140 try (InputStreamReader reader = new InputStreamReader(is, charset)) {
141
142 final TrackBlankContentAndSkipLeadingWhitespaceReader tracker
143 = new TrackBlankContentAndSkipLeadingWhitespaceReader(reader);
144
145 final InputSource source = new InputSource(tracker);
146 final DocumentBuilder builder = factory.newDocumentBuilder();
147 builder.setErrorHandler(DISCARD_MESSAGES_HANDLER);
148 builder.setEntityResolver((publicId, systemId) -> new InputSource(new StringReader("")));
149 try {
150
151 return builder.parse(source);
152 }
153 catch (final SAXException e) {
154 if (tracker.wasBlank()) {
155 return factory.newDocumentBuilder().newDocument();
156 }
157 throw e;
158 }
159 }
160 }
161 }
162
163
164
165
166 private static final class TrackBlankContentAndSkipLeadingWhitespaceReader extends Reader {
167 private final Reader reader_;
168 private boolean wasBlank_ = true;
169
170 TrackBlankContentAndSkipLeadingWhitespaceReader(final Reader characterStream) {
171 super();
172 reader_ = characterStream;
173 }
174
175 public boolean wasBlank() {
176 return wasBlank_;
177 }
178
179 @Override
180 public void close() throws IOException {
181 reader_.close();
182 }
183
184 @Override
185 public int read(final char[] cbuf, final int off, final int len) throws IOException {
186 int readLength = reader_.read(cbuf, off, len);
187
188 if (wasBlank_ && readLength > -1) {
189 for (int i = 0; i < readLength; i++) {
190 final char ch = cbuf[off + i];
191 if (!Character.isWhitespace(ch)) {
192 wasBlank_ = false;
193 if (i > 0) {
194
195 readLength -= i;
196 System.arraycopy(cbuf, off + i, cbuf, off, readLength);
197 }
198 break;
199 }
200 }
201 }
202 return readLength;
203 }
204 }
205
206
207
208
209
210
211
212
213
214
215 public static void appendChild(final SgmlPage page, final DomNode parent, final Node child,
216 final boolean handleXHTMLAsHTML) {
217 final DocumentType documentType = child.getOwnerDocument().getDoctype();
218 if (documentType != null && page instanceof XmlPage xmlPage) {
219 final DomDocumentType domDoctype = new DomDocumentType(
220 page, documentType.getName(), documentType.getPublicId(), documentType.getSystemId());
221 xmlPage.setDocumentType(domDoctype);
222 }
223 final DomNode childXml = createFrom(page, child, handleXHTMLAsHTML);
224 parent.appendChild(childXml);
225 copy(page, child, childXml, handleXHTMLAsHTML);
226 }
227
228 private static DomNode createFrom(final SgmlPage page, final Node source, final boolean handleXHTMLAsHTML) {
229 if (source.getNodeType() == Node.TEXT_NODE) {
230 return new DomText(page, source.getNodeValue());
231 }
232 if (source.getNodeType() == Node.PROCESSING_INSTRUCTION_NODE) {
233 return new DomProcessingInstruction(page, source.getNodeName(), source.getNodeValue());
234 }
235 if (source.getNodeType() == Node.COMMENT_NODE) {
236 return new DomComment(page, source.getNodeValue());
237 }
238 if (source.getNodeType() == Node.DOCUMENT_TYPE_NODE) {
239 final DocumentType documentType = (DocumentType) source;
240 return new DomDocumentType(page, documentType.getName(), documentType.getPublicId(),
241 documentType.getSystemId());
242 }
243 final String ns = source.getNamespaceURI();
244 String localName = source.getLocalName();
245 if (handleXHTMLAsHTML && Html.XHTML_NAMESPACE.equals(ns)) {
246 final ElementFactory factory = page.getWebClient().getPageCreator().getHtmlParser().getFactory(localName);
247 return factory.createElementNS(page, ns, localName,
248 namedNodeMapToSaxAttributes(source.getAttributes()));
249 }
250 final NamedNodeMap nodeAttributes = source.getAttributes();
251 if (page != null && page.isHtmlPage()) {
252 localName = localName.toUpperCase(Locale.ROOT);
253 }
254 final String qualifiedName;
255 if (source.getPrefix() == null) {
256 qualifiedName = localName;
257 }
258 else {
259 qualifiedName = source.getPrefix() + ':' + localName;
260 }
261
262 final String namespaceURI = source.getNamespaceURI();
263 if (Html.SVG_NAMESPACE.equals(namespaceURI)) {
264 return page.getWebClient().getPageCreator().getHtmlParser().getSvgFactory()
265 .createElementNS(page, namespaceURI, qualifiedName,
266 namedNodeMapToSaxAttributes(nodeAttributes));
267 }
268
269 final OrderedFastHashMap<String, DomAttr> attributes = new OrderedFastHashMap<>();
270 for (int i = 0; i < nodeAttributes.getLength(); i++) {
271 final Attr attribute = (Attr) nodeAttributes.item(i);
272 final String attributeNamespaceURI = attribute.getNamespaceURI();
273 final String attributeQualifiedName;
274 if (attribute.getPrefix() == null) {
275 attributeQualifiedName = attribute.getLocalName();
276 }
277 else {
278 attributeQualifiedName = attribute.getPrefix() + ':' + attribute.getLocalName();
279 }
280 final String value = attribute.getNodeValue();
281 final boolean specified = attribute.getSpecified();
282 final DomAttr xmlAttribute =
283 new DomAttr(page, attributeNamespaceURI, attributeQualifiedName, value, specified);
284 attributes.put(attribute.getNodeName(), xmlAttribute);
285 }
286 return new DomElement(namespaceURI, qualifiedName, page, attributes);
287 }
288
289 private static Attributes namedNodeMapToSaxAttributes(final NamedNodeMap attributesMap) {
290 final AttributesImpl attributes = new AttributesImpl();
291 final int length = attributesMap.getLength();
292 for (int i = 0; i < length; i++) {
293 final Node attr = attributesMap.item(i);
294 attributes.addAttribute(attr.getNamespaceURI(), attr.getLocalName(),
295 attr.getNodeName(), null, attr.getNodeValue());
296 }
297
298 return attributes;
299 }
300
301
302
303
304
305
306
307
308
309 private static void copy(final SgmlPage page, final Node source, final DomNode dest,
310 final boolean handleXHTMLAsHTML) {
311 final NodeList nodeChildren = source.getChildNodes();
312 for (int i = 0; i < nodeChildren.getLength(); i++) {
313 final Node child = nodeChildren.item(i);
314 switch (child.getNodeType()) {
315 case Node.ELEMENT_NODE:
316 final DomNode childXml = createFrom(page, child, handleXHTMLAsHTML);
317 dest.appendChild(childXml);
318 copy(page, child, childXml, handleXHTMLAsHTML);
319 break;
320
321 case Node.TEXT_NODE:
322 dest.appendChild(new DomText(page, child.getNodeValue()));
323 break;
324
325 case Node.CDATA_SECTION_NODE:
326 dest.appendChild(new DomCDataSection(page, child.getNodeValue()));
327 break;
328
329 case Node.COMMENT_NODE:
330 dest.appendChild(new DomComment(page, child.getNodeValue()));
331 break;
332
333 case Node.PROCESSING_INSTRUCTION_NODE:
334 dest.appendChild(new DomProcessingInstruction(page, child.getNodeName(), child.getNodeValue()));
335 break;
336
337 default:
338 if (LOG.isWarnEnabled()) {
339 LOG.warn("NodeType " + child.getNodeType()
340 + " (" + child.getNodeName() + ") is not yet supported.");
341 }
342 }
343 }
344 }
345
346
347
348
349
350
351
352
353
354 public static String lookupNamespaceURI(final DomElement element, final String prefix) {
355 final String uri = prefix.isEmpty()
356 ? element.getAttributeDirect("xmlns")
357 : element.getAttribute("xmlns:" + prefix);
358
359 if (ATTRIBUTE_NOT_DEFINED != uri) {
360 return uri;
361 }
362
363 final DomNode parentNode = element.getParentNode();
364 if (parentNode instanceof DomElement domElement) {
365 return lookupNamespaceURI(domElement, prefix);
366 }
367
368 return null;
369 }
370 }