1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.xml;
16
17 import static org.htmlunit.BrowserVersionFeatures.JS_XSLT_TRANSFORM_INDENT;
18
19 import java.io.ByteArrayOutputStream;
20 import java.nio.charset.Charset;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Collections;
23 import java.util.HashMap;
24 import java.util.Map;
25
26 import javax.xml.XMLConstants;
27 import javax.xml.parsers.DocumentBuilderFactory;
28 import javax.xml.transform.OutputKeys;
29 import javax.xml.transform.Source;
30 import javax.xml.transform.Transformer;
31 import javax.xml.transform.TransformerFactory;
32 import javax.xml.transform.dom.DOMResult;
33 import javax.xml.transform.dom.DOMSource;
34 import javax.xml.transform.stream.StreamResult;
35
36 import org.htmlunit.SgmlPage;
37 import org.htmlunit.WebResponse;
38 import org.htmlunit.WebResponseData;
39 import org.htmlunit.html.DomDocumentFragment;
40 import org.htmlunit.html.DomNode;
41 import org.htmlunit.html.DomText;
42 import org.htmlunit.http.HttpStatus;
43 import org.htmlunit.javascript.HtmlUnitScriptable;
44 import org.htmlunit.javascript.JavaScriptEngine;
45 import org.htmlunit.javascript.configuration.JsxClass;
46 import org.htmlunit.javascript.configuration.JsxConstructor;
47 import org.htmlunit.javascript.configuration.JsxFunction;
48 import org.htmlunit.javascript.host.dom.Document;
49 import org.htmlunit.javascript.host.dom.DocumentFragment;
50 import org.htmlunit.javascript.host.dom.Node;
51 import org.htmlunit.util.EncodingSniffer;
52 import org.htmlunit.util.XmlUtils;
53 import org.htmlunit.xml.XmlPage;
54 import org.w3c.dom.NodeList;
55
56
57
58
59
60
61
62 @JsxClass
63 public class XSLTProcessor extends HtmlUnitScriptable {
64
65 private Node style_;
66 private final Map<String, Object> parameters_ = new HashMap<>();
67
68
69
70
71 @JsxConstructor
72 public void jsConstructor() {
73
74 }
75
76
77
78
79
80
81
82
83
84 @JsxFunction
85 public void importStylesheet(final Node style) {
86 style_ = style;
87 }
88
89
90
91
92
93
94
95
96 @JsxFunction
97 public XMLDocument transformToDocument(final Node source) {
98 final XMLDocument doc = new XMLDocument();
99 doc.setPrototype(getPrototype(doc.getClass()));
100 doc.setParentScope(getParentScope());
101
102 final Object transformResult = transform(source);
103 final org.w3c.dom.Node node;
104 if (transformResult instanceof org.w3c.dom.Node transformedDoc) {
105 node = transformedDoc.getFirstChild();
106 }
107 else {
108 node = null;
109 }
110 final XmlPage page = new XmlPage(node, getWindow().getWebWindow());
111 doc.setDomNode(page);
112 return doc;
113 }
114
115
116
117
118 private Object transform(final Node source) {
119 try {
120 final DomNode sourceDomNode = source.getDomNodeOrDie();
121 Source xmlSource = new DOMSource(sourceDomNode);
122
123 final DomNode xsltDomNode = style_.getDomNodeOrDie();
124 final Source xsltSource = new DOMSource(xsltDomNode);
125
126 final TransformerFactory transformerFactory = TransformerFactory.newInstance();
127
128
129
130
131 transformerFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
132
133 final SgmlPage page = sourceDomNode.getPage();
134 if (page != null && page.getWebClient().getBrowserVersion()
135 .hasFeature(JS_XSLT_TRANSFORM_INDENT)) {
136 final DomNode outputNode = findOutputNode(xsltDomNode);
137 if (outputNode != null) {
138 final org.w3c.dom.Node indentNode = outputNode.getAttributes().getNamedItem("indent");
139 if (indentNode != null && "yes".equalsIgnoreCase(indentNode.getNodeValue())) {
140 try {
141 transformerFactory.setAttribute("indent-number", Integer.valueOf(2));
142 }
143 catch (final IllegalArgumentException ignored) {
144
145 }
146 final Transformer transformer = transformerFactory.newTransformer(xsltSource);
147 transformer.setOutputProperty(OutputKeys.INDENT, "yes");
148 try {
149 transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
150 }
151 catch (final IllegalArgumentException ignored) {
152
153 }
154
155 for (final Map.Entry<String, Object> entry : parameters_.entrySet()) {
156 transformer.setParameter(entry.getKey(), entry.getValue());
157 }
158
159
160
161
162 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
163 transformer.transform(xmlSource, new StreamResult(out));
164 final WebResponseData data = new WebResponseData(out.toByteArray(),
165 HttpStatus.OK_200, HttpStatus.OK_200_MSG, Collections.emptyList());
166 final WebResponse response = new WebResponse(data, null, 0) {
167
168
169
170 @Override
171 public Charset getContentCharset() {
172 final Charset cs = EncodingSniffer.toCharset(
173 transformer.getOutputProperty(OutputKeys.ENCODING));
174 if (cs == null) {
175 return StandardCharsets.UTF_8;
176 }
177 return cs;
178 }
179 };
180 return XmlUtils.buildDocument(response);
181 }
182 }
183 }
184 }
185
186 final Transformer transformer = transformerFactory.newTransformer(xsltSource);
187 transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
188
189 for (final Map.Entry<String, Object> entry : parameters_.entrySet()) {
190 transformer.setParameter(entry.getKey(), entry.getValue());
191 }
192
193 final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
194 final org.w3c.dom.Document containerDocument = factory.newDocumentBuilder().newDocument();
195 final org.w3c.dom.Element containerElement = containerDocument.createElement("container");
196 containerDocument.appendChild(containerElement);
197
198 final DOMResult result = new DOMResult(containerElement);
199 transformer.transform(xmlSource, result);
200
201 final org.w3c.dom.Node transformedNode = result.getNode();
202 final org.w3c.dom.Node transformedFirstChild = transformedNode.getFirstChild();
203 if (transformedFirstChild != null && transformedFirstChild.getNodeType() == Node.ELEMENT_NODE) {
204 return transformedNode;
205 }
206
207
208 xmlSource = new DOMSource(source.getDomNodeOrDie());
209 try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
210 transformer.transform(xmlSource, new StreamResult(out));
211
212 final Charset cs = EncodingSniffer.toCharset(transformer.getOutputProperty(OutputKeys.ENCODING));
213 if (cs == null) {
214 return new String(out.toByteArray(), StandardCharsets.UTF_8);
215 }
216 return new String(out.toByteArray(), cs);
217 }
218 }
219 catch (final RuntimeException e) {
220 throw e;
221 }
222 catch (final Exception e) {
223 throw JavaScriptEngine.reportRuntimeError("Exception: " + e);
224 }
225 }
226
227
228
229
230
231
232
233
234 @JsxFunction
235 public DocumentFragment transformToFragment(final Node source, final Object output) {
236 final SgmlPage page = (SgmlPage) ((Document) output).getDomNodeOrDie();
237
238 final DomDocumentFragment fragment = page.createDocumentFragment();
239 final DocumentFragment rv = new DocumentFragment();
240 rv.setPrototype(getPrototype(rv.getClass()));
241 rv.setParentScope(getParentScope());
242 rv.setDomNode(fragment);
243
244 final Object result = transform(source);
245 if (result instanceof org.w3c.dom.Node node) {
246 final SgmlPage parentPage = fragment.getPage();
247 final NodeList children = node.getChildNodes();
248 final int length = children.getLength();
249 for (int i = 0; i < length; i++) {
250 XmlUtils.appendChild(parentPage, fragment, children.item(i), true);
251 }
252 }
253 else {
254 final DomText text = new DomText(fragment.getPage(), (String) result);
255 fragment.appendChild(text);
256 }
257
258 return rv;
259 }
260
261
262
263
264
265
266
267
268 @JsxFunction
269 public void setParameter(final String namespaceURI, final String localName, final Object value) {
270 parameters_.put(getQualifiedName(namespaceURI, localName), value);
271 }
272
273
274
275
276
277
278
279 @JsxFunction
280 public Object getParameter(final String namespaceURI, final String localName) {
281 return parameters_.get(getQualifiedName(namespaceURI, localName));
282 }
283
284 private static String getQualifiedName(final String namespaceURI, final String localName) {
285 final String qualifiedName;
286 if (namespaceURI != null && !namespaceURI.isEmpty() && !"null".equals(namespaceURI)) {
287 qualifiedName = '{' + namespaceURI + '}' + localName;
288 }
289 else {
290 qualifiedName = localName;
291 }
292 return qualifiedName;
293 }
294
295 private static DomNode findOutputNode(final DomNode xsltDomNode) {
296 for (final DomNode child : xsltDomNode.getChildren()) {
297 if ("output".equals(child.getLocalName())) {
298 return child;
299 }
300
301 for (final DomNode child1 : child.getChildren()) {
302 if ("output".equals(child1.getLocalName())) {
303 return child1;
304 }
305 }
306 }
307 return null;
308 }
309 }