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