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