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