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 * <module name="RegexpHeader">
213 * <property name="headerFile" value="${checkstyle.header.file}"/>
214 * </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 }