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 java.io.File;
11  import java.io.IOException;
12  import java.util.ArrayList;
13  import java.util.Arrays;
14  import java.util.List;
15  
16  import org.apache.maven.plugin.AbstractMojo;
17  import org.apache.maven.plugin.MojoExecutionException;
18  import org.apache.maven.plugin.MojoFailureException;
19  import org.apache.maven.plugins.annotations.Parameter;
20  import org.codehaus.plexus.util.DirectoryScanner;
21  import org.dom4j.DocumentException;
22  
23  /**
24   * Common infrastructure for the various plugin goals.
25   */
26  @SuppressWarnings("DesignForExtension")
27  public abstract class AbstractXmlPlugin extends AbstractMojo {
28  
29      /**
30       * Quote character to use when writing attributes.
31       */
32      @Parameter(property = "attributeQuoteChar", defaultValue = "\"")
33      @SuppressWarnings("PMD.ImmutableField")
34      private char attributeQuoteChar = '"';
35  
36      /**
37       * The base directory of the project.
38       */
39      @Parameter(defaultValue = ".", readonly = true, required = true, property = "project.basedir")
40      private File baseDirectory;
41  
42      /**
43       * The encoding format.
44       */
45      @Parameter(property = "encoding", defaultValue = "UTF-8")
46      @SuppressWarnings("PMD.ImmutableField")
47      private String encoding = "UTF-8";
48  
49      /**
50       * A set of file patterns that allow you to exclude certain files/folders from the formatting. In addition to these
51       * exclusions, the project build directory (typically <code>target</code>) is always excluded if skipTargetFolder is
52       * true.
53       */
54      @Parameter(property = "excludes")
55      private String[] excludes;
56  
57      /**
58       * Whether or not to expand empty elements to &lt;tagName&gt;&lt;/tagName&gt;.
59       */
60      @Parameter(property = "expandEmptyElements", defaultValue = "false")
61      private boolean expandEmptyElements;
62  
63      /**
64       * A set of file patterns that dictate which files should be included in the formatting with each file pattern being
65       * relative to the base directory.
66       */
67      @Parameter(property = "includes")
68      private String[] includes;
69  
70      /**
71       * Indicates the number of spaces to apply when indenting.
72       */
73      @Parameter(property = "indentSize", defaultValue = "2")
74      private int indentSize;
75  
76      /**
77       * Use tabs instead of spaces for indents. If set to <code>true</code>, <code>indentSize</code> will be ignored.
78       */
79      @Parameter(property = "tabIndent", defaultValue = "false")
80      private boolean tabIndent;
81  
82      /**
83       * Sets the line-ending of files after formatting. Valid values are:
84       * <ul>
85       * <li><b>"SYSTEM"</b> - Use line endings of current system</li>
86       * <li><b>"LF"</b> - Use Unix and Mac style line endings</li>
87       * <li><b>"CRLF"</b> - Use DOS and Windows style line endings</li>
88       * <li><b>"CR"</b> - Use early Mac style line endings</li>
89       * </ul>
90       * This property is only used if {@link #lineSeparator} has its default value. Do not set any value for
91       * {@link #lineSeparator}.
92       */
93      @Parameter(property = "lineEnding", defaultValue = "LF")
94      @SuppressWarnings("PMD.ImmutableField")
95      private LineEnding lineEnding = LineEnding.LF;
96  
97      /**
98       * New line separator.
99       *
100      * @deprecated Please do not set this value; use {@link #lineEnding} instead
101      */
102     @Parameter(property = "lineSeparator", defaultValue = "\n")
103     @SuppressWarnings("PMD.ImmutableField")
104     @Deprecated
105     private String lineSeparator = "\n";
106 
107     /**
108      * Whether or not to print new line after the XML declaration.
109      */
110     @Parameter(property = "newLineAfterDeclaration", defaultValue = "false")
111     private boolean newLineAfterDeclaration;
112 
113     /**
114      * Controls when to output a line.separator every so many tags in case of no lines and total text trimming.
115      */
116     @Parameter(property = "newLineAfterNTags", defaultValue = "0")
117     private int newLineAfterNTags;
118 
119     /**
120      * The default new line flag, set to do new lines only as in original document.
121      */
122     @Parameter(property = "newlines", defaultValue = "true")
123     private boolean newlines;
124 
125     /**
126      * Whether or not to output the encoding in the XML declaration.
127      */
128     @Parameter(property = "omitEncoding", defaultValue = "false")
129     private boolean omitEncoding;
130 
131     /**
132      * Pad string-element boundaries with whitespace.
133      */
134     @Parameter(property = "padText", defaultValue = "false")
135     private boolean padText;
136 
137     /**
138      * Skip XML formatting.
139      */
140     @Parameter(property = "xml-format.skip", defaultValue = "false")
141     private boolean skip;
142 
143     /**
144      * In addition to the exclusions, the project build directory (typically <code>target</code>) is always excluded if
145      * true.
146      */
147     @Parameter(property = "skipTargetFolder", defaultValue = "true")
148     private boolean skipTargetFolder = true;
149 
150     /**
151      * Whether or not to suppress the XML declaration.
152      */
153     @Parameter(property = "suppressDeclaration", defaultValue = "false")
154     private boolean suppressDeclaration;
155 
156     /**
157      * The project target directory. This is always excluded from formatting.
158      */
159     @Parameter(defaultValue = "${project.build.directory}", readonly = true, required = true)
160     private File targetDirectory;
161 
162     /**
163      * Should we preserve whitespace or not in text nodes.
164      */
165     @Parameter(property = "trimText", defaultValue = "true")
166     private boolean trimText;
167 
168     /**
169      * Whether or not to use XHTML standard.
170      */
171     @Parameter(property = "xhtml", defaultValue = "false")
172     private boolean xhtml;
173 
174     /**
175      * Whether to keep blank lines. A maximum of one line is preserved between each tag.
176      */
177     @Parameter(property = "keepBlankLines", defaultValue = "false")
178     private boolean keepBlankLines;
179 
180     @Override
181     public void execute() throws MojoExecutionException, MojoFailureException {
182         assert baseDirectory != null;
183         assert targetDirectory != null;
184 
185         if (skip) {
186             getLog().info("[xml-format] Skipped");
187             return;
188         }
189 
190         initializeIncludes();
191         initializeExcludes();
192 
193         final XmlOutputFormat fmt = buildFormatter();
194 
195         boolean success = true;
196         boolean neededFormatting = false;
197         for (final String inputName : find()) {
198             final File input = baseDirectory.toPath().resolve(inputName).toFile();
199             try {
200                 neededFormatting |= processFile(input, fmt);
201             } catch (final DocumentException | IOException ex) {
202                 success = false;
203                 getLog().error("[xml-format] Error for " + input, ex);
204             }
205         }
206 
207         if (!success) {
208             throw new MojoFailureException("[xml-format] Failed)");
209         }
210         afterAllProcessed(neededFormatting);
211     }
212 
213     /**
214      * Processes a single file found in the project.
215      *
216      * @param input
217      *            the file to process
218      * @param fmt
219      *            the formatting options
220      *
221      * @return true if the file required changes to match the formatting style
222      *
223      * @throws DocumentException
224      *             if input XML could not be parsed
225      * @throws IOException
226      *             if output XML stream could not be written
227      */
228     protected abstract boolean processFile(File input, XmlOutputFormat fmt) throws DocumentException, IOException;
229 
230     /**
231      * Invoked after all files in the project have been processed.
232      *
233      * @param neededFormatting
234      *            whether any processed file required changes to match the formatting style
235      *
236      * @throws MojoExecutionException
237      *             if the build must be failed
238      */
239     protected abstract void afterAllProcessed(boolean neededFormatting) throws MojoExecutionException;
240 
241     void setBaseDirectory(final File baseDirectory) {
242         this.baseDirectory = baseDirectory;
243     }
244 
245     void setExcludes(final String... excludes) {
246         this.excludes = excludes == null ? null : Arrays.copyOf(excludes, excludes.length);
247     }
248 
249     void setIncludes(final String... includes) {
250         this.includes = includes == null ? null : Arrays.copyOf(includes, includes.length);
251     }
252 
253     void setSkip(final boolean skip) {
254         this.skip = skip;
255     }
256 
257     void setSkipTargetFolder(final boolean skipTargetFolder) {
258         this.skipTargetFolder = skipTargetFolder;
259     }
260 
261     void setTargetDirectory(final File targetDirectory) {
262         this.targetDirectory = targetDirectory;
263     }
264 
265     private XmlOutputFormat buildFormatter() {
266         final XmlOutputFormat fmt = new XmlOutputFormat();
267         fmt.setAttributeQuoteCharacter(attributeQuoteChar);
268         fmt.setEncoding(encoding);
269         fmt.setExpandEmptyElements(expandEmptyElements);
270         if (tabIndent) {
271             fmt.setIndent("\t");
272         } else {
273             fmt.setIndentSize(indentSize);
274         }
275         fmt.setLineSeparator(determineLineSeparator());
276         fmt.setNewLineAfterDeclaration(newLineAfterDeclaration);
277         fmt.setNewLineAfterNTags(newLineAfterNTags);
278         fmt.setNewlines(newlines);
279         fmt.setOmitEncoding(omitEncoding);
280         fmt.setPadText(padText);
281         fmt.setSuppressDeclaration(suppressDeclaration);
282         fmt.setTrimText(trimText);
283         fmt.setXHTML(xhtml);
284         fmt.setKeepBlankLines(keepBlankLines);
285         return fmt;
286     }
287 
288     private String determineLineSeparator() {
289         return "\n".equals(lineSeparator) ? lineEnding.getChars() : lineSeparator;
290     }
291 
292     private String[] find() {
293         final DirectoryScanner dirScanner = new DirectoryScanner();
294         dirScanner.setBasedir(baseDirectory);
295         dirScanner.setIncludes(includes);
296 
297         final List<String> exclude = new ArrayList<>(Arrays.asList(excludes));
298         if (skipTargetFolder && baseDirectory.equals(targetDirectory.getParentFile())) {
299             exclude.add(targetDirectory.getName() + "/**");
300         }
301         final String[] excluded = new String[exclude.size()];
302         dirScanner.setExcludes(exclude.toArray(excluded));
303 
304         dirScanner.scan();
305         return dirScanner.getIncludedFiles();
306     }
307 
308     private void initializeExcludes() {
309         if (excludes == null || excludes.length == 0) {
310             excludes = new String[0];
311         }
312     }
313 
314     private void initializeIncludes() {
315         if (includes == null || includes.length == 0) {
316             includes = new String[] { "**/*.xml" };
317         }
318     }
319 }