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