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.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.Properties;
033import java.util.ResourceBundle;
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.google.common.io.Closeables;
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.XMLLogger;
054import com.puppycrawl.tools.checkstyle.api.AuditListener;
055import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
056import com.puppycrawl.tools.checkstyle.api.Configuration;
057import com.puppycrawl.tools.checkstyle.api.RootModule;
058import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
059import com.puppycrawl.tools.checkstyle.api.SeverityLevelCounter;
060
061/**
062 * An implementation of a ANT task for calling checkstyle. See the documentation
063 * of the task for usage.
064 * @author Oliver Burn
065 */
066public class CheckstyleAntTask extends Task {
067    /** Poor man's enum for an xml formatter. */
068    private static final String E_XML = "xml";
069    /** Poor man's enum for an plain formatter. */
070    private static final String E_PLAIN = "plain";
071
072    /** Suffix for time string. */
073    private static final String TIME_SUFFIX = " ms.";
074
075    /** Contains the filesets to process. */
076    private final List<FileSet> fileSets = new ArrayList<>();
077
078    /** Contains the formatters to log to. */
079    private final List<Formatter> formatters = new ArrayList<>();
080
081    /** Contains the Properties to override. */
082    private final List<Property> overrideProps = new ArrayList<>();
083
084    /** Class path to locate class files. */
085    private Path classpath;
086
087    /** Name of file to check. */
088    private String fileName;
089
090    /** Config file containing configuration. */
091    private String configLocation;
092
093    /** Whether to fail build on violations. */
094    private boolean failOnViolation = true;
095
096    /** Property to set on violations. */
097    private String failureProperty;
098
099    /** The name of the properties file. */
100    private File properties;
101
102    /** The maximum number of errors that are tolerated. */
103    private int maxErrors;
104
105    /** The maximum number of warnings that are tolerated. */
106    private int maxWarnings = Integer.MAX_VALUE;
107
108    /**
109     * Whether to omit ignored modules - some modules may log tove
110     * their severity depending on their configuration (e.g. WriteTag) so
111     * need to be included
112     */
113    private boolean omitIgnoredModules = true;
114
115    ////////////////////////////////////////////////////////////////////////////
116    // Setters for ANT specific attributes
117    ////////////////////////////////////////////////////////////////////////////
118
119    /**
120     * Tells this task to write failure message to the named property when there
121     * is a violation.
122     * @param propertyName the name of the property to set
123     *                      in the event of an failure.
124     */
125    public void setFailureProperty(String propertyName) {
126        failureProperty = propertyName;
127    }
128
129    /**
130     * Sets flag - whether to fail if a violation is found.
131     * @param fail whether to fail if a violation is found
132     */
133    public void setFailOnViolation(boolean fail) {
134        failOnViolation = fail;
135    }
136
137    /**
138     * Sets the maximum number of errors allowed. Default is 0.
139     * @param maxErrors the maximum number of errors allowed.
140     */
141    public void setMaxErrors(int maxErrors) {
142        this.maxErrors = maxErrors;
143    }
144
145    /**
146     * Sets the maximum number of warnings allowed. Default is
147     * {@link Integer#MAX_VALUE}.
148     * @param maxWarnings the maximum number of warnings allowed.
149     */
150    public void setMaxWarnings(int maxWarnings) {
151        this.maxWarnings = maxWarnings;
152    }
153
154    /**
155     * Adds set of files (nested fileset attribute).
156     * @param fileSet the file set to add
157     */
158    public void addFileset(FileSet fileSet) {
159        fileSets.add(fileSet);
160    }
161
162    /**
163     * Add a formatter.
164     * @param formatter the formatter to add for logging.
165     */
166    public void addFormatter(Formatter formatter) {
167        formatters.add(formatter);
168    }
169
170    /**
171     * Add an override property.
172     * @param property the property to add
173     */
174    public void addProperty(Property property) {
175        overrideProps.add(property);
176    }
177
178    /**
179     * Set the class path.
180     * @param classpath the path to locate classes
181     */
182    public void setClasspath(Path classpath) {
183        if (this.classpath == null) {
184            this.classpath = classpath;
185        }
186        else {
187            this.classpath.append(classpath);
188        }
189    }
190
191    /**
192     * Set the class path from a reference defined elsewhere.
193     * @param classpathRef the reference to an instance defining the classpath
194     */
195    public void setClasspathRef(Reference classpathRef) {
196        createClasspath().setRefid(classpathRef);
197    }
198
199    /**
200     * Creates classpath.
201     * @return a created path for locating classes
202     */
203    public Path createClasspath() {
204        if (classpath == null) {
205            classpath = new Path(getProject());
206        }
207        return classpath.createPath();
208    }
209
210    /**
211     * Sets file to be checked.
212     * @param file the file to be checked
213     */
214    public void setFile(File file) {
215        fileName = file.getAbsolutePath();
216    }
217
218    /**
219     * Sets configuration file.
220     * @param file the configuration file to use
221     */
222    public void setConfig(File file) {
223        setConfigLocation(file.getAbsolutePath());
224    }
225
226    /**
227     * Sets URL to the configuration.
228     * @param url the URL of the configuration to use
229     * @deprecated please use setConfigUrl instead
230     */
231    // -@cs[AbbreviationAsWordInName] Should be removed at 7.0 version,
232    // we keep for some time to avoid braking compatibility.
233    @Deprecated
234    public void setConfigURL(URL url) {
235        setConfigUrl(url);
236    }
237
238    /**
239     * Sets URL to the configuration.
240     * @param url the URL of the configuration to use
241     */
242    public void setConfigUrl(URL url) {
243        setConfigLocation(url.toExternalForm());
244    }
245
246    /**
247     * Sets the location of the configuration.
248     * @param location the location, which is either a
249     */
250    private void setConfigLocation(String location) {
251        if (configLocation != null) {
252            throw new BuildException("Attributes 'config' and 'configURL' "
253                    + "must not be set at the same time");
254        }
255        configLocation = location;
256    }
257
258    /**
259     * Sets flag - whether to omit ignored modules.
260     * @param omit whether to omit ignored modules
261     */
262    public void setOmitIgnoredModules(boolean omit) {
263        omitIgnoredModules = omit;
264    }
265
266    ////////////////////////////////////////////////////////////////////////////
267    // Setters for Root Module's configuration attributes
268    ////////////////////////////////////////////////////////////////////////////
269
270    /**
271     * Sets a properties file for use instead
272     * of individually setting them.
273     * @param props the properties File to use
274     */
275    public void setProperties(File props) {
276        properties = props;
277    }
278
279    ////////////////////////////////////////////////////////////////////////////
280    // The doers
281    ////////////////////////////////////////////////////////////////////////////
282
283    @Override
284    public void execute() {
285        final long startTime = System.currentTimeMillis();
286
287        try {
288            // output version info in debug mode
289            final ResourceBundle compilationProperties = ResourceBundle
290                    .getBundle("checkstylecompilation", Locale.ROOT);
291            final String version = compilationProperties
292                    .getString("checkstyle.compile.version");
293            final String compileTimestamp = compilationProperties
294                    .getString("checkstyle.compile.timestamp");
295            log("checkstyle version " + version, Project.MSG_VERBOSE);
296            log("compiled on " + compileTimestamp, Project.MSG_VERBOSE);
297
298            // Check for no arguments
299            if (fileName == null && fileSets.isEmpty()) {
300                throw new BuildException(
301                        "Must specify at least one of 'file' or nested 'fileset'.",
302                        getLocation());
303            }
304            if (configLocation == null) {
305                throw new BuildException("Must specify 'config'.", getLocation());
306            }
307            realExecute(version);
308        }
309        finally {
310            final long endTime = System.currentTimeMillis();
311            log("Total execution took " + (endTime - startTime) + TIME_SUFFIX,
312                Project.MSG_VERBOSE);
313        }
314    }
315
316    /**
317     * Helper implementation to perform execution.
318     * @param checkstyleVersion Checkstyle compile version.
319     */
320    private void realExecute(String checkstyleVersion) {
321        // Create the root module
322        RootModule rootModule = null;
323        try {
324            rootModule = createRootModule();
325
326            // setup the listeners
327            final AuditListener[] listeners = getListeners();
328            for (AuditListener element : listeners) {
329                rootModule.addListener(element);
330            }
331            final SeverityLevelCounter warningCounter =
332                new SeverityLevelCounter(SeverityLevel.WARNING);
333            rootModule.addListener(warningCounter);
334
335            processFiles(rootModule, warningCounter, checkstyleVersion);
336        }
337        finally {
338            destroyRootModule(rootModule);
339        }
340    }
341
342    /**
343     * Destroy root module. This method exists only due to bug in cobertura library
344     * https://github.com/cobertura/cobertura/issues/170
345     * @param rootModule Root module that was used to process files
346     */
347    private static void destroyRootModule(RootModule rootModule) {
348        if (rootModule != null) {
349            rootModule.destroy();
350        }
351    }
352
353    /**
354     * Scans and processes files by means given root module.
355     * @param rootModule Root module to process files
356     * @param warningCounter Root Module's counter of warnings
357     * @param checkstyleVersion Checkstyle compile version
358     */
359    private void processFiles(RootModule rootModule, final SeverityLevelCounter warningCounter,
360            final String checkstyleVersion) {
361        final long startTime = System.currentTimeMillis();
362        final List<File> files = scanFileSets();
363        final long endTime = System.currentTimeMillis();
364        log("To locate the files took " + (endTime - startTime) + TIME_SUFFIX,
365            Project.MSG_VERBOSE);
366
367        log("Running Checkstyle " + checkstyleVersion + " on " + files.size()
368                + " files", Project.MSG_INFO);
369        log("Using configuration " + configLocation, Project.MSG_VERBOSE);
370
371        final int numErrs;
372
373        try {
374            final long processingStartTime = System.currentTimeMillis();
375            numErrs = rootModule.process(files);
376            final long processingEndTime = System.currentTimeMillis();
377            log("To process the files took " + (processingEndTime - processingStartTime)
378                + TIME_SUFFIX, Project.MSG_VERBOSE);
379        }
380        catch (CheckstyleException ex) {
381            throw new BuildException("Unable to process files: " + files, ex);
382        }
383        final int numWarnings = warningCounter.getCount();
384        final boolean okStatus = numErrs <= maxErrors && numWarnings <= maxWarnings;
385
386        // Handle the return status
387        if (!okStatus) {
388            final String failureMsg =
389                    "Got " + numErrs + " errors and " + numWarnings
390                            + " warnings.";
391            if (failureProperty != null) {
392                getProject().setProperty(failureProperty, failureMsg);
393            }
394
395            if (failOnViolation) {
396                throw new BuildException(failureMsg, getLocation());
397            }
398        }
399    }
400
401    /**
402     * Creates new instance of the root module.
403     * @return new instance of the root module
404     */
405    private RootModule createRootModule() {
406        final RootModule rootModule;
407        try {
408            final Properties props = createOverridingProperties();
409            final Configuration config =
410                ConfigurationLoader.loadConfiguration(
411                    configLocation,
412                    new PropertiesExpander(props),
413                    omitIgnoredModules);
414
415            final ClassLoader moduleClassLoader =
416                Checker.class.getClassLoader();
417
418            final ModuleFactory factory = new PackageObjectFactory(
419                    Checker.class.getPackage().getName() + ".", moduleClassLoader);
420
421            rootModule = (RootModule) factory.createModule(config.getName());
422            rootModule.setModuleClassLoader(moduleClassLoader);
423
424            if (rootModule instanceof Checker) {
425                final ClassLoader loader = new AntClassLoader(getProject(),
426                        classpath);
427
428                ((Checker) rootModule).setClassLoader(loader);
429            }
430
431            rootModule.configure(config);
432        }
433        catch (final CheckstyleException ex) {
434            throw new BuildException(String.format(Locale.ROOT, "Unable to create Root Module: "
435                    + "configLocation {%s}, classpath {%s}.", configLocation, classpath), ex);
436        }
437        return rootModule;
438    }
439
440    /**
441     * Create the Properties object based on the arguments specified
442     * to the ANT task.
443     * @return the properties for property expansion expansion
444     * @throws BuildException if an error occurs
445     */
446    private Properties createOverridingProperties() {
447        final Properties returnValue = new Properties();
448
449        // Load the properties file if specified
450        if (properties != null) {
451            FileInputStream inStream = null;
452            try {
453                inStream = new FileInputStream(properties);
454                returnValue.load(inStream);
455            }
456            catch (final IOException ex) {
457                throw new BuildException("Error loading Properties file '"
458                        + properties + "'", ex, getLocation());
459            }
460            finally {
461                Closeables.closeQuietly(inStream);
462            }
463        }
464
465        // override with Ant properties like ${basedir}
466        final Map<String, Object> antProps = getProject().getProperties();
467        for (Map.Entry<String, Object> entry : antProps.entrySet()) {
468            final String value = String.valueOf(entry.getValue());
469            returnValue.setProperty(entry.getKey(), value);
470        }
471
472        // override with properties specified in subelements
473        for (Property p : overrideProps) {
474            returnValue.setProperty(p.getKey(), p.getValue());
475        }
476
477        return returnValue;
478    }
479
480    /**
481     * Return the list of listeners set in this task.
482     * @return the list of listeners.
483     */
484    private AuditListener[] getListeners() {
485        final int formatterCount = Math.max(1, formatters.size());
486
487        final AuditListener[] listeners = new AuditListener[formatterCount];
488
489        // formatters
490        try {
491            if (formatters.isEmpty()) {
492                final OutputStream debug = new LogOutputStream(this, Project.MSG_DEBUG);
493                final OutputStream err = new LogOutputStream(this, Project.MSG_ERR);
494                listeners[0] = new DefaultLogger(debug, true, err, true);
495            }
496            else {
497                for (int i = 0; i < formatterCount; i++) {
498                    final Formatter formatter = formatters.get(i);
499                    listeners[i] = formatter.createListener(this);
500                }
501            }
502        }
503        catch (IOException ex) {
504            throw new BuildException(String.format(Locale.ROOT, "Unable to create listeners: "
505                    + "formatters {%s}.", formatters), ex);
506        }
507        return listeners;
508    }
509
510    /**
511     * Returns the list of files (full path name) to process.
512     * @return the list of files included via the filesets.
513     */
514    protected List<File> scanFileSets() {
515        final List<File> list = new ArrayList<>();
516        if (fileName != null) {
517            // oops we've got an additional one to process, don't
518            // forget it. No sweat, it's fully resolved via the setter.
519            log("Adding standalone file for audit", Project.MSG_VERBOSE);
520            list.add(new File(fileName));
521        }
522        for (int i = 0; i < fileSets.size(); i++) {
523            final FileSet fileSet = fileSets.get(i);
524            final DirectoryScanner scanner = fileSet.getDirectoryScanner(getProject());
525            scanner.scan();
526
527            final String[] names = scanner.getIncludedFiles();
528            log(i + ") Adding " + names.length + " files from directory "
529                    + scanner.getBasedir(), Project.MSG_VERBOSE);
530
531            for (String element : names) {
532                final String pathname = scanner.getBasedir() + File.separator
533                        + element;
534                list.add(new File(pathname));
535            }
536        }
537
538        return list;
539    }
540
541    /**
542     * Poor mans enumeration for the formatter types.
543     * @author Oliver Burn
544     */
545    public static class FormatterType extends EnumeratedAttribute {
546        /** My possible values. */
547        private static final String[] VALUES = {E_XML, E_PLAIN};
548
549        @Override
550        public String[] getValues() {
551            return VALUES.clone();
552        }
553    }
554
555    /**
556     * Details about a formatter to be used.
557     * @author Oliver Burn
558     */
559    public static class Formatter {
560        /** The formatter type. */
561        private FormatterType type;
562        /** The file to output to. */
563        private File toFile;
564        /** Whether or not the write to the named file. */
565        private boolean useFile = true;
566
567        /**
568         * Set the type of the formatter.
569         * @param type the type
570         */
571        public void setType(FormatterType type) {
572            this.type = type;
573        }
574
575        /**
576         * Set the file to output to.
577         * @param destination destination the file to output to
578         */
579        public void setTofile(File destination) {
580            toFile = destination;
581        }
582
583        /**
584         * Sets whether or not we write to a file if it is provided.
585         * @param use whether not not to use provided file.
586         */
587        public void setUseFile(boolean use) {
588            useFile = use;
589        }
590
591        /**
592         * Creates a listener for the formatter.
593         * @param task the task running
594         * @return a listener
595         * @throws IOException if an error occurs
596         */
597        public AuditListener createListener(Task task) throws IOException {
598            if (type != null
599                    && E_XML.equals(type.getValue())) {
600                return createXmlLogger(task);
601            }
602            return createDefaultLogger(task);
603        }
604
605        /**
606         * Creates default logger.
607         * @param task the task to possibly log to
608         * @return a DefaultLogger instance
609         * @throws IOException if an error occurs
610         */
611        private AuditListener createDefaultLogger(Task task)
612                throws IOException {
613            if (toFile == null || !useFile) {
614                return new DefaultLogger(
615                    new LogOutputStream(task, Project.MSG_DEBUG),
616                    true, new LogOutputStream(task, Project.MSG_ERR), true);
617            }
618            final FileOutputStream infoStream = new FileOutputStream(toFile);
619            return new DefaultLogger(infoStream, true, infoStream, false);
620        }
621
622        /**
623         * Creates XML logger.
624         * @param task the task to possibly log to
625         * @return an XMLLogger instance
626         * @throws IOException if an error occurs
627         */
628        private AuditListener createXmlLogger(Task task) throws IOException {
629            if (toFile == null || !useFile) {
630                return new XMLLogger(new LogOutputStream(task,
631                        Project.MSG_INFO), true);
632            }
633            return new XMLLogger(new FileOutputStream(toFile), true);
634        }
635    }
636
637    /**
638     * Represents a property that consists of a key and value.
639     */
640    public static class Property {
641        /** The property key. */
642        private String key;
643        /** The property value. */
644        private String value;
645
646        /**
647         * Gets key.
648         * @return the property key
649         */
650        public String getKey() {
651            return key;
652        }
653
654        /**
655         * Sets key.
656         * @param key sets the property key
657         */
658        public void setKey(String key) {
659            this.key = key;
660        }
661
662        /**
663         * Gets value.
664         * @return the property value
665         */
666        public String getValue() {
667            return value;
668        }
669
670        /**
671         * Sets value.
672         * @param value set the property value
673         */
674        public void setValue(String value) {
675            this.value = value;
676        }
677
678        /**
679         * Sets the property value from a File.
680         * @param file set the property value from a File
681         */
682        public void setFile(File file) {
683            value = file.getAbsolutePath();
684        }
685    }
686
687    /** Represents a custom listener. */
688    public static class Listener {
689        /** Class name of the listener class. */
690        private String className;
691
692        /**
693         * Gets class name.
694         * @return the class name
695         */
696        public String getClassname() {
697            return className;
698        }
699
700        /**
701         * Sets class name.
702         * @param name set the class name
703         */
704        public void setClassname(String name) {
705            className = name;
706        }
707    }
708}