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