1 package org.codehaus.mojo.macker;
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 org.apache.maven.artifact.Artifact;
23 import org.apache.maven.artifact.handler.ArtifactHandler;
24 import org.apache.maven.artifact.repository.ArtifactRepository;
25 import org.apache.maven.artifact.resolver.AbstractArtifactResolutionException;
26 import org.apache.maven.artifact.resolver.ArtifactResolver;
27 import org.apache.maven.plugin.AbstractMojo;
28 import org.apache.maven.plugin.MojoExecutionException;
29 import org.apache.maven.plugin.MojoFailureException;
30 import org.apache.maven.project.MavenProject;
31
32 import java.io.File;
33 import java.io.IOException;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.HashMap;
38 import java.util.Iterator;
39 import java.util.LinkedHashSet;
40 import java.util.LinkedList;
41 import java.util.List;
42 import java.util.Map;
43
44 import org.codehaus.plexus.resource.ResourceManager;
45 import org.codehaus.plexus.resource.loader.FileResourceCreationException;
46 import org.codehaus.plexus.resource.loader.FileResourceLoader;
47 import org.codehaus.plexus.resource.loader.ResourceNotFoundException;
48 import org.codehaus.plexus.util.FileUtils;
49 import org.codehaus.plexus.util.StringUtils;
50
51 /**
52 * Runs Macker against the compiled classes of the project.
53 *
54 * @goal macker
55 * @execute phase="compile"
56 * @requiresDependencyResolution compile
57 * @requiresProject
58 * @author <a href="http://www.codehaus.org/~wfay/">Wayne Fay</a>
59 * @author <a href="http://people.apache.org/~bellingard/">Fabrice Bellingard</a>
60 * @author <a href="http://www.code-cop.org/">Peter Kofler</a>
61 */
62 public class MackerMojo
63 extends AbstractMojo
64 {
65 /**
66 * Directory containing the class files for Macker to analyze.
67 *
68 * @parameter expression="${project.build.outputDirectory}"
69 * @required
70 */
71 private File classesDirectory;
72
73 /**
74 * The directories containing the test-classes to be analyzed.
75 *
76 * @parameter expression="${project.build.testOutputDirectory}"
77 * @required
78 */
79 private File testClassesDirectory;
80
81 /**
82 * A list of files to exclude from checking. Can contain Ant-style wildcards and double wildcards. Note that these
83 * exclusion patterns only operate on the path of a source file relative to its source root directory. In other
84 * words, files are excluded based on their package and/or class name. If you want to exclude entire root
85 * directories, use the parameter <code>excludeRoots</code> instead.
86 *
87 * @parameter
88 */
89 private String[] excludes;
90
91 /**
92 * A list of files to include from checking. Can contain Ant-style wildcards and double wildcards.
93 * Defaults to **\/*.class.
94 *
95 * @parameter
96 */
97 private String[] includes;
98
99 /**
100 * Run Macker on the tests.
101 *
102 * @parameter default-value="false"
103 */
104 private boolean includeTests;
105
106 /**
107 * Directory containing the rules files for Macker.
108 *
109 * @parameter expression="${basedir}/src/main/config"
110 * @required
111 */
112 private File rulesDirectory;
113
114 /**
115 * Directory where the Macker output file will be generated.
116 *
117 * @parameter default-value="${project.build.directory}"
118 * @required
119 */
120 private File outputDirectory;
121
122 /**
123 * Name of the Macker output file.
124 *
125 * @parameter expression="${outputName}" default-value="macker-out.xml"
126 * @required
127 */
128 private String outputName;
129
130 /**
131 * Print max messages.
132 *
133 * @parameter expression="${maxmsg}" default-value="0"
134 */
135 private int maxmsg;
136
137 /**
138 * Print threshold. Valid options are error, warning, info, and debug.
139 *
140 * @parameter expression="${print}"
141 */
142 private String print;
143
144 /**
145 * Anger threshold. Valid options are error, warning, info, and debug.
146 *
147 * @parameter expression="${anger}"
148 */
149 private String anger;
150
151 /**
152 * Name of the Macker rules file.
153 *
154 * @parameter expression="${rule}" default-value="macker-rules.xml"
155 */
156 private String rule;
157
158 /**
159 * Name of the Macker rules files.
160 *
161 * @parameter expression="${rules}"
162 */
163 private String[] rules = new String[0];
164
165 /**
166 * @component
167 * @required
168 * @readonly
169 */
170 private ResourceManager locator;
171
172 /**
173 * Variables map that will be passed to Macker.
174 *
175 * @parameter expression="${variables}"
176 */
177 private Map/*<String, String>*/variables = new HashMap/*<String, String>*/();
178
179 /**
180 * Verbose setting for Macker tool execution.
181 *
182 * @parameter expression="${verbose}" default-value="false"
183 */
184 private boolean verbose;
185
186 /**
187 * Fail the build on an error.
188 *
189 * @parameter default-value="true"
190 */
191 private boolean failOnError;
192
193 /**
194 * Skip the checks. Most useful on the command line
195 * via "-Dmacker.skip=true".
196 *
197 * @parameter expression="${macker.skip}" default-value="false"
198 */
199 private boolean skip;
200
201 /**
202 * <i>Maven Internal</i>: Project to interact with.
203 *
204 * @parameter expression="${project}"
205 * @required
206 * @readonly
207 */
208 private MavenProject project;
209
210 /**
211 * Maximum memory to pass JVM of Macker processes.
212 *
213 * @parameter expression="${macker.maxmem}" default-value="64m"
214 */
215 private String maxmem;
216
217 /**
218 * <i>Maven Internal</i>: List of artifacts for the plugin.
219 *
220 * @parameter expression="${plugin.artifacts}"
221 * @required
222 * @readonly
223 */
224 private List/*<Artifact>*/ pluginClasspathList;
225
226 /**
227 * Only output Macker errors, avoid info messages.
228 *
229 * @parameter expression="${quiet}" default-value="false"
230 */
231 private boolean quiet;
232
233 /**
234 * @parameter default-value="${localRepository}"
235 * @required
236 * @readonly
237 */
238 private ArtifactRepository localRepository;
239
240 /**
241 * @parameter default-value="${project.remoteArtifactRepositories}"
242 * @required
243 * @readonly
244 */
245 private List remoteRepositories;
246
247 /**
248 * @component
249 * @required
250 * @readonly
251 */
252 private ArtifactResolver resolver;
253
254 /**
255 * @throws MojoExecutionException if a error occurs during Macker execution
256 * @throws MojoFailureException if Macker detects a failure.
257 * @see org.apache.maven.plugin.Mojo#execute()
258 */
259 public void execute()
260 throws MojoExecutionException, MojoFailureException
261 {
262 if ( skip )
263 {
264 return;
265 }
266
267 ArtifactHandler artifactHandler = project.getArtifact().getArtifactHandler();
268 if ( !"java".equals( artifactHandler.getLanguage() ) )
269 {
270 if ( !quiet )
271 {
272 getLog().info( "Not executing macker as the project is not a Java classpath-capable package" );
273 }
274 return;
275 }
276
277 //configure ResourceManager
278 locator.addSearchPath( FileResourceLoader.ID, project.getFile().getParentFile().getAbsolutePath() );
279 locator.addSearchPath( "url", "" );
280 locator.setOutputDirectory( new File( project.getBuild().getDirectory() ) );
281
282 // check if rules were specified
283 if ( null == rules || 0 == rules.length )
284 {
285 rules = new String[1]; // at least the default name
286 rules[0] = rule;
287 }
288
289 // check if there are class files to analyze
290 List/*<File>*/files;
291 try
292 {
293 files = getFilesToProcess();
294 }
295 catch ( IOException e )
296 {
297 throw new MojoExecutionException( "Error during Macker execution: error in file selection", e );
298 }
299 if ( files == null || files.size() == 0 )
300 {
301 // no class file, we can't do anything
302 if ( !quiet )
303 {
304 if ( includeTests )
305 {
306 getLog().info( "No class files in directories " + classesDirectory + ", " + testClassesDirectory );
307 }
308 else
309 {
310 getLog().info( "No class files in specified directory " + classesDirectory );
311 }
312 }
313 }
314 else
315 {
316 if ( !outputDirectory.exists() )
317 {
318 if ( !outputDirectory.mkdirs() )
319 {
320 throw new MojoExecutionException( "Error during Macker execution: Could not create directory "
321 + outputDirectory.getAbsolutePath() );
322 }
323 }
324
325 // let's go!
326 File outputFile = new File( outputDirectory, outputName );
327 launchMacker( outputFile, files );
328 }
329 }
330
331 /**
332 * Executes Macker as requested.
333 *
334 * @param outputFile the result file that will should produced by macker
335 * @param files classes files that should be analysed
336 * @throws MojoExecutionException if a error occurs during Macker execution
337 * @throws MojoFailureException if Macker detects a failure.
338 */
339 private void launchMacker( File outputFile, List/*<File>*/files )
340 throws MojoExecutionException, MojoFailureException
341 {
342 try
343 {
344 Macker macker = createMacker( outputFile );
345 configureRules( macker );
346 initMackerVariables( macker );
347 specifyClassFilesToAnalyse( files, macker );
348 // we're OK with configuration, let's run Macker
349 macker.check();
350 // if we're here, then everything went fine
351 if ( !quiet )
352 {
353 getLog().info( "Macker has not found any violation." );
354 }
355 }
356 catch ( MojoExecutionException ex )
357 {
358 throw ex;
359 }
360 catch ( MojoFailureException ex )
361 {
362 getLog().warn( "Macker has detected violations. Please refer to the XML report for more information." );
363 if ( failOnError )
364 {
365 throw ex;
366 }
367 }
368 catch ( Exception ex )
369 {
370 throw new MojoExecutionException( "Error during Macker execution: " + ex.getMessage(), ex );
371 }
372 }
373
374 /**
375 * Tell Macker where to look for Class files to analyze.
376 *
377 * @param files the ".class" files to analyze
378 * @param macker the Macker instance
379 * @throws IOException if there's a problem reading a file
380 * @throws MojoExecutionException if there's a problem parsing a class
381 */
382 private void specifyClassFilesToAnalyse( List/*<File>*/files, Macker macker )
383 throws IOException, MojoExecutionException
384 {
385 for ( Iterator/*<File>*/i = files.iterator(); i.hasNext(); )
386 {
387 macker.addClass( (File) i.next() );
388 }
389 }
390
391 /**
392 * If specific variables are set in the POM, give them to Macker.
393 *
394 * @param macker the Macker isntance
395 */
396 private void initMackerVariables( Macker macker )
397 {
398 if ( variables != null && variables.size() > 0 )
399 {
400 Iterator/*<String>*/it = variables.keySet().iterator();
401 while ( it.hasNext() )
402 {
403 String key = (String) it.next();
404 macker.setVariable( key, (String) variables.get( key ) );
405 }
406 }
407 }
408
409 /**
410 * Configure Macker with the rule files specified in the POM.
411 *
412 * @param macker the Macker instance
413 * @throws IOException if there's a problem reading a file
414 * @throws MojoExecutionException if there's a problem parsing a rule file
415 */
416 private void configureRules( Macker macker )
417 throws IOException, MojoExecutionException
418 {
419 try
420 {
421 for ( int i = 0; i < rules.length; i++ )
422 {
423 String set = rules[i];
424 File ruleFile = new File( rulesDirectory, set );
425 if ( ruleFile.exists() )
426 {
427 getLog().debug( "Add rules file: " + rulesDirectory + File.separator + rules[i] );
428 }
429 else
430 {
431 getLog().debug( "Preparing ruleset: " + set );
432 ruleFile = locator.getResourceAsFile( set, getLocationTemp( set ) );
433
434 if ( null == ruleFile )
435 {
436 throw new MojoExecutionException( "Could not resolve rules file: " + set );
437 }
438 }
439 macker.addRulesFile( ruleFile );
440 }
441 }
442 catch ( ResourceNotFoundException e )
443 {
444 throw new MojoExecutionException( e.getMessage(), e );
445 }
446 catch ( FileResourceCreationException e )
447 {
448 throw new MojoExecutionException( e.getMessage(), e );
449 }
450 }
451
452 /**
453 * Convenience method to get the location of the specified file name.
454 *
455 * @param name the name of the file whose location is to be resolved
456 * @return a String that contains the absolute file name of the file
457 */
458 private String getLocationTemp( String name )
459 {
460 String loc = name;
461 if ( loc.indexOf( '/' ) != -1 )
462 {
463 loc = loc.substring( loc.lastIndexOf( '/' ) + 1 );
464 }
465 if ( loc.indexOf( '\\' ) != -1 )
466 {
467 loc = loc.substring( loc.lastIndexOf( '\\' ) + 1 );
468 }
469 getLog().debug( "Before: " + name + " After: " + loc );
470 return loc;
471 }
472
473 /**
474 * Prepares Macker for the analysis.
475 *
476 * @param outputFile the result file that will should produced by Macker
477 * @return the new instance of Macker
478 * @throws IOException if there's a problem with the report file
479 * @throws MojoExecutionException if there's a creating the classpath for forking
480 */
481 private Macker createMacker( File outputFile )
482 throws IOException, MojoExecutionException
483 {
484 // Macker macker = new LinkedMacker();
485 ForkedMacker macker = new ForkedMacker( );
486 macker.setLog( getLog() );
487 macker.setMaxmem( maxmem );
488 macker.setPluginClasspathList( collectArtifactList() );
489 macker.setQuiet( quiet );
490
491 macker.setVerbose( verbose );
492 macker.setXmlReportFile( outputFile );
493 if ( maxmsg > 0 )
494 {
495 macker.setPrintMaxMessages( maxmsg );
496 }
497 if ( print != null )
498 {
499 macker.setPrintThreshold( print );
500 }
501 if ( anger != null )
502 {
503 macker.setAngerThreshold( anger );
504 }
505 return macker;
506 }
507
508 /**
509 * Get the full classpath of this plugin including the plugin itself.
510 * @throws MojoExecutionException if there's a creating the classpath for forking
511 */
512 private List collectArtifactList()
513 throws MojoExecutionException
514 {
515 // look up myself, it must be here
516 Artifact myself = (Artifact) getProject().getPluginArtifactMap().get( "org.codehaus.mojo:macker-maven-plugin" );
517 try
518 {
519 resolver.resolve( myself, remoteRepositories, localRepository );
520 }
521 catch ( AbstractArtifactResolutionException e )
522 {
523 throw new MojoExecutionException( e.getMessage(), e );
524 }
525
526 List/*<Artifact>*/ classpath = new ArrayList/*<Artifact>*/();
527 classpath.add( myself );
528 classpath.addAll( pluginClasspathList );
529 return classpath;
530 }
531
532 /**
533 * Returns the MavenProject object.
534 *
535 * @return MavenProject
536 */
537 public MavenProject getProject()
538 {
539 return this.project;
540 }
541
542 /**
543 * Convenience method to get the list of files where the PMD tool will be executed
544 *
545 * @return a List of the files where the MACKER tool will be executed
546 * @throws IOException if there's a problem scanning the directories
547 */
548 private List/*<File>*/getFilesToProcess()
549 throws IOException
550 {
551 List/*<File>*/directories = new ArrayList/*<File>*/();
552
553 if ( classesDirectory != null && classesDirectory.isDirectory() )
554 {
555 directories.add( classesDirectory );
556 }
557 if ( includeTests )
558 {
559 if ( testClassesDirectory != null && testClassesDirectory.isDirectory() )
560 {
561 directories.add( testClassesDirectory );
562 }
563 else
564 {
565 getLog().info( "No class files in test directory: " + testClassesDirectory );
566 }
567 }
568
569 String excluding = getExcludes();
570 getLog().debug( "Exclusions: " + excluding );
571 String including = getIncludes();
572 getLog().debug( "Inclusions: " + including );
573
574 List/*<File>*/files = new LinkedList/*<File>*/();
575
576 for ( Iterator/*<File>*/i = directories.iterator(); i.hasNext(); )
577 {
578 File sourceDirectory = (File) i.next();
579 if ( sourceDirectory.isDirectory() )
580 {
581 List/*<File>*/newfiles = FileUtils.getFiles( sourceDirectory, including, excluding );
582 files.addAll( newfiles );
583 }
584 }
585
586 return files;
587 }
588
589 /**
590 * Gets the comma separated list of effective include patterns.
591 *
592 * @return The comma separated list of effective include patterns, never <code>null</code>.
593 */
594 private String getIncludes()
595 {
596 Collection/*<String>*/patterns = new LinkedHashSet/*<String>*/();
597 if ( includes != null )
598 {
599 patterns.addAll( Arrays.asList( includes ) );
600 }
601 if ( patterns.isEmpty() )
602 {
603 patterns.add( "**/*.class" );
604 }
605 return StringUtils.join( patterns.iterator(), "," );
606 }
607
608 /**
609 * Gets the comma separated list of effective exclude patterns.
610 *
611 * @return The comma separated list of effective exclude patterns, never <code>null</code>.
612 */
613 private String getExcludes()
614 {
615 Collection/*<String>*/patterns = new LinkedHashSet/*<String>*/( FileUtils.getDefaultExcludesAsList() );
616 if ( excludes != null )
617 {
618 patterns.addAll( Arrays.asList( excludes ) );
619 }
620 return StringUtils.join( patterns.iterator(), "," );
621 }
622
623 /**
624 * For test purposes only.
625 */
626 void setRules( String[] ruleSets )
627 {
628 rules = (String[]) ruleSets.clone();
629 }
630
631 }