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