View Javadoc
1   /*
2    * SPDX-License-Identifier: Apache-2.0
3    * See LICENSE file for details.
4    *
5    * Copyright 2025-2026 Hazendaz
6    * Copyright 2011-2025 Acegi Technology Pty Limited.
7    */
8   package au.com.acegi.xmlformat;
9   
10  import static java.io.File.createTempFile;
11  import static java.nio.file.Files.copy;
12  import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
13  
14  import static au.com.acegi.xmlformat.IOUtil.hash;
15  
16  import java.io.File;
17  import java.io.IOException;
18  import java.io.InputStream;
19  import java.io.OutputStream;
20  import java.io.StringReader;
21  import java.io.UnsupportedEncodingException;
22  import java.nio.file.Files;
23  import java.nio.file.Path;
24  
25  import org.dom4j.Document;
26  import org.dom4j.DocumentException;
27  import org.dom4j.io.SAXReader;
28  import org.dom4j.io.XMLWriter;
29  import org.xml.sax.EntityResolver;
30  import org.xml.sax.InputSource;
31  import org.xml.sax.SAXException;
32  
33  /**
34   * Utility methods private to the package.
35   */
36  final class FormatUtil {
37  
38      private static final String TMP_FILE_PREFIX = FormatUtil.class.getSimpleName();
39  
40      private FormatUtil() {
41      }
42  
43      /**
44       * Ingest an input stream, writing formatted XML to the output stream. The caller is responsible for closing the
45       * input and output streams. Any errors in the input stream will cause an exception and the output stream should not
46       * be relied upon.
47       *
48       * @param in
49       *            input XML stream
50       * @param out
51       *            output XML stream
52       * @param fmt
53       *            format configuration to apply
54       *
55       * @throws DocumentException
56       *             if input XML could not be parsed
57       * @throws IOException
58       *             if output XML stream could not be written
59       */
60      static void format(final InputStream in, final OutputStream out, final XmlOutputFormat fmt)
61              throws DocumentException, IOException {
62          final SAXReader reader = new SAXReader();
63          reader.setEntityResolver(new EntityResolver() {
64              @Override
65              public InputSource resolveEntity(final String publicId, final String systemId)
66                      throws SAXException, IOException {
67                  return new InputSource(new StringReader(""));
68              }
69          });
70          final Document xmlDoc = reader.read(in);
71  
72          final XMLWriter xmlWriter = getXmlWriter(out, fmt);
73          xmlWriter.write(xmlDoc);
74          xmlWriter.flush();
75      }
76  
77      private static XMLWriter getXmlWriter(final OutputStream out, final XmlOutputFormat fmt)
78              throws UnsupportedEncodingException {
79          final XMLWriter xmlWriter;
80          if (fmt.isKeepBlankLines()) {
81              xmlWriter = new BlankLinesWriter(out, fmt);
82          } else {
83              xmlWriter = new XMLWriter(out, fmt);
84          }
85          return xmlWriter;
86      }
87  
88      /**
89       * Formats the input file, overwriting the input file with the new content if the formatted content differs.
90       *
91       * @param file
92       *            to read and then potentially overwrite
93       * @param fmt
94       *            format configuration to apply
95       *
96       * @return true if the file was overwritten
97       *
98       * @throws DocumentException
99       *             if input XML could not be parsed
100      * @throws IOException
101      *             if output XML stream could not be written
102      */
103     static boolean formatInPlace(final File file, final XmlOutputFormat fmt) throws DocumentException, IOException {
104         if (file.length() == 0) {
105             return false;
106         }
107 
108         final File tmpFile = createTempFile(TMP_FILE_PREFIX, ".xml");
109         tmpFile.deleteOnExit();
110 
111         try (InputStream in = Files.newInputStream(file.toPath());
112                 OutputStream out = Files.newOutputStream(tmpFile.toPath())) {
113             format(in, out, fmt);
114         }
115 
116         final long hashFile = hash(file);
117         final long hashTmp = hash(tmpFile);
118         if (hashFile == hashTmp) {
119             return false;
120         }
121 
122         final Path source = tmpFile.toPath();
123         final Path target = file.toPath();
124         copy(source, target, REPLACE_EXISTING);
125         return true;
126     }
127 
128     /**
129      * Only checks if the input file would be modified by the formatter, without overwriting it.
130      *
131      * @param file
132      *            to read
133      * @param fmt
134      *            format configuration to apply
135      *
136      * @return true if the file would be modified by the formatter
137      *
138      * @throws DocumentException
139      *             if input XML could not be parsed
140      * @throws IOException
141      *             if output XML stream could not be written
142      */
143     static boolean needsFormatting(final File file, final XmlOutputFormat fmt) throws DocumentException, IOException {
144         if (file.length() == 0) {
145             return false;
146         }
147 
148         final File tmpFile = createTempFile(TMP_FILE_PREFIX, ".xml");
149         tmpFile.deleteOnExit();
150 
151         try (InputStream in = Files.newInputStream(file.toPath());
152                 OutputStream out = Files.newOutputStream(tmpFile.toPath())) {
153             format(in, out, fmt);
154         }
155 
156         final long hashFile = hash(file);
157         final long hashTmp = hash(tmpFile);
158         return hashFile != hashTmp;
159     }
160 
161 }