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