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.net.URL;
028import java.util.ArrayList;
029import java.util.Arrays;
030import java.util.List;
031import java.util.Locale;
032import java.util.Map;
033import java.util.Properties;
034import java.util.ResourceBundle;
035import java.util.stream.Collectors;
036
037import org.apache.tools.ant.AntClassLoader;
038import org.apache.tools.ant.BuildException;
039import org.apache.tools.ant.DirectoryScanner;
040import org.apache.tools.ant.Project;
041import org.apache.tools.ant.Task;
042import org.apache.tools.ant.taskdefs.LogOutputStream;
043import org.apache.tools.ant.types.EnumeratedAttribute;
044import org.apache.tools.ant.types.FileSet;
045import org.apache.tools.ant.types.Path;
046import org.apache.tools.ant.types.Reference;
047
048import com.google.common.io.Closeables;
049import com.puppycrawl.tools.checkstyle.Checker;
050import com.puppycrawl.tools.checkstyle.ConfigurationLoader;
051import com.puppycrawl.tools.checkstyle.DefaultLogger;
052import com.puppycrawl.tools.checkstyle.ModuleFactory;
053import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
054import com.puppycrawl.tools.checkstyle.PropertiesExpander;
055import com.puppycrawl.tools.checkstyle.XMLLogger;
056import com.puppycrawl.tools.checkstyle.api.AuditListener;
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 * @author Oliver Burn
067 */
068public class CheckstyleAntTask extends Task {
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 configLocation;
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 omit ignored modules - some modules may log tove
115     * their severity depending on their configuration (e.g. WriteTag) so
116     * need to be included
117     */
118    private boolean omitIgnoredModules = true;
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 file the configuration file to use
234     */
235    public void setConfig(File file) {
236        setConfigLocation(file.getAbsolutePath());
237    }
238
239    /**
240     * Sets URL to the configuration.
241     * @param url the URL of the configuration to use
242     * @deprecated please use setConfigUrl instead
243     */
244    // -@cs[AbbreviationAsWordInName] Should be removed at 7.0 version,
245    // we keep for some time to avoid braking compatibility.
246    @Deprecated
247    public void setConfigURL(URL url) {
248        setConfigUrl(url);
249    }
250
251    /**
252     * Sets URL to the configuration.
253     * @param url the URL of the configuration to use
254     */
255    public void setConfigUrl(URL url) {
256        setConfigLocation(url.toExternalForm());
257    }
258
259    /**
260     * Sets the location of the configuration.
261     * @param location the location, which is either a
262     */
263    private void setConfigLocation(String location) {
264        if (configLocation != null) {
265            throw new BuildException("Attributes 'config' and 'configURL' "
266                    + "must not be set at the same time");
267        }
268        configLocation = location;
269    }
270
271    /**
272     * Sets flag - whether to omit ignored modules.
273     * @param omit whether to omit ignored modules
274     */
275    public void setOmitIgnoredModules(boolean omit) {
276        omitIgnoredModules = omit;
277    }
278
279    ////////////////////////////////////////////////////////////////////////////
280    // Setters for Root Module's configuration attributes
281    ////////////////////////////////////////////////////////////////////////////
282
283    /**
284     * Sets a properties file for use instead
285     * of individually setting them.
286     * @param props the properties File to use
287     */
288    public void setProperties(File props) {
289        properties = props;
290    }
291
292    ////////////////////////////////////////////////////////////////////////////
293    // The doers
294    ////////////////////////////////////////////////////////////////////////////
295
296    @Override
297    public void execute() {
298        final long startTime = System.currentTimeMillis();
299
300        try {
301            // output version info in debug mode
302            final ResourceBundle compilationProperties = ResourceBundle
303                    .getBundle("checkstylecompilation", Locale.ROOT);
304            final String version = compilationProperties
305                    .getString("checkstyle.compile.version");
306            final String compileTimestamp = compilationProperties
307                    .getString("checkstyle.compile.timestamp");
308            log("checkstyle version " + version, Project.MSG_VERBOSE);
309            log("compiled on " + compileTimestamp, Project.MSG_VERBOSE);
310
311            // Check for no arguments
312            if (fileName == null
313                    && fileSets.isEmpty()
314                    && paths.isEmpty()) {
315                throw new BuildException(
316                        "Must specify at least one of 'file' or nested 'fileset' or 'path'.",
317                        getLocation());
318            }
319            if (configLocation == null) {
320                throw new BuildException("Must specify 'config'.", getLocation());
321            }
322            realExecute(version);
323        }
324        finally {
325            final long endTime = System.currentTimeMillis();
326            log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
327                Project.MSG_VERBOSE);
328        }
329    }
330
331    /**
332     * Helper implementation to perform execution.
333     * @param checkstyleVersion Checkstyle compile version.
334     */
335    private void realExecute(String checkstyleVersion) {
336        // Create the root module
337        RootModule rootModule = null;
338        try {
339            rootModule = createRootModule();
340
341            // setup the listeners
342            final AuditListener[] listeners = getListeners();
343            for (AuditListener element : listeners) {
344                rootModule.addListener(element);
345            }
346            final SeverityLevelCounter warningCounter =
347                new SeverityLevelCounter(SeverityLevel.WARNING);
348            rootModule.addListener(warningCounter);
349
350            processFiles(rootModule, warningCounter, checkstyleVersion);
351        }
352        finally {
353            destroyRootModule(rootModule);
354        }
355    }
356
357    /**
358     * Destroy root module. This method exists only due to bug in cobertura library
359     * https://github.com/cobertura/cobertura/issues/170
360     * @param rootModule Root module that was used to process files
361     */
362    private static void destroyRootModule(RootModule rootModule) {
363        if (rootModule != null) {
364            rootModule.destroy();
365        }
366    }
367
368    /**
369     * Scans and processes files by means given root module.
370     * @param rootModule Root module to process files
371     * @param warningCounter Root Module's counter of warnings
372     * @param checkstyleVersion Checkstyle compile version
373     */
374    private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
375            final String checkstyleVersion) {
376        final long startTime = System.currentTimeMillis();
377        final List<File> files = getFilesToCheck();
378        final long endTime = System.currentTimeMillis();
379        log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
380            Project.MSG_VERBOSE);
381
382        log("Running Checkstyle " + checkstyleVersion + " on " + files.size()
383                + " files", Project.MSG_INFO);
384        log("Using configuration " + configLocation, Project.MSG_VERBOSE);
385
386        final int numErrs;
387
388        try {
389            final long processingStartTime = System.currentTimeMillis();
390            numErrs = rootModule.process(files);
391            final long processingEndTime = System.currentTimeMillis();
392            log("To process the files took " + (processingEndTime - processingStartTime)
393                + TIME_SUFFIX, Project.MSG_VERBOSE);
394        }
395        catch (CheckstyleException ex) {
396            throw new BuildException("Unable to process files: " + files, ex);
397        }
398        final int numWarnings = warningCounter.getCount();
399        final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
400
401        // Handle the return status
402        if (!okStatus) {
403            final String failureMsg =
404                    "Got " + numErrs + " errors and " + numWarnings
405                            + " warnings.";
406            if (failureProperty != null) {
407                getProject().setProperty(failureProperty, failureMsg);
408            }
409
410            if (failOnViolation) {
411                throw new BuildException(failureMsg, getLocation());
412            }
413        }
414    }
415
416    /**
417     * Creates new instance of the root module.
418     * @return new instance of the root module
419     */
420    private RootModule createRootModule() {
421        final RootModule rootModule;
422        try {
423            final Properties props = createOverridingProperties();
424            final Configuration config =
425                ConfigurationLoader.loadConfiguration(
426                    configLocation,
427                    new PropertiesExpander(props),
428                    omitIgnoredModules);
429
430            final ClassLoader moduleClassLoader =
431                Checker.class.getClassLoader();
432
433            final ModuleFactory factory = new PackageObjectFactory(
434                    Checker.class.getPackage().getName() + ".", moduleClassLoader);
435
436            rootModule = (RootModule) factory.createModule(config.getName());
437            rootModule.setModuleClassLoader(moduleClassLoader);
438
439            if (rootModule instanceof Checker) {
440                final ClassLoader loader = new AntClassLoader(getProject(),
441                        classpath);
442
443                ((Checker) rootModule).setClassLoader(loader);
444            }
445
446            rootModule.configure(config);
447        }
448        catch (final CheckstyleException ex) {
449            throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
450                    + "configLocation {%s}, classpath {%s}.", configLocation, classpath), ex);
451        }
452        return rootModule;
453    }
454
455    /**
456     * Create the Properties object based on the arguments specified
457     * to the ANT task.
458     * @return the properties for property expansion expansion
459     * @throws BuildException if an error occurs
460     */
461    private Properties createOverridingProperties() {
462        final Properties returnValue = new Properties();
463
464        // Load the properties file if specified
465        if (properties != null) {
466            FileInputStream inStream = null;
467            try {
468                inStream = new FileInputStream(properties);
469                returnValue.load(inStream);
470            }
471            catch (final IOException ex) {
472                throw new BuildException("Error loading Properties file '"
473                        + properties + "'", ex, getLocation());
474            }
475            finally {
476                Closeables.closeQuietly(inStream);
477            }
478        }
479
480        // override with Ant properties like ${basedir}
481        final Map<String, Object> antProps = getProject().getProperties();
482        for (Map.Entry<String, Object> entry : antProps.entrySet()) {
483            final String value = String.valueOf(entry.getValue());
484            returnValue.setProperty(entry.getKey(), value);
485        }
486
487        // override with properties specified in subelements
488        for (Property p : overrideProps) {
489            returnValue.setProperty(p.getKey(), p.getValue());
490        }
491
492        return returnValue;
493    }
494
495    /**
496     * Return the list of listeners set in this task.
497     * @return the list of listeners.
498     */
499    private AuditListener[] getListeners() {
500        final int formatterCount = Math.max(1, formatters.size());
501
502        final AuditListener[] listeners = new AuditListener[formatterCount];
503
504        // formatters
505        try {
506            if (formatters.isEmpty()) {
507                final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
508                final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
509                listeners[0] = new DefaultLogger(debug, true, err, true);
510            }
511            else {
512                for (int i = 0; i < formatterCount; i++) {
513                    final Formatter formatter = formatters.get(i);
514                    listeners[i] = formatter.createListener(this);
515                }
516            }
517        }
518        catch (IOException ex) {
519            throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
520                    + "formatters {%s}.", formatters), ex);
521        }
522        return listeners;
523    }
524
525    /**
526     * Returns the list of files (full path name) to process.
527     * @return the list of files included via the fileName, filesets and paths.
528     */
529    protected List<File> getFilesToCheck() {
530        final List<File> allFiles = new ArrayList<>();
531        if (fileName != null) {
532            // oops we've got an additional one to process, don't
533            // forget it. No sweat, it's fully resolved via the setter.
534            log("Adding standalone file for audit", Project.MSG_VERBOSE);
535            allFiles.add(new File(fileName));
536        }
537
538        final List<File> filesFromFileSets = scanFileSets();
539        allFiles.addAll(filesFromFileSets);
540
541        final List<File> filesFromPaths = scanPaths();
542        allFiles.addAll(filesFromPaths);
543
544        return allFiles;
545    }
546
547    /**
548     * Retrieves all files from the defined paths.
549     * @return a list of files defined via paths.
550     */
551    private List<File> scanPaths() {
552        final List<File> allFiles = new ArrayList<>();
553
554        for (int i = 0; i < paths.size(); i++) {
555            final Path currentPath = paths.get(i);
556            final List<File> pathFiles = scanPath(currentPath, i + 1);
557            allFiles.addAll(pathFiles);
558        }
559
560        return allFiles;
561    }
562
563    /**
564     * Scans the given path and retrieves all files for the given path.
565     *
566     * @param path      A path to scan.
567     * @param pathIndex The index of the given path. Used in log messages only.
568     * @return A list of files, extracted from the given path.
569     */
570    private List<File> scanPath(Path path, int pathIndex) {
571        final String[] resources = path.list();
572        log(pathIndex + ") Scanning path " + path, Project.MSG_VERBOSE);
573        final List<File> allFiles = new ArrayList<>();
574        int concreteFilesCount = 0;
575
576        for (String resource : resources) {
577            final File file = new File(resource);
578            if (file.isFile()) {
579                concreteFilesCount++;
580                allFiles.add(file);
581            }
582            else {
583                final DirectoryScanner scanner = new DirectoryScanner();
584                scanner.setBasedir(file);
585                scanner.scan();
586                final List<File> scannedFiles = retrieveAllScannedFiles(scanner, pathIndex);
587                allFiles.addAll(scannedFiles);
588            }
589        }
590
591        if (concreteFilesCount > 0) {
592            log(String.format(Locale.ROOT, "%d) Adding %d files from path %s",
593                pathIndex, concreteFilesCount, path), Project.MSG_VERBOSE);
594        }
595
596        return allFiles;
597    }
598
599    /**
600     * Returns the list of files (full path name) to process.
601     * @return the list of files included via the filesets.
602     */
603    protected List<File> scanFileSets() {
604        final List<File> allFiles = new ArrayList<>();
605
606        for (int i = 0; i < fileSets.size(); i++) {
607            final FileSet fileSet = fileSets.get(i);
608            final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
609            scanner.scan();
610
611            final List<File> scannedFiles = retrieveAllScannedFiles(scanner, i);
612            allFiles.addAll(scannedFiles);
613        }
614
615        return allFiles;
616    }
617
618    /**
619     * Retrieves all matched files from the given scanner.
620     *
621     * @param scanner  A directory scanner. Note, that {@link DirectoryScanner#scan()}
622     *                 must be called before calling this method.
623     * @param logIndex A log entry index. Used only for log messages.
624     * @return A list of files, retrieved from the given scanner.
625     */
626    private List<File> retrieveAllScannedFiles(DirectoryScanner scanner, int logIndex) {
627        final String[] fileNames = scanner.getIncludedFiles();
628        log(String.format(Locale.ROOT, "%d) Adding %d files from directory %s",
629            logIndex, fileNames.length, scanner.getBasedir()), Project.MSG_VERBOSE);
630
631        return Arrays.stream(fileNames)
632            .map(name -> scanner.getBasedir() + File.separator + name)
633            .map(File::new)
634            .collect(Collectors.toList());
635    }
636
637    /**
638     * Poor mans enumeration for the formatter types.
639     * @author Oliver Burn
640     */
641    public static class FormatterType extends EnumeratedAttribute {
642        /** My possible values. */
643        private static final String[] VALUES = {E_XML, E_PLAIN};
644
645        @Override
646        public String[] getValues() {
647            return VALUES.clone();
648        }
649    }
650
651    /**
652     * Details about a formatter to be used.
653     * @author Oliver Burn
654     */
655    public static class Formatter {
656        /** The formatter type. */
657        private FormatterType type;
658        /** The file to output to. */
659        private File toFile;
660        /** Whether or not the write to the named file. */
661        private boolean useFile = true;
662
663        /**
664         * Set the type of the formatter.
665         * @param type the type
666         */
667        public void setType(FormatterType type) {
668            this.type = type;
669        }
670
671        /**
672         * Set the file to output to.
673         * @param destination destination the file to output to
674         */
675        public void setTofile(File destination) {
676            toFile = destination;
677        }
678
679        /**
680         * Sets whether or not we write to a file if it is provided.
681         * @param use whether not not to use provided file.
682         */
683        public void setUseFile(boolean use) {
684            useFile = use;
685        }
686
687        /**
688         * Creates a listener for the formatter.
689         * @param task the task running
690         * @return a listener
691         * @throws IOException if an error occurs
692         */
693        public AuditListener createListener(Task task) throws IOException {
694            final AuditListener listener;
695            if (type != null
696                    && E_XML.equals(type.getValue())) {
697                listener = createXmlLogger(task);
698            }
699            else {
700                listener = createDefaultLogger(task);
701            }
702            return listener;
703        }
704
705        /**
706         * Creates default logger.
707         * @param task the task to possibly log to
708         * @return a DefaultLogger instance
709         * @throws IOException if an error occurs
710         */
711        private AuditListener createDefaultLogger(Task task)
712                throws IOException {
713            final AuditListener defaultLogger;
714            if (toFile == null || !useFile) {
715                defaultLogger = new DefaultLogger(
716                    new LogOutputStream(task, Project.MSG_DEBUG),
717                    true, new LogOutputStream(task, Project.MSG_ERR), true);
718            }
719            else {
720                final FileOutputStream infoStream = new FileOutputStream(toFile);
721                defaultLogger = new DefaultLogger(infoStream, true, infoStream, false);
722            }
723            return defaultLogger;
724        }
725
726        /**
727         * Creates XML logger.
728         * @param task the task to possibly log to
729         * @return an XMLLogger instance
730         * @throws IOException if an error occurs
731         */
732        private AuditListener createXmlLogger(Task task) throws IOException {
733            final AuditListener xmlLogger;
734            if (toFile == null || !useFile) {
735                xmlLogger = new XMLLogger(new LogOutputStream(task, Project.MSG_INFO), true);
736            }
737            else {
738                xmlLogger = new XMLLogger(new FileOutputStream(toFile), true);
739            }
740            return xmlLogger;
741        }
742    }
743
744    /**
745     * Represents a property that consists of a key and value.
746     */
747    public static class Property {
748        /** The property key. */
749        private String key;
750        /** The property value. */
751        private String value;
752
753        /**
754         * Gets key.
755         * @return the property key
756         */
757        public String getKey() {
758            return key;
759        }
760
761        /**
762         * Sets key.
763         * @param key sets the property key
764         */
765        public void setKey(String key) {
766            this.key = key;
767        }
768
769        /**
770         * Gets value.
771         * @return the property value
772         */
773        public String getValue() {
774            return value;
775        }
776
777        /**
778         * Sets value.
779         * @param value set the property value
780         */
781        public void setValue(String value) {
782            this.value = value;
783        }
784
785        /**
786         * Sets the property value from a File.
787         * @param file set the property value from a File
788         */
789        public void setFile(File file) {
790            value = file.getAbsolutePath();
791        }
792    }
793
794    /** Represents a custom listener. */
795    public static class Listener {
796        /** Class name of the listener class. */
797        private String className;
798
799        /**
800         * Gets class name.
801         * @return the class name
802         */
803        public String getClassname() {
804            return className;
805        }
806
807        /**
808         * Sets class name.
809         * @param name set the class name
810         */
811        public void setClassname(String name) {
812            className = name;
813        }
814    }
815}