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