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