001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 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.FileInputStream;
024import java.io.FileOutputStream;
025import java.io.IOException;
026import java.io.OutputStream;
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.google.common.io.Closeables;
048import com.puppycrawl.tools.checkstyle.Checker;
049import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
050import com.puppycrawl.tools.checkstyle.DefaultLogger;
051import com.puppycrawl.tools.checkstyle.ModuleFactory;
052import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
053import com.puppycrawl.tools.checkstyle.PropertiesExpander;
054import com.puppycrawl.tools.checkstyle.XMLLogger;
055import com.puppycrawl.tools.checkstyle.api.AuditListener;
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 * @author Oliver Burn
066 */
067public class CheckstyleAntTask extends Task {
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     */
234    public void setConfig(String configuration) {
235        if (config != null) {
236            throw new BuildException("Attribute 'config' has already been set");
237        }
238        config = configuration;
239    }
240
241    /**
242     * Sets flag - whether to execute ignored modules.
243     * @param omit whether to execute ignored modules
244     */
245    public void setExecuteIgnoredModules(boolean omit) {
246        executeIgnoredModules = omit;
247    }
248
249    ////////////////////////////////////////////////////////////////////////////
250    // Setters for Root Module's configuration attributes
251    ////////////////////////////////////////////////////////////////////////////
252
253    /**
254     * Sets a properties file for use instead
255     * of individually setting them.
256     * @param props the properties File to use
257     */
258    public void setProperties(File props) {
259        properties = props;
260    }
261
262    ////////////////////////////////////////////////////////////////////////////
263    // The doers
264    ////////////////////////////////////////////////////////////////////////////
265
266    @Override
267    public void execute() {
268        final long startTime = System.currentTimeMillis();
269
270        try {
271            // output version info in debug mode
272            final ResourceBundle compilationProperties = ResourceBundle
273                    .getBundle("checkstylecompilation", Locale.ROOT);
274            final String version = compilationProperties
275                    .getString("checkstyle.compile.version");
276            final String compileTimestamp = compilationProperties
277                    .getString("checkstyle.compile.timestamp");
278            log("checkstyle version " + version, Project.MSG_VERBOSE);
279            log("compiled on " + compileTimestamp, Project.MSG_VERBOSE);
280
281            // Check for no arguments
282            if (fileName == null
283                    && fileSets.isEmpty()
284                    && paths.isEmpty()) {
285                throw new BuildException(
286                        "Must specify at least one of 'file' or nested 'fileset' or 'path'.",
287                        getLocation());
288            }
289            if (config == null) {
290                throw new BuildException("Must specify 'config'.", getLocation());
291            }
292            realExecute(version);
293        }
294        finally {
295            final long endTime = System.currentTimeMillis();
296            log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
297                Project.MSG_VERBOSE);
298        }
299    }
300
301    /**
302     * Helper implementation to perform execution.
303     * @param checkstyleVersion Checkstyle compile version.
304     */
305    private void realExecute(String checkstyleVersion) {
306        // Create the root module
307        RootModule rootModule = null;
308        try {
309            rootModule = createRootModule();
310
311            // setup the listeners
312            final AuditListener[] listeners = getListeners();
313            for (AuditListener element : listeners) {
314                rootModule.addListener(element);
315            }
316            final SeverityLevelCounter warningCounter =
317                new SeverityLevelCounter(SeverityLevel.WARNING);
318            rootModule.addListener(warningCounter);
319
320            processFiles(rootModule, warningCounter, checkstyleVersion);
321        }
322        finally {
323            destroyRootModule(rootModule);
324        }
325    }
326
327    /**
328     * Destroy root module. This method exists only due to bug in cobertura library
329     * https://github.com/cobertura/cobertura/issues/170
330     * @param rootModule Root module that was used to process files
331     */
332    private static void destroyRootModule(RootModule rootModule) {
333        if (rootModule != null) {
334            rootModule.destroy();
335        }
336    }
337
338    /**
339     * Scans and processes files by means given root module.
340     * @param rootModule Root module to process files
341     * @param warningCounter Root Module's counter of warnings
342     * @param checkstyleVersion Checkstyle compile version
343     */
344    private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
345            final String checkstyleVersion) {
346        final long startTime = System.currentTimeMillis();
347        final List<File> files = getFilesToCheck();
348        final long endTime = System.currentTimeMillis();
349        log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
350            Project.MSG_VERBOSE);
351
352        log("Running Checkstyle " + checkstyleVersion + " on " + files.size()
353                + " files", Project.MSG_INFO);
354        log("Using configuration " + config, Project.MSG_VERBOSE);
355
356        final int numErrs;
357
358        try {
359            final long processingStartTime = System.currentTimeMillis();
360            numErrs = rootModule.process(files);
361            final long processingEndTime = System.currentTimeMillis();
362            log("To process the files took " + (processingEndTime - processingStartTime)
363                + TIME_SUFFIX, Project.MSG_VERBOSE);
364        }
365        catch (CheckstyleException ex) {
366            throw new BuildException("Unable to process files: " + files, ex);
367        }
368        final int numWarnings = warningCounter.getCount();
369        final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
370
371        // Handle the return status
372        if (!okStatus) {
373            final String failureMsg =
374                    "Got " + numErrs + " errors and " + numWarnings
375                            + " warnings.";
376            if (failureProperty != null) {
377                getProject().setProperty(failureProperty, failureMsg);
378            }
379
380            if (failOnViolation) {
381                throw new BuildException(failureMsg, getLocation());
382            }
383        }
384    }
385
386    /**
387     * Creates new instance of the root module.
388     * @return new instance of the root module
389     */
390    private RootModule createRootModule() {
391        final RootModule rootModule;
392        try {
393            final Properties props = createOverridingProperties();
394            final Configuration configuration =
395                ConfigurationLoader.loadConfiguration(
396                    config,
397                    new PropertiesExpander(props),
398                    !executeIgnoredModules);
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            FileInputStream inStream = null;
437            try {
438                inStream = new FileInputStream(properties);
439                returnValue.load(inStream);
440            }
441            catch (final IOException ex) {
442                throw new BuildException("Error loading Properties file '"
443                        + properties + "'", ex, getLocation());
444            }
445            finally {
446                Closeables.closeQuietly(inStream);
447            }
448        }
449
450        // override with Ant properties like ${basedir}
451        final Map<String, Object> antProps = getProject().getProperties();
452        for (Map.Entry<String, Object> entry : antProps.entrySet()) {
453            final String value = String.valueOf(entry.getValue());
454            returnValue.setProperty(entry.getKey(), value);
455        }
456
457        // override with properties specified in subelements
458        for (Property p : overrideProps) {
459            returnValue.setProperty(p.getKey(), p.getValue());
460        }
461
462        return returnValue;
463    }
464
465    /**
466     * Return the list of listeners set in this task.
467     * @return the list of listeners.
468     */
469    private AuditListener[] getListeners() {
470        final int formatterCount = Math.max(1, formatters.size());
471
472        final AuditListener[] listeners = new AuditListener[formatterCount];
473
474        // formatters
475        try {
476            if (formatters.isEmpty()) {
477                final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
478                final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
479                listeners[0] = new DefaultLogger(debug, true, err, true);
480            }
481            else {
482                for (int i = 0; i < formatterCount; i++) {
483                    final Formatter formatter = formatters.get(i);
484                    listeners[i] = formatter.createListener(this);
485                }
486            }
487        }
488        catch (IOException ex) {
489            throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
490                    + "formatters {%s}.", formatters), ex);
491        }
492        return listeners;
493    }
494
495    /**
496     * Returns the list of files (full path name) to process.
497     * @return the list of files included via the fileName, filesets and paths.
498     */
499    protected List<File> getFilesToCheck() {
500        final List<File> allFiles = new ArrayList<>();
501        if (fileName != null) {
502            // oops we've got an additional one to process, don't
503            // forget it. No sweat, it's fully resolved via the setter.
504            log("Adding standalone file for audit", Project.MSG_VERBOSE);
505            allFiles.add(new File(fileName));
506        }
507
508        final List<File> filesFromFileSets = scanFileSets();
509        allFiles.addAll(filesFromFileSets);
510
511        final List<File> filesFromPaths = scanPaths();
512        allFiles.addAll(filesFromPaths);
513
514        return allFiles;
515    }
516
517    /**
518     * Retrieves all files from the defined paths.
519     * @return a list of files defined via paths.
520     */
521    private List<File> scanPaths() {
522        final List<File> allFiles = new ArrayList<>();
523
524        for (int i = 0; i < paths.size(); i++) {
525            final Path currentPath = paths.get(i);
526            final List<File> pathFiles = scanPath(currentPath, i + 1);
527            allFiles.addAll(pathFiles);
528        }
529
530        return allFiles;
531    }
532
533    /**
534     * Scans the given path and retrieves all files for the given path.
535     *
536     * @param path      A path to scan.
537     * @param pathIndex The index of the given path. Used in log messages only.
538     * @return A list of files, extracted from the given path.
539     */
540    private List<File> scanPath(Path path, int pathIndex) {
541        final String[] resources = path.list();
542        log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
543        final List<File> allFiles = new ArrayList<>();
544        int concreteFilesCount = 0;
545
546        for (String resource : resources) {
547            final File file = new File(resource);
548            if (file.isFile()) {
549                concreteFilesCount++;
550                allFiles.add(file);
551            }
552            else {
553                final DirectoryScanner scanner = new DirectoryScanner();
554                scanner.setBasedir(file);
555                scanner.scan();
556                final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
557                allFiles.addAll(scannedFiles);
558            }
559        }
560
561        if (concreteFilesCount > 0) {
562            log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
563                pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
564        }
565
566        return allFiles;
567    }
568
569    /**
570     * Returns the list of files (full path name) to process.
571     * @return the list of files included via the filesets.
572     */
573    protected List<File> scanFileSets() {
574        final List<File> allFiles = new ArrayList<>();
575
576        for (int i = 0; i < fileSets.size(); i++) {
577            final FileSet fileSet = fileSets.get(i);
578            final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
579            scanner.scan();
580
581            final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
582            allFiles.addAll(scannedFiles);
583        }
584
585        return allFiles;
586    }
587
588    /**
589     * Retrieves all matched files from the given scanner.
590     *
591     * @param scanner  A directory scanner. Note, that {@link DirectoryScanner#scan()}
592     *                 must be called before calling this method.
593     * @param logIndex A log entry index. Used only for log messages.
594     * @return A list of files, retrieved from the given scanner.
595     */
596    private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
597        final String[] fileNames = scanner.getIncludedFiles();
598        log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
599            logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
600
601        return Arrays.stream(fileNames)
602            .map(name -> scanner.getBasedir() + File.separator + name)
603            .map(File::new)
604            .collect(Collectors.toList());
605    }
606
607    /**
608     * Poor mans enumeration for the formatter types.
609     * @author Oliver Burn
610     */
611    public static class FormatterType extends EnumeratedAttribute {
612        /** My possible values. */
613        private static final String[] VALUES = {E_XML, E_PLAIN};
614
615        @Override
616        public String[] getValues() {
617            return VALUES.clone();
618        }
619    }
620
621    /**
622     * Details about a formatter to be used.
623     * @author Oliver Burn
624     */
625    public static class Formatter {
626        /** The formatter type. */
627        private FormatterType type;
628        /** The file to output to. */
629        private File toFile;
630        /** Whether or not the write to the named file. */
631        private boolean useFile = true;
632
633        /**
634         * Set the type of the formatter.
635         * @param type the type
636         */
637        public void setType(FormatterType type) {
638            this.type = type;
639        }
640
641        /**
642         * Set the file to output to.
643         * @param destination destination the file to output to
644         */
645        public void setTofile(File destination) {
646            toFile = destination;
647        }
648
649        /**
650         * Sets whether or not we write to a file if it is provided.
651         * @param use whether not not to use provided file.
652         */
653        public void setUseFile(boolean use) {
654            useFile = use;
655        }
656
657        /**
658         * Creates a listener for the formatter.
659         * @param task the task running
660         * @return a listener
661         * @throws IOException if an error occurs
662         */
663        public AuditListener createListener(Task task) throws IOException {
664            final AuditListener listener;
665            if (type != null
666                    && E_XML.equals(type.getValue())) {
667                listener = createXmlLogger(task);
668            }
669            else {
670                listener = createDefaultLogger(task);
671            }
672            return listener;
673        }
674
675        /**
676         * Creates default logger.
677         * @param task the task to possibly log to
678         * @return a DefaultLogger instance
679         * @throws IOException if an error occurs
680         */
681        private AuditListener createDefaultLogger(Task task)
682                throws IOException {
683            final AuditListener defaultLogger;
684            if (toFile == null || !useFile) {
685                defaultLogger = new DefaultLogger(
686                    new LogOutputStream(task, Project.MSG_DEBUG),
687                    true, new LogOutputStream(task, Project.MSG_ERR), true);
688            }
689            else {
690                final FileOutputStream infoStream = new FileOutputStream(toFile);
691                defaultLogger = new DefaultLogger(infoStream, true, infoStream, false);
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), true);
706            }
707            else {
708                xmlLogger = new XMLLogger(new FileOutputStream(toFile), true);
709            }
710            return xmlLogger;
711        }
712    }
713
714    /**
715     * Represents a property that consists of a key and value.
716     */
717    public static class Property {
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    /** Represents a custom listener. */
765    public static class Listener {
766        /** Class name of the listener class. */
767        private String className;
768
769        /**
770         * Gets class name.
771         * @return the class name
772         */
773        public String getClassname() {
774            return className;
775        }
776
777        /**
778         * Sets class name.
779         * @param name set the class name
780         */
781        public void setClassname(String name) {
782            className = name;
783        }
784    }
785}