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