View Javadoc

1   package org.apache.maven.plugin.checkstyle;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.BufferedReader;
23  import java.io.ByteArrayOutputStream;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.OutputStream;
29  import java.io.Reader;
30  
31  import org.apache.maven.plugin.AbstractMojo;
32  import org.apache.maven.plugin.MojoExecutionException;
33  import org.apache.maven.plugin.MojoFailureException;
34  import org.apache.maven.project.MavenProject;
35  import org.codehaus.plexus.resource.ResourceManager;
36  import org.codehaus.plexus.resource.loader.FileResourceLoader;
37  import org.codehaus.plexus.util.ReaderFactory;
38  import org.codehaus.plexus.util.StringUtils;
39  import org.codehaus.plexus.util.xml.pull.MXParser;
40  import org.codehaus.plexus.util.xml.pull.XmlPullParser;
41  import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
42  
43  import com.puppycrawl.tools.checkstyle.DefaultLogger;
44  import com.puppycrawl.tools.checkstyle.XMLLogger;
45  import com.puppycrawl.tools.checkstyle.api.AuditListener;
46  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
47  
48  /**
49   * Perform a violation check against the last Checkstyle run to see if there are
50   * any violations. It reads the Checkstyle output file, counts the number of
51   * violations found and displays it on the console.
52   *
53   * @author <a href="mailto:joakim@erdfelt.net">Joakim Erdfelt</a>
54   * @version $Id: CheckstyleViolationCheckMojo.html 816658 2012-05-08 13:56:19Z hboutemy $
55   * @goal check
56   * @phase verify
57   * @requiresDependencyResolution compile
58   */
59  public class CheckstyleViolationCheckMojo
60      extends AbstractMojo
61  {
62      /**
63       * Specifies the path and filename to save the Checkstyle output. The format
64       * of the output file is determined by the <code>outputFileFormat</code>
65       * parameter.
66       *
67       * @parameter expression="${checkstyle.output.file}"
68       *            default-value="${project.build.directory}/checkstyle-result.xml"
69       */
70      private File outputFile;
71  
72      /**
73       * Specifies the format of the output to be used when writing to the output
74       * file. Valid values are "plain" and "xml".
75       *
76       * @parameter expression="${checkstyle.output.format}" default-value="xml"
77       */
78      private String outputFileFormat;
79  
80      /**
81       * Do we fail the build on a violation?
82       *
83       * @parameter expression="${checkstyle.failOnViolation}"
84       *            default-value="true"
85       */
86      private boolean failOnViolation;
87  
88      /**
89       * The maximum number of allowed violations. The execution fails only if the
90       * number of violations is above this limit.
91       *
92       * @parameter expression="${checkstyle.maxAllowedViolations}" default-value="0"
93       * @since 2.3
94       */
95      private int maxAllowedViolations = 0;
96  
97      /**
98       * The lowest severity level that is considered a violation.
99       * Valid values are "error", "warning" and "info".
100      *
101      * @parameter expression="${checkstyle.violationSeverity}" default-value="error"
102      * @since 2.2
103      */
104     private String violationSeverity = "error";
105 
106     /**
107      * Skip entire check.
108      *
109      * @parameter expression="${checkstyle.skip}" default-value="false"
110      * @since 2.2
111      */
112     private boolean skip;
113     
114     /**
115      * Skip checktyle execution will only scan the outputFile.
116      *
117      * @parameter expression="${checkstyle.skipExec}" default-value="false"
118      * @since 2.5
119      */
120     private boolean skipExec;    
121 
122     /**
123      * Output the detected violations to the console.
124      *
125      * @parameter expression="${checkstyle.console}" default-value="false"
126      * @since 2.3
127      */
128     private boolean logViolationsToConsole;
129     
130     /**
131      * <p>
132      * Specifies the location of the XML configuration to use.
133      * </p>
134      *
135      * <p>
136      * Potential values are a filesystem path, a URL, or a classpath resource.
137      * This parameter expects that the contents of the location conform to the
138      * xml format (Checkstyle <a
139      * href="http://checkstyle.sourceforge.net/config.html#Modules">Checker
140      * module</a>) configuration of rulesets.
141      * </p>
142      *
143      * <p>
144      * This parameter is resolved as resource, URL, then file. If successfully
145      * resolved, the contents of the configuration is copied into the
146      * <code>${project.build.directory}/checkstyle-configuration.xml</code>
147      * file before being passed to Checkstyle as a configuration.
148      * </p>
149      *
150      * <p>
151      * There are 4 predefined rulesets.
152      * </p>
153      *
154      * <ul>
155      * <li><code>config/sun_checks.xml</code>: Sun Checks.</li>
156      * <li><code>config/turbine_checks.xml</code>: Turbine Checks.</li>
157      * <li><code>config/avalon_checks.xml</code>: Avalon Checks.</li>
158      * <li><code>config/maven_checks.xml</code>: Maven Source Checks.</li>
159      * </ul>
160      * @since 2.5
161      * @parameter expression="${checkstyle.config.location}"
162      *            default-value="config/sun_checks.xml"
163      */
164     private String configLocation;    
165 
166     /**
167      * <p>
168      * Specifies the location of the properties file.
169      * </p>
170      *
171      * <p>
172      * This parameter is resolved as URL, File then resource. If successfully
173      * resolved, the contents of the properties location is copied into the
174      * <code>${project.build.directory}/checkstyle-checker.properties</code>
175      * file before being passed to Checkstyle for loading.
176      * </p>
177      *
178      * <p>
179      * The contents of the <code>propertiesLocation</code> will be made
180      * available to Checkstyle for specifying values for parameters within the
181      * xml configuration (specified in the <code>configLocation</code>
182      * parameter).
183      * </p>
184      *
185      * @parameter expression="${checkstyle.properties.location}"
186      * @since 2.5
187      */
188     private String propertiesLocation;
189     
190     /**
191      * Allows for specifying raw property expansion information.
192      *
193      * @parameter
194      */
195     private String propertyExpansion;
196     
197     /**
198      * <p>
199      * Specifies the location of the License file (a.k.a. the header file) that
200      * can be used by Checkstyle to verify that source code has the correct
201      * license header.
202      * </p>
203      * <p>
204      * You need to use ${checkstyle.header.file} in your Checkstyle xml
205      * configuration to reference the name of this header file.
206      * </p>
207      * <p>
208      * For instance:
209      * </p>
210      * <p>
211      * <code>
212      * &lt;module name="RegexpHeader">
213      *   &lt;property name="headerFile" value="${checkstyle.header.file}"/>
214      * &lt;/module>
215      * </code>
216      * </p>
217      *
218      * @parameter expression="${checkstyle.header.file}"
219      *            default-value="LICENSE.txt"
220      * @since 2.0-beta-2
221      */
222     private String headerLocation;    
223     
224     /**
225      * Specifies the cache file used to speed up Checkstyle on successive runs.
226      *
227      * @parameter default-value="${project.build.directory}/checkstyle-cachefile"
228      */
229     private String cacheFile;    
230     
231     /**
232      * The key to be used in the properties for the suppressions file.
233      *
234      * @parameter expression="${checkstyle.suppression.expression}"
235      *            default-value="checkstyle.suppressions.file"
236      * @since 2.1
237      */
238     private String suppressionsFileExpression;    
239  
240     /**
241      * <p>
242      * Specifies the location of the suppressions XML file to use.
243      * </p>
244      *
245      * <p>
246      * This parameter is resolved as resource, URL, then file. If successfully
247      * resolved, the contents of the suppressions XML is copied into the
248      * <code>${project.build.directory}/checkstyle-supressions.xml</code> file
249      * before being passed to Checkstyle for loading.
250      * </p>
251      *
252      * <p>
253      * See <code>suppressionsFileExpression</code> for the property that will
254      * be made available to your checkstyle configuration.
255      * </p>
256      *
257      * @parameter expression="${checkstyle.suppressions.location}"
258      * @since 2.0-beta-2
259      */
260     private String suppressionsLocation;    
261     
262     /**
263      * The file encoding to use when reading the source files. If the property <code>project.build.sourceEncoding</code>
264      * is not set, the platform default encoding is used. <strong>Note:</strong> This parameter always overrides the
265      * property <code>charset</code> from Checkstyle's <code>TreeWalker</code> module.
266      *
267      * @parameter expression="${encoding}" default-value="${project.build.sourceEncoding}"
268      * @since 2.2
269      */
270     private String encoding;    
271     
272     /**
273      * @since 2.5
274      * @component role="org.codehaus.plexus.resource.ResourceManager" role-hint="default"
275      * @required
276      * @readonly
277      */
278     protected ResourceManager locator;    
279     
280     /**
281      * @since 2.5
282      * @component role="org.apache.maven.plugin.checkstyle.CheckstyleExecutor" role-hint="default"
283      * @required
284      * @readonly
285      */
286     protected CheckstyleExecutor checkstyleExecutor;    
287     
288     /**
289      * Output errors to console.
290      *
291      * @parameter default-value="false"
292      */
293     private boolean consoleOutput;    
294     
295     /**
296      * The Maven Project Object.
297      *
298      * @parameter default-value="${project}"
299      * @required
300      * @readonly
301      */
302     protected MavenProject project;
303 
304     /**
305      * If <code>null</code>, the Checkstyle plugin will display violations on stdout.
306      * Otherwise, a text file will be created with the violations.
307      *
308      * @parameter
309      */
310     private File useFile;
311 
312     /**
313      * Specifies the names filter of the source files to be excluded for
314      * Checkstyle.
315      *
316      * @parameter expression="${checkstyle.excludes}"
317      */
318     private String excludes;    
319     
320     /**
321      * Specifies the names filter of the source files to be used for Checkstyle.
322      *
323      * @parameter expression="${checkstyle.includes}" default-value="**\/*.java"
324      * @required
325      */
326     private String includes;
327     
328     /**
329      * Specifies if the build should fail upon a violation.
330      *
331      * @parameter default-value="false"
332      */
333     private boolean failsOnError;    
334     
335     /**
336      * Specifies the location of the test source directory to be used for
337      * Checkstyle.
338      *
339      * @parameter default-value="${project.build.testSourceDirectory}"
340      * @since 2.2
341      */
342     private File testSourceDirectory;
343 
344     /**
345      * Include or not the test source directory to be used for Checkstyle.
346      *
347      * @parameter default-value="${false}"
348      * @since 2.2
349      */
350     private boolean includeTestSourceDirectory;    
351     
352     /**
353      * Specifies the location of the source directory to be used for Checkstyle.
354      *
355      * @parameter default-value="${project.build.sourceDirectory}"
356      * @required
357      */
358     private File sourceDirectory;    
359     
360     private ByteArrayOutputStream stringOutputStream;
361     
362     
363     /** {@inheritDoc} */
364     public void execute()
365         throws MojoExecutionException, MojoFailureException
366     {
367         
368         if ( !skip )
369         {
370 
371             if ( !skipExec )
372             {
373                 locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
374                 locator.addSearchPath( "url", "" );
375 
376                 locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
377 
378                 ClassLoader currentClassLoader = Thread.currentThread().getContextClassLoader();
379 
380                 try
381                 {
382                     CheckstyleExecutorRequest request = new CheckstyleExecutorRequest();
383                     request.setConsoleListener( getConsoleListener() ).setConsoleOutput( consoleOutput )
384                         .setExcludes( excludes ).setFailsOnError( failsOnError ).setIncludes( includes )
385                         .setIncludeTestSourceDirectory( includeTestSourceDirectory ).setListener( getListener() )
386                         .setLog( getLog() ).setProject( project ).setSourceDirectory( sourceDirectory )
387                         .setStringOutputStream( stringOutputStream ).setSuppressionsLocation( suppressionsLocation )
388                         .setTestSourceDirectory( testSourceDirectory ).setConfigLocation( configLocation )
389                         .setPropertyExpansion( propertyExpansion ).setHeaderLocation( headerLocation )
390                         .setCacheFile( cacheFile ).setSuppressionsFileExpression( suppressionsFileExpression )
391                         .setEncoding( encoding ).setPropertiesLocation( propertiesLocation );
392 
393                     checkstyleExecutor.executeCheckstyle( request );
394 
395                 }
396                 catch ( CheckstyleException e )
397                 {
398                     throw new MojoExecutionException( "Failed during checkstyle configuration", e );
399                 }
400                 catch ( CheckstyleExecutorException e )
401                 {
402                     throw new MojoExecutionException( "Failed during checkstyle execution", e );
403                 }
404                 finally
405                 {
406                     //be sure to restore original context classloader
407                     Thread.currentThread().setContextClassLoader( currentClassLoader );
408                 }
409             
410             }
411             if ( !"xml".equals( outputFileFormat ) )
412             {
413                 throw new MojoExecutionException( "Output format is '" + outputFileFormat
414                     + "', checkstyle:check requires format to be 'xml'." );
415             }
416 
417             if ( !outputFile.exists() )
418             {
419                 getLog().info(
420                                "Unable to perform checkstyle:check, "
421                                    + "unable to find checkstyle:checkstyle outputFile." );
422                 return;
423             }
424            
425             try
426             {
427                 XmlPullParser xpp = new MXParser();
428                 Reader freader = ReaderFactory.newXmlReader( outputFile );
429                 BufferedReader breader = new BufferedReader( freader );
430                 xpp.setInput( breader );
431 
432                 int violations = countViolations( xpp );
433                 if ( violations > maxAllowedViolations )
434                 {
435                     if ( failOnViolation )
436                     {
437                         String msg = "You have " + violations + " Checkstyle violation"
438                             + ( ( violations > 1 ) ? "s" : "" ) + ".";
439                         if ( maxAllowedViolations > 0 )
440                         {
441                             msg += " The maximum number of allowed violations is " + maxAllowedViolations + ".";
442                         }
443                         throw new MojoFailureException( msg );
444                     }
445 
446                     getLog().warn( "checkstyle:check violations detected but failOnViolation set to false" );
447                 }
448             }
449             catch ( IOException e )
450             {
451                 throw new MojoExecutionException( "Unable to read Checkstyle results xml: "
452                     + outputFile.getAbsolutePath(), e );
453             }
454             catch ( XmlPullParserException e )
455             {
456                 throw new MojoExecutionException( "Unable to read Checkstyle results xml: "
457                     + outputFile.getAbsolutePath(), e );
458             }
459         }
460     }
461 
462     private int countViolations( XmlPullParser xpp )
463         throws XmlPullParserException, IOException
464     {
465         int count = 0;
466 
467         int eventType = xpp.getEventType();
468         String file = "";
469         while ( eventType != XmlPullParser.END_DOCUMENT )
470         {
471             if ( eventType == XmlPullParser.START_TAG && "file".equals( xpp.getName() ) )
472             {
473                 file = xpp.getAttributeValue( "", "name" );
474                 file = file.substring( file.lastIndexOf( File.separatorChar ) + 1 );
475             }
476 
477             if ( eventType == XmlPullParser.START_TAG && "error".equals( xpp.getName() )
478                 && isViolation( xpp.getAttributeValue( "", "severity" ) ) )
479             {
480                 if ( logViolationsToConsole )
481                 {
482                     StringBuffer stb = new StringBuffer();
483                     stb.append( file );
484                     stb.append( '[' );
485                     stb.append( xpp.getAttributeValue( "", "line" ) );
486                     stb.append( ':' );
487                     stb.append( xpp.getAttributeValue( "", "column" ) );
488                     stb.append( "] " );
489                     stb.append( xpp.getAttributeValue( "", "message" ) );
490                     getLog().error( stb.toString() );
491                 }
492                 count++;
493             }
494             eventType = xpp.next();
495         }
496 
497         return count;
498     }
499 
500     /**
501      * Checks if the given severity is considered a violation.
502      * 
503      * @param severity The severity to check
504      * @return <code>true</code> if the given severity is a violation, otherwise <code>false</code>
505      */
506     private boolean isViolation( String severity )
507     {
508         if ( "error".equals( severity ) )
509         {
510             return "error".equals( violationSeverity ) || "warning".equals( violationSeverity )
511                 || "info".equals( violationSeverity );
512         }
513         else if ( "warning".equals( severity ) )
514         {
515             return "warning".equals( violationSeverity ) || "info".equals( violationSeverity );
516         }
517         else if ( "info".equals( severity ) )
518         {
519             return "info".equals( violationSeverity );
520         }
521         else
522         {
523             return false;
524         }
525     }
526     private DefaultLogger getConsoleListener()
527         throws MojoExecutionException
528     {
529         DefaultLogger consoleListener;
530 
531         if ( useFile == null )
532         {
533             stringOutputStream = new ByteArrayOutputStream();
534             consoleListener = new DefaultLogger( stringOutputStream, false );
535         }
536         else
537         {
538             OutputStream out = getOutputStream( useFile );
539 
540             consoleListener = new DefaultLogger( out, true );
541         }
542 
543         return consoleListener;
544     }
545 
546     private OutputStream getOutputStream( File file )
547         throws MojoExecutionException
548     {
549         File parentFile = file.getAbsoluteFile().getParentFile();
550 
551         if ( !parentFile.exists() )
552         {
553             parentFile.mkdirs();
554         }
555 
556         FileOutputStream fileOutputStream;
557         try
558         {
559             fileOutputStream = new FileOutputStream( file );
560         }
561         catch ( FileNotFoundException e )
562         {
563             throw new MojoExecutionException( "Unable to create output stream: " + file, e );
564         }
565         return fileOutputStream;
566     }
567 
568     private AuditListener getListener()
569         throws MojoFailureException, MojoExecutionException
570     {
571         AuditListener listener = null;
572 
573         if ( StringUtils.isNotEmpty( outputFileFormat ) )
574         {
575             File resultFile = outputFile;
576 
577             OutputStream out = getOutputStream( resultFile );
578 
579             if ( "xml".equals( outputFileFormat ) )
580             {
581                 listener = new XMLLogger( out, true );
582             }
583             else if ( "plain".equals( outputFileFormat ) )
584             {
585                 listener = new DefaultLogger( out, true );
586             }
587             else
588             {
589                 throw new MojoFailureException( "Invalid output file format: (" + outputFileFormat
590                     + "). Must be 'plain' or 'xml'." );
591             }
592         }
593 
594         return listener;
595     }
596     
597 }