1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html;
16
17 import static java.nio.charset.StandardCharsets.ISO_8859_1;
18
19 import java.io.File;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.OutputStream;
23 import java.net.URL;
24 import java.nio.charset.Charset;
25 import java.nio.file.Files;
26 import java.util.HashMap;
27 import java.util.Map;
28 import java.util.regex.Pattern;
29
30 import org.apache.commons.io.FileUtils;
31 import org.apache.commons.io.IOUtils;
32 import org.apache.commons.lang3.StringUtils;
33 import org.apache.commons.logging.Log;
34 import org.apache.commons.logging.LogFactory;
35 import org.htmlunit.Page;
36 import org.htmlunit.SgmlPage;
37 import org.htmlunit.WebResponse;
38 import org.htmlunit.util.MimeType;
39
40
41
42
43
44
45
46 public class XmlSerializer {
47
48 private static final String FILE_SEPARATOR = "/";
49 private static final Pattern CREATE_FILE_PATTERN = Pattern.compile(".*/");
50
51 private static final Log LOG = LogFactory.getLog(XmlSerializer.class);
52
53 private final StringBuilder builder_ = new StringBuilder();
54 private final StringBuilder indent_ = new StringBuilder();
55 private File outputDir_;
56
57
58
59
60
61
62
63
64 public void save(final SgmlPage page, final File file) throws IOException {
65 save(page, file, false);
66 }
67
68 private void save(final SgmlPage page, final File file, final boolean append) throws IOException {
69 String fileName = file.getName();
70
71 if (!append) {
72 if (!fileName.endsWith(".htm") && !fileName.endsWith(".html")) {
73 fileName += ".html";
74 }
75 }
76 final File outputFile = new File(file.getParentFile(), fileName);
77
78 if (!append && outputFile.exists()) {
79 throw new IOException("File already exists: " + outputFile);
80 }
81 fileName = fileName.substring(0, fileName.lastIndexOf('.'));
82 outputDir_ = new File(file.getParentFile(), fileName);
83
84
85
86 final DomElement node = page.getDocumentElement();
87 Charset charsetName = ISO_8859_1;
88 builder_.setLength(0);
89 indent_.setLength(0);
90 if (page.isHtmlPage()) {
91 charsetName = page.getCharset();
92 if (charsetName != null && node instanceof HtmlHtml) {
93 builder_.append("<?xml version=\"1.0\" encoding=\"").append(charsetName).append("\"?>\n");
94 }
95 }
96 printXml(node);
97 final String response = builder_.toString();
98 builder_.setLength(0);
99 FileUtils.writeStringToFile(outputFile, response, charsetName, append);
100 }
101
102
103
104
105
106
107
108
109 public String asXml(final DomElement node) throws IOException {
110 builder_.setLength(0);
111 indent_.setLength(0);
112 final SgmlPage page = node.getPage();
113 if (null != page && page.isHtmlPage()) {
114 final Charset charsetName = page.getCharset();
115 if (charsetName != null && node instanceof HtmlHtml) {
116 builder_.append("<?xml version=\"1.0\" encoding=\"").append(charsetName).append("\"?>\n");
117 }
118 }
119 printXml(node);
120 final String response = builder_.toString();
121 builder_.setLength(0);
122 return response;
123 }
124
125 protected void printXml(final DomElement node) throws IOException {
126 if (!isExcluded(node)) {
127 final boolean hasChildren = node.getFirstChild() != null;
128 builder_.append(indent_).append('<');
129 printOpeningTag(node);
130
131 if (hasChildren || node.isEmptyXmlTagExpanded()) {
132 builder_.append(">\n");
133 for (DomNode child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
134 indent_.append(" ");
135 if (child instanceof DomElement element) {
136 printXml(element);
137 }
138 else {
139 builder_.append(child);
140 }
141 indent_.setLength(indent_.length() - 2);
142 }
143 builder_.append(indent_).append("</").append(node.getTagName()).append(">\n");
144 }
145 else {
146 builder_.append("/>\n");
147 }
148 }
149 }
150
151
152
153
154
155
156
157 public String asText(final DomNode node) {
158 builder_.setLength(0);
159
160 if (node instanceof DomText text) {
161 builder_.append(text.getData());
162 }
163 else {
164 printText(node);
165 }
166
167 final String response = builder_.toString();
168 builder_.setLength(0);
169 return response;
170 }
171
172
173
174
175
176 protected void printText(final DomNode node) {
177 for (DomNode child = node.getFirstChild(); child != null; child = child.getNextSibling()) {
178 if (child instanceof DomText text) {
179 builder_.append(text.getData());
180 }
181 else {
182 printText(child);
183 }
184 }
185 }
186
187
188
189
190
191
192
193 protected void printOpeningTag(final DomElement node) throws IOException {
194 builder_.append(node.getTagName());
195 final Map<String, DomAttr> attributes = readAttributes(node);
196
197 for (final Map.Entry<String, DomAttr> entry : attributes.entrySet()) {
198 builder_.append(' ')
199 .append(entry.getKey())
200 .append("=\"");
201 final String value = entry.getValue().getNodeValue();
202 builder_.append(org.htmlunit.util.StringUtils.escapeXmlAttributeValue(value))
203 .append('"');
204 }
205 }
206
207 private Map<String, DomAttr> readAttributes(final DomElement node) throws IOException {
208 if (node instanceof HtmlImage image) {
209 return getAttributesFor(image);
210 }
211 else if (node instanceof HtmlLink link) {
212 return getAttributesFor(link);
213 }
214 else if (node instanceof BaseFrameElement element) {
215 return getAttributesFor(element);
216 }
217
218 Map<String, DomAttr> attributes = node.getAttributesMap();
219 if (node instanceof HtmlOption option) {
220 attributes = new HashMap<>(attributes);
221 if (option.isSelected()) {
222 if (!attributes.containsKey("selected")) {
223 attributes.put("selected", new DomAttr(node.getPage(), null, "selected", "selected", false));
224 }
225 }
226 else {
227 attributes.remove("selected");
228 }
229 }
230 return attributes;
231 }
232
233
234
235
236
237
238
239 private Map<String, DomAttr> getAttributesFor(final BaseFrameElement frame) throws IOException {
240 final Map<String, DomAttr> map = createAttributesCopyWithClonedAttribute(frame, DomElement.SRC_ATTRIBUTE);
241 final DomAttr srcAttr = map.get(DomElement.SRC_ATTRIBUTE);
242 if (srcAttr == null) {
243 return map;
244 }
245
246 final Page enclosedPage = frame.getEnclosedPage();
247 final String suffix = getFileExtension(enclosedPage);
248 final File file = createFile(srcAttr.getValue(), "." + suffix);
249
250 if (enclosedPage != null) {
251 if (enclosedPage.isHtmlPage()) {
252 new XmlSerializer().save((HtmlPage) enclosedPage, file, true);
253 }
254 else {
255 try (InputStream is = enclosedPage.getWebResponse().getContentAsStream()) {
256 try (OutputStream fos = Files.newOutputStream(file.toPath())) {
257 IOUtils.copyLarge(is, fos);
258 }
259 }
260 }
261 }
262
263 srcAttr.setValue(file.getParentFile().getName() + FILE_SEPARATOR + file.getName());
264 return map;
265 }
266
267 private static String getFileExtension(final Page enclosedPage) {
268 if (enclosedPage != null) {
269 if (enclosedPage.isHtmlPage()) {
270 return "html";
271 }
272
273 final URL url = enclosedPage.getUrl();
274 if (url.getPath().contains(".")) {
275 return StringUtils.substringAfterLast(url.getPath(), ".");
276 }
277 }
278
279 return ".unknown";
280 }
281
282
283
284
285
286
287
288
289 protected Map<String, DomAttr> getAttributesFor(final HtmlLink link) throws IOException {
290 final Map<String, DomAttr> map = createAttributesCopyWithClonedAttribute(link, "href");
291 final DomAttr hrefAttr = map.get("href");
292 if (hrefAttr != null && org.htmlunit.util.StringUtils.isNotBlank(hrefAttr.getValue())) {
293 final String protocol = link.getWebRequest().getUrl().getProtocol();
294 if ("http".equals(protocol) || "https".equals(protocol)) {
295 try {
296 final WebResponse response = link.getWebResponse(true, null, false, null);
297
298 final File file = createFile(hrefAttr.getValue(), ".css");
299 FileUtils.writeStringToFile(file, response.getContentAsString(), ISO_8859_1);
300 hrefAttr.setValue(outputDir_.getName() + FILE_SEPARATOR + file.getName());
301 }
302 catch (final IOException e) {
303 LOG.error("XmlSerializer: IOException while downloading link content from url '"
304 + hrefAttr + "'", e);
305 }
306 catch (final IllegalStateException e) {
307 LOG.error("XmlSerializer: IllegalStateException while downloading link content from url '"
308 + hrefAttr + "'", e);
309 }
310 }
311 }
312
313 return map;
314 }
315
316
317
318
319
320
321
322 protected Map<String, DomAttr> getAttributesFor(final HtmlImage image) {
323 final Map<String, DomAttr> map = createAttributesCopyWithClonedAttribute(image, DomElement.SRC_ATTRIBUTE);
324 final DomAttr srcAttr = map.get(DomElement.SRC_ATTRIBUTE);
325 if (srcAttr != null && org.htmlunit.util.StringUtils.isNotBlank(srcAttr.getValue())) {
326 try {
327 final WebResponse response = image.getWebResponse(true);
328
329 try (InputStream inputStream = response.getContentAsStream()) {
330 final File file = createFile(srcAttr.getValue(), "." + getSuffix(response));
331 FileUtils.copyInputStreamToFile(inputStream, file);
332
333 final String valueOnFileSystem = outputDir_.getName() + FILE_SEPARATOR + file.getName();
334
335 srcAttr.setValue(valueOnFileSystem);
336 }
337 }
338 catch (final IOException e) {
339 LOG.error("XmlSerializer: IOException while downloading image content from url '" + srcAttr + "'", e);
340 }
341 catch (final IllegalStateException e) {
342 LOG.error("XmlSerializer: IllegalStateException while downloading image content from url '"
343 + srcAttr + "'", e);
344 }
345 }
346
347 return map;
348 }
349
350
351
352
353
354
355
356 private static String getSuffix(final WebResponse response) {
357
358 final String url = response.getWebRequest().getUrl().toString();
359 final String fileName =
360 StringUtils.substringAfterLast(org.htmlunit.util.StringUtils.substringBefore(url, "?"), "/");
361
362 final String suffix = StringUtils.substringAfterLast(fileName, ".");
363 if (suffix.length() > 1 && suffix.length() < 5) {
364 return suffix;
365 }
366
367
368 return MimeType.getFileExtension(response.getContentType());
369 }
370
371
372
373
374
375
376
377
378 private static Map<String, DomAttr> createAttributesCopyWithClonedAttribute(final HtmlElement elt,
379 final String attrName) {
380 final Map<String, DomAttr> newMap = new HashMap<>(elt.getAttributesMap());
381
382
383 final DomAttr attr = newMap.get(attrName);
384 if (null == attr) {
385 return newMap;
386 }
387
388 final DomAttr clonedAttr = new DomAttr(attr.getPage(), attr.getNamespaceURI(),
389 attr.getQualifiedName(), attr.getValue(), attr.getSpecified());
390
391 newMap.put(attrName, clonedAttr);
392
393 return newMap;
394 }
395
396
397
398
399
400
401
402 protected boolean isExcluded(final DomElement element) {
403 return element instanceof HtmlScript;
404 }
405
406
407
408
409
410
411
412
413 private File createFile(final String url, final String extension) throws IOException {
414 String name = url.replaceFirst("/$", "");
415 name = CREATE_FILE_PATTERN.matcher(name).replaceAll("");
416 name = org.htmlunit.util.StringUtils.substringBefore(name, "?");
417 name = org.htmlunit.util.StringUtils.substringBefore(name, ";");
418 name = StringUtils.substring(name, 0, 30);
419 name = org.htmlunit.util.StringUtils.sanitizeForFileName(name);
420 if (!name.endsWith(extension)) {
421 name += extension;
422 }
423 int counter = 0;
424 while (true) {
425 final String fileName;
426 if (counter == 0) {
427 fileName = name;
428 }
429 else {
430 fileName = StringUtils.substringBeforeLast(name, ".")
431 + "_" + counter + "." + StringUtils.substringAfterLast(name, ".");
432 }
433 FileUtils.forceMkdir(outputDir_);
434 final File f = new File(outputDir_, fileName);
435 if (f.createNewFile()) {
436 return f;
437 }
438 counter++;
439 }
440 }
441 }