001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2019 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.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            final String version = CheckstyleAntTask.class.getPackage().getImplementationVersion();
274
275            log("checkstyle version " + version, Project.MSG_VERBOSE);
276
277            // Check for no arguments
278            if (fileName == null
279                    && fileSets.isEmpty()
280                    && paths.isEmpty()) {
281                throw new BuildException(
282                        "Must specify at least one of 'file' or nested 'fileset' or 'path'.",
283                        getLocation());
284            }
285            if (config == null) {
286                throw new BuildException("Must specify 'config'.", getLocation());
287            }
288            realExecute(version);
289        }
290        finally {
291            final long endTime = System.currentTimeMillis();
292            log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
293                Project.MSG_VERBOSE);
294        }
295    }
296
297    /**
298     * Helper implementation to perform execution.
299     * @param checkstyleVersion Checkstyle compile version.
300     */
301    private void realExecute(String checkstyleVersion) {
302        // Create the root module
303        RootModule rootModule = null;
304        try {
305            rootModule = createRootModule();
306
307            // setup the listeners
308            final AuditListener[] listeners = getListeners();
309            for (AuditListener element : listeners) {
310                rootModule.addListener(element);
311            }
312            final SeverityLevelCounter warningCounter =
313                new SeverityLevelCounter(SeverityLevel.WARNING);
314            rootModule.addListener(warningCounter);
315
316            processFiles(rootModule, warningCounter, checkstyleVersion);
317        }
318        finally {
319            if (rootModule != null) {
320                rootModule.destroy();
321            }
322        }
323    }
324
325    /**
326     * Scans and processes files by means given root module.
327     * @param rootModule Root module to process files
328     * @param warningCounter Root Module's counter of warnings
329     * @param checkstyleVersion Checkstyle compile version
330     */
331    private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
332            final String checkstyleVersion) {
333        final long startTime = System.currentTimeMillis();
334        final List<File> files = getFilesToCheck();
335        final long endTime = System.currentTimeMillis();
336        log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
337            Project.MSG_VERBOSE);
338
339        log("Running Checkstyle "
340                + Objects.toString(checkstyleVersion, "")
341                + " on " + files.size()
342                + " files", Project.MSG_INFO);
343        log("Using configuration " + config, Project.MSG_VERBOSE);
344
345        final int numErrs;
346
347        try {
348            final long processingStartTime = System.currentTimeMillis();
349            numErrs = rootModule.process(files);
350            final long processingEndTime = System.currentTimeMillis();
351            log("To process the files took " + (processingEndTime - processingStartTime)
352                + TIME_SUFFIX, Project.MSG_VERBOSE);
353        }
354        catch (CheckstyleException ex) {
355            throw new BuildException("Unable to process files: " + files, ex);
356        }
357        final int numWarnings = warningCounter.getCount();
358        final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
359
360        // Handle the return status
361        if (!okStatus) {
362            final String failureMsg =
363                    "Got " + numErrs + " errors and " + numWarnings
364                            + " warnings.";
365            if (failureProperty != null) {
366                getProject().setProperty(failureProperty, failureMsg);
367            }
368
369            if (failOnViolation) {
370                throw new BuildException(failureMsg, getLocation());
371            }
372        }
373    }
374
375    /**
376     * Creates new instance of the root module.
377     * @return new instance of the root module
378     */
379    private RootModule createRootModule() {
380        final RootModule rootModule;
381        try {
382            final Properties props = createOverridingProperties();
383            final ThreadModeSettings threadModeSettings =
384                    ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE;
385            final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
386            if (executeIgnoredModules) {
387                ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
388            }
389            else {
390                ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
391            }
392
393            final Configuration configuration = ConfigurationLoader.loadConfiguration(config,
394                    new PropertiesExpander(props), ignoredModulesOptions, threadModeSettings);
395
396            final ClassLoader moduleClassLoader =
397                Checker.class.getClassLoader();
398
399            final ModuleFactory factory = new PackageObjectFactory(
400                    Checker.class.getPackage().getName() + ".", moduleClassLoader);
401
402            rootModule = (RootModule) factory.createModule(configuration.getName());
403            rootModule.setModuleClassLoader(moduleClassLoader);
404
405            if (rootModule instanceof Checker) {
406                final ClassLoader loader = new AntClassLoader(getProject(),
407                        classpath);
408
409                ((Checker) rootModule).setClassLoader(loader);
410            }
411
412            rootModule.configure(configuration);
413        }
414        catch (final CheckstyleException ex) {
415            throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
416                    + "config {%s}, classpath {%s}.", config, classpath), ex);
417        }
418        return rootModule;
419    }
420
421    /**
422     * Create the Properties object based on the arguments specified
423     * to the ANT task.
424     * @return the properties for property expansion expansion
425     * @throws BuildException if an error occurs
426     */
427    private Properties createOverridingProperties() {
428        final Properties returnValue = new Properties();
429
430        // Load the properties file if specified
431        if (properties != null) {
432            try (InputStream inStream = Files.newInputStream(properties.toPath())) {
433                returnValue.load(inStream);
434            }
435            catch (final IOException ex) {
436                throw new BuildException("Error loading Properties file '"
437                        + properties + "'", ex, getLocation());
438            }
439        }
440
441        // override with Ant properties like ${basedir}
442        final Map<String, Object> antProps = getProject().getProperties();
443        for (Map.Entry<String, Object> entry : antProps.entrySet()) {
444            final String value = String.valueOf(entry.getValue());
445            returnValue.setProperty(entry.getKey(), value);
446        }
447
448        // override with properties specified in subelements
449        for (Property p : overrideProps) {
450            returnValue.setProperty(p.getKey(), p.getValue());
451        }
452
453        return returnValue;
454    }
455
456    /**
457     * Return the list of listeners set in this task.
458     * @return the list of listeners.
459     */
460    private AuditListener[] getListeners() {
461        final int formatterCount = Math.max(1, formatters.size());
462
463        final AuditListener[] listeners = new AuditListener[formatterCount];
464
465        // formatters
466        try {
467            if (formatters.isEmpty()) {
468                final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
469                final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
470                listeners[0] = new DefaultLogger(debug, AutomaticBean.OutputStreamOptions.CLOSE,
471                        err, AutomaticBean.OutputStreamOptions.CLOSE);
472            }
473            else {
474                for (int i = 0; i < formatterCount; i++) {
475                    final Formatter formatter = formatters.get(i);
476                    listeners[i] = formatter.createListener(this);
477                }
478            }
479        }
480        catch (IOException ex) {
481            throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
482                    + "formatters {%s}.", formatters), ex);
483        }
484        return listeners;
485    }
486
487    /**
488     * Returns the list of files (full path name) to process.
489     * @return the list of files included via the fileName, filesets and paths.
490     */
491    private List<File> getFilesToCheck() {
492        final List<File> allFiles = new ArrayList<>();
493        if (fileName != null) {
494            // oops we've got an additional one to process, don't
495            // forget it. No sweat, it's fully resolved via the setter.
496            log("Adding standalone file for audit", Project.MSG_VERBOSE);
497            allFiles.add(new File(fileName));
498        }
499
500        final List<File> filesFromFileSets = scanFileSets();
501        allFiles.addAll(filesFromFileSets);
502
503        final List<File> filesFromPaths = scanPaths();
504        allFiles.addAll(filesFromPaths);
505
506        return allFiles;
507    }
508
509    /**
510     * Retrieves all files from the defined paths.
511     * @return a list of files defined via paths.
512     */
513    private List<File> scanPaths() {
514        final List<File> allFiles = new ArrayList<>();
515
516        for (int i = 0; i < paths.size(); i++) {
517            final Path currentPath = paths.get(i);
518            final List<File> pathFiles = scanPath(currentPath, i + 1);
519            allFiles.addAll(pathFiles);
520        }
521
522        return allFiles;
523    }
524
525    /**
526     * Scans the given path and retrieves all files for the given path.
527     *
528     * @param path      A path to scan.
529     * @param pathIndex The index of the given path. Used in log messages only.
530     * @return A list of files, extracted from the given path.
531     */
532    private List<File> scanPath(Path path, int pathIndex) {
533        final String[] resources = path.list();
534        log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
535        final List<File> allFiles = new ArrayList<>();
536        int concreteFilesCount = 0;
537
538        for (String resource : resources) {
539            final File file = new File(resource);
540            if (file.isFile()) {
541                concreteFilesCount++;
542                allFiles.add(file);
543            }
544            else {
545                final DirectoryScanner scanner = new DirectoryScanner();
546                scanner.setBasedir(file);
547                scanner.scan();
548                final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
549                allFiles.addAll(scannedFiles);
550            }
551        }
552
553        if (concreteFilesCount > 0) {
554            log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
555                pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
556        }
557
558        return allFiles;
559    }
560
561    /**
562     * Returns the list of files (full path name) to process.
563     * @return the list of files included via the filesets.
564     */
565    protected List<File> scanFileSets() {
566        final List<File> allFiles = new ArrayList<>();
567
568        for (int i = 0; i < fileSets.size(); i++) {
569            final FileSet fileSet = fileSets.get(i);
570            final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
571            final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
572            allFiles.addAll(scannedFiles);
573        }
574
575        return allFiles;
576    }
577
578    /**
579     * Retrieves all matched files from the given scanner.
580     *
581     * @param scanner  A directory scanner. Note, that {@link DirectoryScanner#scan()}
582     *                 must be called before calling this method.
583     * @param logIndex A log entry index. Used only for log messages.
584     * @return A list of files, retrieved from the given scanner.
585     */
586    private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
587        final String[] fileNames = scanner.getIncludedFiles();
588        log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
589            logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
590
591        return Arrays.stream(fileNames)
592            .map(name -> scanner.getBasedir() + File.separator + name)
593            .map(File::new)
594            .collect(Collectors.toList());
595    }
596
597    /**
598     * Poor mans enumeration for the formatter types.
599     */
600    public static class FormatterType extends EnumeratedAttribute {
601
602        /** My possible values. */
603        private static final String[] VALUES = {E_XML, E_PLAIN};
604
605        @Override
606        public String[] getValues() {
607            return VALUES.clone();
608        }
609
610    }
611
612    /**
613     * Details about a formatter to be used.
614     */
615    public static class Formatter {
616
617        /** The formatter type. */
618        private FormatterType type;
619        /** The file to output to. */
620        private File toFile;
621        /** Whether or not the write to the named file. */
622        private boolean useFile = true;
623
624        /**
625         * Set the type of the formatter.
626         * @param type the type
627         */
628        public void setType(FormatterType type) {
629            this.type = type;
630        }
631
632        /**
633         * Set the file to output to.
634         * @param destination destination the file to output to
635         */
636        public void setTofile(File destination) {
637            toFile = destination;
638        }
639
640        /**
641         * Sets whether or not we write to a file if it is provided.
642         * @param use whether not not to use provided file.
643         */
644        public void setUseFile(boolean use) {
645            useFile = use;
646        }
647
648        /**
649         * Creates a listener for the formatter.
650         * @param task the task running
651         * @return a listener
652         * @throws IOException if an error occurs
653         */
654        public AuditListener createListener(Task task) throws IOException {
655            final AuditListener listener;
656            if (type != null
657                    && E_XML.equals(type.getValue())) {
658                listener = createXmlLogger(task);
659            }
660            else {
661                listener = createDefaultLogger(task);
662            }
663            return listener;
664        }
665
666        /**
667         * Creates default logger.
668         * @param task the task to possibly log to
669         * @return a DefaultLogger instance
670         * @throws IOException if an error occurs
671         */
672        private AuditListener createDefaultLogger(Task task)
673                throws IOException {
674            final AuditListener defaultLogger;
675            if (toFile == null || !useFile) {
676                defaultLogger = new DefaultLogger(
677                    new LogOutputStream(task, Project.MSG_DEBUG),
678                        AutomaticBean.OutputStreamOptions.CLOSE,
679                        new LogOutputStream(task, Project.MSG_ERR),
680                        AutomaticBean.OutputStreamOptions.CLOSE
681                );
682            }
683            else {
684                final OutputStream infoStream = Files.newOutputStream(toFile.toPath());
685                defaultLogger =
686                        new DefaultLogger(infoStream, AutomaticBean.OutputStreamOptions.CLOSE,
687                                infoStream, AutomaticBean.OutputStreamOptions.NONE);
688            }
689            return defaultLogger;
690        }
691
692        /**
693         * Creates XML logger.
694         * @param task the task to possibly log to
695         * @return an XMLLogger instance
696         * @throws IOException if an error occurs
697         */
698        private AuditListener createXmlLogger(Task task) throws IOException {
699            final AuditListener xmlLogger;
700            if (toFile == null || !useFile) {
701                xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO),
702                        AutomaticBean.OutputStreamOptions.CLOSE);
703            }
704            else {
705                xmlLogger = new XMLLogger(Files.newOutputStream(toFile.toPath()),
706                        AutomaticBean.OutputStreamOptions.CLOSE);
707            }
708            return xmlLogger;
709        }
710
711    }
712
713    /**
714     * Represents a property that consists of a key and value.
715     */
716    public static class Property {
717
718        /** The property key. */
719        private String key;
720        /** The property value. */
721        private String value;
722
723        /**
724         * Gets key.
725         * @return the property key
726         */
727        public String getKey() {
728            return key;
729        }
730
731        /**
732         * Sets key.
733         * @param key sets the property key
734         */
735        public void setKey(String key) {
736            this.key = key;
737        }
738
739        /**
740         * Gets value.
741         * @return the property value
742         */
743        public String getValue() {
744            return value;
745        }
746
747        /**
748         * Sets value.
749         * @param value set the property value
750         */
751        public void setValue(String value) {
752            this.value = value;
753        }
754
755        /**
756         * Sets the property value from a File.
757         * @param file set the property value from a File
758         */
759        public void setFile(File file) {
760            value = file.getAbsolutePath();
761        }
762
763    }
764
765    /** Represents a custom listener. */
766    public static class Listener {
767
768        /** Class name of the listener class. */
769        private String className;
770
771        /**
772         * Gets class name.
773         * @return the class name
774         */
775        public String getClassname() {
776            return className;
777        }
778
779        /**
780         * Sets class name.
781         * @param name set the class name
782         */
783        public void setClassname(String name) {
784            className = name;
785        }
786
787    }
788
789}