001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.ant;
021
022import java.io.File;
023import java.io.IOException;
024import java.io.InputStream;
025import java.io.OutputStream;
026import java.nio.file.Files;
027import java.util.ArrayList;
028import java.util.Arrays;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.Objects;
033import java.util.Properties;
034import java.util.stream.Collectors;
035
036import org.apache.tools.ant.BuildException;
037import org.apache.tools.ant.DirectoryScanner;
038import org.apache.tools.ant.Project;
039import org.apache.tools.ant.Task;
040import org.apache.tools.ant.taskdefs.LogOutputStream;
041import org.apache.tools.ant.types.EnumeratedAttribute;
042import org.apache.tools.ant.types.FileSet;
043import org.apache.tools.ant.types.Path;
044import org.apache.tools.ant.types.Reference;
045
046import com.puppycrawl.tools.checkstyle.Checker;
047import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
048import com.puppycrawl.tools.checkstyle.DefaultLogger;
049import com.puppycrawl.tools.checkstyle.ModuleFactory;
050import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
051import com.puppycrawl.tools.checkstyle.PropertiesExpander;
052import com.puppycrawl.tools.checkstyle.ThreadModeSettings;
053import com.puppycrawl.tools.checkstyle.XMLLogger;
054import com.puppycrawl.tools.checkstyle.api.AuditListener;
055import com.puppycrawl.tools.checkstyle.api.AutomaticBean;
056import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
057import com.puppycrawl.tools.checkstyle.api.Configuration;
058import com.puppycrawl.tools.checkstyle.api.RootModule;
059import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
060import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
061
062/**
063 * An implementation of a ANT task for calling checkstyle. See the documentation
064 * of the task for usage.
065 */
066public class CheckstyleAntTask extends Task {
067
068    /** Poor man's enum for an xml formatter. */
069    private static final String E_XML = "xml";
070    /** Poor man's enum for an plain formatter. */
071    private static final String E_PLAIN = "plain";
072
073    /** Suffix for time string. */
074    private static final String TIME_SUFFIX = " ms.";
075
076    /** Contains the paths to process. */
077    private final List<Path> paths = new ArrayList<>();
078
079    /** Contains the filesets to process. */
080    private final List<FileSet> fileSets = new ArrayList<>();
081
082    /** Contains the formatters to log to. */
083    private final List<Formatter> formatters = new ArrayList<>();
084
085    /** Contains the Properties to override. */
086    private final List<Property> overrideProps = new ArrayList<>();
087
088    /** Class path to locate class files. */
089    private Path classpath;
090
091    /** Name of file to check. */
092    private String fileName;
093
094    /** Config file containing configuration. */
095    private String config;
096
097    /** Whether to fail build on violations. */
098    private boolean failOnViolation = true;
099
100    /** Property to set on violations. */
101    private String failureProperty;
102
103    /** The name of the properties file. */
104    private File properties;
105
106    /** The maximum number of errors that are tolerated. */
107    private int maxErrors;
108
109    /** The maximum number of warnings that are tolerated. */
110    private int maxWarnings = Integer.MAX_VALUE;
111
112    /**
113     * Whether to execute ignored modules - some modules may log above
114     * their severity depending on their configuration (e.g. WriteTag) so
115     * need to be included
116     */
117    private boolean executeIgnoredModules;
118
119    ////////////////////////////////////////////////////////////////////////////
120    // Setters for ANT specific attributes
121    ////////////////////////////////////////////////////////////////////////////
122
123    /**
124     * Tells this task to write failure message to the named property when there
125     * is a violation.
126     *
127     * @param propertyName the name of the property to set
128     *                      in the event of an failure.
129     */
130    public void setFailureProperty(String propertyName) {
131        failureProperty = propertyName;
132    }
133
134    /**
135     * Sets flag - whether to fail if a violation is found.
136     *
137     * @param fail whether to fail if a violation is found
138     */
139    public void setFailOnViolation(boolean fail) {
140        failOnViolation = fail;
141    }
142
143    /**
144     * Sets the maximum number of errors allowed. Default is 0.
145     *
146     * @param maxErrors the maximum number of errors allowed.
147     */
148    public void setMaxErrors(int maxErrors) {
149        this.maxErrors = maxErrors;
150    }
151
152    /**
153     * Sets the maximum number of warnings allowed. Default is
154     * {@link Integer#MAX_VALUE}.
155     *
156     * @param maxWarnings the maximum number of warnings allowed.
157     */
158    public void setMaxWarnings(int maxWarnings) {
159        this.maxWarnings = maxWarnings;
160    }
161
162    /**
163     * Adds a path.
164     *
165     * @param path the path to add.
166     */
167    public void addPath(Path path) {
168        paths.add(path);
169    }
170
171    /**
172     * Adds set of files (nested fileset attribute).
173     *
174     * @param fileSet the file set to add
175     */
176    public void addFileset(FileSet fileSet) {
177        fileSets.add(fileSet);
178    }
179
180    /**
181     * Add a formatter.
182     *
183     * @param formatter the formatter to add for logging.
184     */
185    public void addFormatter(Formatter formatter) {
186        formatters.add(formatter);
187    }
188
189    /**
190     * Add an override property.
191     *
192     * @param property the property to add
193     */
194    public void addProperty(Property property) {
195        overrideProps.add(property);
196    }
197
198    /**
199     * Set the class path.
200     *
201     * @param classpath the path to locate classes
202     */
203    public void setClasspath(Path classpath) {
204        if (this.classpath == null) {
205            this.classpath = classpath;
206        }
207        else {
208            this.classpath.append(classpath);
209        }
210    }
211
212    /**
213     * Set the class path from a reference defined elsewhere.
214     *
215     * @param classpathRef the reference to an instance defining the classpath
216     */
217    public void setClasspathRef(Reference classpathRef) {
218        createClasspath().setRefid(classpathRef);
219    }
220
221    /**
222     * Creates classpath.
223     *
224     * @return a created path for locating classes
225     */
226    public Path createClasspath() {
227        if (classpath == null) {
228            classpath = new Path(getProject());
229        }
230        return classpath.createPath();
231    }
232
233    /**
234     * Sets file to be checked.
235     *
236     * @param file the file to be checked
237     */
238    public void setFile(File file) {
239        fileName = file.getAbsolutePath();
240    }
241
242    /**
243     * Sets configuration file.
244     *
245     * @param configuration the configuration file, URL, or resource to use
246     * @throws BuildException when config was already set
247     */
248    public void setConfig(String configuration) {
249        if (config != null) {
250            throw new BuildException("Attribute 'config' has already been set");
251        }
252        config = configuration;
253    }
254
255    /**
256     * Sets flag - whether to execute ignored modules.
257     *
258     * @param omit whether to execute ignored modules
259     */
260    public void setExecuteIgnoredModules(boolean omit) {
261        executeIgnoredModules = omit;
262    }
263
264    ////////////////////////////////////////////////////////////////////////////
265    // Setters for Root Module's configuration attributes
266    ////////////////////////////////////////////////////////////////////////////
267
268    /**
269     * Sets a properties file for use instead
270     * of individually setting them.
271     *
272     * @param props the properties File to use
273     */
274    public void setProperties(File props) {
275        properties = props;
276    }
277
278    ////////////////////////////////////////////////////////////////////////////
279    // The doers
280    ////////////////////////////////////////////////////////////////////////////
281
282    @Override
283    public void execute() {
284        final long startTime = System.currentTimeMillis();
285
286        try {
287            final String version = CheckstyleAntTask.class.getPackage().getImplementationVersion();
288
289            log("checkstyle version " + version, Project.MSG_VERBOSE);
290
291            // Check for no arguments
292            if (fileName == null
293                    && fileSets.isEmpty()
294                    && paths.isEmpty()) {
295                throw new BuildException(
296                        "Must specify at least one of 'file' or nested 'fileset' or 'path'.",
297                        getLocation());
298            }
299            if (config == null) {
300                throw new BuildException("Must specify 'config'.", getLocation());
301            }
302            realExecute(version);
303        }
304        finally {
305            final long endTime = System.currentTimeMillis();
306            log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
307                Project.MSG_VERBOSE);
308        }
309    }
310
311    /**
312     * Helper implementation to perform execution.
313     *
314     * @param checkstyleVersion Checkstyle compile version.
315     */
316    private void realExecute(String checkstyleVersion) {
317        // Create the root module
318        RootModule rootModule = null;
319        try {
320            rootModule = createRootModule();
321
322            // setup the listeners
323            final AuditListener[] listeners = getListeners();
324            for (AuditListener element : listeners) {
325                rootModule.addListener(element);
326            }
327            final SeverityLevelCounter warningCounter =
328                new SeverityLevelCounter(SeverityLevel.WARNING);
329            rootModule.addListener(warningCounter);
330
331            processFiles(rootModule, warningCounter, checkstyleVersion);
332        }
333        finally {
334            if (rootModule != null) {
335                rootModule.destroy();
336            }
337        }
338    }
339
340    /**
341     * Scans and processes files by means given root module.
342     *
343     * @param rootModule Root module to process files
344     * @param warningCounter Root Module's counter of warnings
345     * @param checkstyleVersion Checkstyle compile version
346     */
347    private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
348            final String checkstyleVersion) {
349        final long startTime = System.currentTimeMillis();
350        final List<File> files = getFilesToCheck();
351        final long endTime = System.currentTimeMillis();
352        log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
353            Project.MSG_VERBOSE);
354
355        log("Running Checkstyle "
356                + Objects.toString(checkstyleVersion, "")
357                + " on " + files.size()
358                + " files", Project.MSG_INFO);
359        log("Using configuration " + config, Project.MSG_VERBOSE);
360
361        final int numErrs;
362
363        try {
364            final long processingStartTime = System.currentTimeMillis();
365            numErrs = rootModule.process(files);
366            final long processingEndTime = System.currentTimeMillis();
367            log("To process the files took " + (processingEndTime - processingStartTime)
368                + TIME_SUFFIX, Project.MSG_VERBOSE);
369        }
370        catch (CheckstyleException ex) {
371            throw new BuildException("Unable to process files: " + files, ex);
372        }
373        final int numWarnings = warningCounter.getCount();
374        final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
375
376        // Handle the return status
377        if (!okStatus) {
378            final String failureMsg =
379                    "Got " + numErrs + " errors and " + numWarnings
380                            + " warnings.";
381            if (failureProperty != null) {
382                getProject().setProperty(failureProperty, failureMsg);
383            }
384
385            if (failOnViolation) {
386                throw new BuildException(failureMsg, getLocation());
387            }
388        }
389    }
390
391    /**
392     * Creates new instance of the root module.
393     *
394     * @return new instance of the root module
395     */
396    private RootModule createRootModule() {
397        final RootModule rootModule;
398        try {
399            final Properties props = createOverridingProperties();
400            final ThreadModeSettings threadModeSettings =
401                    ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE;
402            final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
403            if (executeIgnoredModules) {
404                ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
405            }
406            else {
407                ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
408            }
409
410            final Configuration configuration = ConfigurationLoader.loadConfiguration(config,
411                    new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings);
412
413            final ClassLoader moduleClassLoader =
414                Checker.class.getClassLoader();
415
416            final ModuleFactory factory = new PackageObjectFactory(
417                    Checker.class.getPackage().getName() + ".", moduleClassLoader);
418
419            rootModule = (RootModule) factory.createModule(configuration.getName());
420            rootModule.setModuleClassLoader(moduleClassLoader);
421            rootModule.configure(configuration);
422        }
423        catch (final CheckstyleException ex) {
424            throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
425                    + "config {%s}, classpath {%s}.", config, classpath), ex);
426        }
427        return rootModule;
428    }
429
430    /**
431     * Create the Properties object based on the arguments specified
432     * to the ANT task.
433     *
434     * @return the properties for property expansion expansion
435     */
436    private Properties createOverridingProperties() {
437        final Properties returnValue = new Properties();
438
439        // Load the properties file if specified
440        if (properties != null) {
441            try (InputStream inStream = Files.newInputStream(properties.toPath())) {
442                returnValue.load(inStream);
443            }
444            catch (final IOException ex) {
445                throw new BuildException("Error loading Properties file '"
446                        + properties + "'", ex, getLocation());
447            }
448        }
449
450        // override with Ant properties like ${basedir}
451        final Map<String, Object> antProps = getProject().getProperties();
452        for (Map.Entry<String, Object> entry : antProps.entrySet()) {
453            final String value = String.valueOf(entry.getValue());
454            returnValue.setProperty(entry.getKey(), value);
455        }
456
457        // override with properties specified in subelements
458        for (Property p : overrideProps) {
459            returnValue.setProperty(p.getKey(), p.getValue());
460        }
461
462        return returnValue;
463    }
464
465    /**
466     * Return the list of listeners set in this task.
467     *
468     * @return the list of listeners.
469     */
470    private AuditListener[] getListeners() {
471        final int formatterCount = Math.max(1, formatters.size());
472
473        final AuditListener[] listeners = new AuditListener[formatterCount];
474
475        // formatters
476        try {
477            if (formatters.isEmpty()) {
478                final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
479                final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
480                listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE,
481                        err, AutomaticBean.OutputStreamOptions.CLOSE);
482            }
483            else {
484                for (int i = 0; i < formatterCount; i++) {
485                    final Formatter formatter = formatters.get(i);
486                    listeners[i] = formatter.createListener(this);
487                }
488            }
489        }
490        catch (IOException ex) {
491            throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
492                    + "formatters {%s}.", formatters), ex);
493        }
494        return listeners;
495    }
496
497    /**
498     * Returns the list of files (full path name) to process.
499     *
500     * @return the list of files included via the fileName, filesets and paths.
501     */
502    private List<File> getFilesToCheck() {
503        final List<File> allFiles = new ArrayList<>();
504        if (fileName != null) {
505            // oops we've got an additional one to process, don't
506            // forget it. No sweat, it's fully resolved via the setter.
507            log("Adding standalone file for audit", Project.MSG_VERBOSE);
508            allFiles.add(new File(fileName));
509        }
510
511        final List<File> filesFromFileSets = scanFileSets();
512        allFiles.addAll(filesFromFileSets);
513
514        final List<File> filesFromPaths = scanPaths();
515        allFiles.addAll(filesFromPaths);
516
517        return allFiles;
518    }
519
520    /**
521     * Retrieves all files from the defined paths.
522     *
523     * @return a list of files defined via paths.
524     */
525    private List<File> scanPaths() {
526        final List<File> allFiles = new ArrayList<>();
527
528        for (int i = 0; i < paths.size(); i++) {
529            final Path currentPath = paths.get(i);
530            final List<File> pathFiles = scanPath(currentPath, i + 1);
531            allFiles.addAll(pathFiles);
532        }
533
534        return allFiles;
535    }
536
537    /**
538     * Scans the given path and retrieves all files for the given path.
539     *
540     * @param path      A path to scan.
541     * @param pathIndex The index of the given path. Used in log messages only.
542     * @return A list of files, extracted from the given path.
543     */
544    private List<File> scanPath(Path path, int pathIndex) {
545        final String[] resources = path.list();
546        log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
547        final List<File> allFiles = new ArrayList<>();
548        int concreteFilesCount = 0;
549
550        for (String resource : resources) {
551            final File file = new File(resource);
552            if (file.isFile()) {
553                concreteFilesCount++;
554                allFiles.add(file);
555            }
556            else {
557                final DirectoryScanner scanner = new DirectoryScanner();
558                scanner.setBasedir(file);
559                scanner.scan();
560                final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
561                allFiles.addAll(scannedFiles);
562            }
563        }
564
565        if (concreteFilesCount > 0) {
566            log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
567                pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
568        }
569
570        return allFiles;
571    }
572
573    /**
574     * Returns the list of files (full path name) to process.
575     *
576     * @return the list of files included via the filesets.
577     */
578    protected List<File> scanFileSets() {
579        final List<File> allFiles = new ArrayList<>();
580
581        for (int i = 0; i < fileSets.size(); i++) {
582            final FileSet fileSet = fileSets.get(i);
583            final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
584            final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
585            allFiles.addAll(scannedFiles);
586        }
587
588        return allFiles;
589    }
590
591    /**
592     * Retrieves all matched files from the given scanner.
593     *
594     * @param scanner  A directory scanner. Note, that {@link DirectoryScanner#scan()}
595     *                 must be called before calling this method.
596     * @param logIndex A log entry index. Used only for log messages.
597     * @return A list of files, retrieved from the given scanner.
598     */
599    private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
600        final String[] fileNames = scanner.getIncludedFiles();
601        log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
602            logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
603
604        return Arrays.stream(fileNames)
605            .map(name -> scanner.getBasedir() + File.separator + name)
606            .map(File::new)
607            .collect(Collectors.toList());
608    }
609
610    /**
611     * Poor mans enumeration for the formatter types.
612     */
613    public static class FormatterType extends EnumeratedAttribute {
614
615        /** My possible values. */
616        private static final String[] VALUES = {E_XML, E_PLAIN};
617
618        @Override
619        public String[] getValues() {
620            return VALUES.clone();
621        }
622
623    }
624
625    /**
626     * Details about a formatter to be used.
627     */
628    public static class Formatter {
629
630        /** The formatter type. */
631        private FormatterType type;
632        /** The file to output to. */
633        private File toFile;
634        /** Whether or not the write to the named file. */
635        private boolean useFile = true;
636
637        /**
638         * Set the type of the formatter.
639         *
640         * @param type the type
641         */
642        public void setType(FormatterType type) {
643            this.type = type;
644        }
645
646        /**
647         * Set the file to output to.
648         *
649         * @param destination destination the file to output to
650         */
651        public void setTofile(File destination) {
652            toFile = destination;
653        }
654
655        /**
656         * Sets whether or not we write to a file if it is provided.
657         *
658         * @param use whether not not to use provided file.
659         */
660        public void setUseFile(boolean use) {
661            useFile = use;
662        }
663
664        /**
665         * Creates a listener for the formatter.
666         *
667         * @param task the task running
668         * @return a listener
669         * @throws IOException if an error occurs
670         */
671        public AuditListener createListener(Task task) throws IOException {
672            final AuditListener listener;
673            if (type != null
674                    && E_XML.equals(type.getValue())) {
675                listener = createXmlLogger(task);
676            }
677            else {
678                listener = createDefaultLogger(task);
679            }
680            return listener;
681        }
682
683        /**
684         * Creates default logger.
685         *
686         * @param task the task to possibly log to
687         * @return a DefaultLogger instance
688         * @throws IOException if an error occurs
689         */
690        private AuditListener createDefaultLogger(Task task)
691                throws IOException {
692            final AuditListener defaultLogger;
693            if (toFile == null || !useFile) {
694                defaultLogger = new DefaultLogger(
695                    new LogOutputStream(task, Project.MSG_DEBUG),
696                        AutomaticBean.OutputStreamOptions.CLOSE,
697                        new LogOutputStream(task, Project.MSG_ERR),
698                        AutomaticBean.OutputStreamOptions.CLOSE
699                );
700            }
701            else {
702                final OutputStream infoStream = Files.newOutputStream(toFile.toPath());
703                defaultLogger =
704                        new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE,
705                                infoStream, AutomaticBean.OutputStreamOptions.NONE);
706            }
707            return defaultLogger;
708        }
709
710        /**
711         * Creates XML logger.
712         *
713         * @param task the task to possibly log to
714         * @return an XMLLogger instance
715         * @throws IOException if an error occurs
716         */
717        private AuditListener createXmlLogger(Task task) throws IOException {
718            final AuditListener xmlLogger;
719            if (toFile == null || !useFile) {
720                xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO),
721                        AutomaticBean.OutputStreamOptions.CLOSE);
722            }
723            else {
724                xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()),
725                        AutomaticBean.OutputStreamOptions.CLOSE);
726            }
727            return xmlLogger;
728        }
729
730    }
731
732    /**
733     * Represents a property that consists of a key and value.
734     */
735    public static class Property {
736
737        /** The property key. */
738        private String key;
739        /** The property value. */
740        private String value;
741
742        /**
743         * Gets key.
744         *
745         * @return the property key
746         */
747        public String getKey() {
748            return key;
749        }
750
751        /**
752         * Sets key.
753         *
754         * @param key sets the property key
755         */
756        public void setKey(String key) {
757            this.key = key;
758        }
759
760        /**
761         * Gets value.
762         *
763         * @return the property value
764         */
765        public String getValue() {
766            return value;
767        }
768
769        /**
770         * Sets value.
771         *
772         * @param value set the property value
773         */
774        public void setValue(String value) {
775            this.value = value;
776        }
777
778        /**
779         * Sets the property value from a File.
780         *
781         * @param file set the property value from a File
782         */
783        public void setFile(File file) {
784            value = file.getAbsolutePath();
785        }
786
787    }
788
789    /** Represents a custom listener. */
790    public static class Listener {
791
792        /** Class name of the listener class. */
793        private String className;
794
795        /**
796         * Gets class name.
797         *
798         * @return the class name
799         */
800        public String getClassname() {
801            return className;
802        }
803
804        /**
805         * Sets class name.
806         *
807         * @param name set the class name
808         */
809        public void setClassname(String name) {
810            className = name;
811        }
812
813    }
814
815}