View Javadoc
1   /*
2    * #%L
3    * Mojo's Maven plugin for Cobertura
4    * %%
5    * Copyright (C) 2005 - 2013 Codehaus
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   * 
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   * 
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  package org.codehaus.mojo.cobertura;
21  
22  import net.sourceforge.cobertura.coveragedata.CoverageDataFileHandler;
23  import net.sourceforge.cobertura.coveragedata.ProjectData;
24  import org.apache.maven.artifact.Artifact;
25  import org.apache.maven.doxia.siterenderer.Renderer;
26  import org.apache.maven.plugin.MojoExecutionException;
27  import org.apache.maven.project.MavenProject;
28  import org.apache.maven.reporting.AbstractMavenReport;
29  import org.apache.maven.reporting.MavenReportException;
30  import org.codehaus.mojo.cobertura.configuration.MaxHeapSizeUtil;
31  import org.codehaus.mojo.cobertura.tasks.CommandLineArguments;
32  import org.codehaus.mojo.cobertura.tasks.ReportTask;
33  
34  import java.io.File;
35  import java.net.URI;
36  import java.util.ArrayList;
37  import java.util.Collections;
38  import java.util.HashMap;
39  import java.util.List;
40  import java.util.Locale;
41  import java.util.Map;
42  import java.util.ResourceBundle;
43  
44  /**
45   * Instrument the compiled classes, run the unit tests and generate a Cobertura
46   * report.
47   *
48   * @author <a href="will.gwaltney@sas.com">Will Gwaltney</a>
49   * @author <a href="mailto:joakim@erdfelt.com">Joakim Erdfelt</a>
50   * @goal cobertura
51   * @execute phase="test" lifecycle="cobertura"
52   */
53  public class CoberturaReportMojo
54      extends AbstractMavenReport
55  {
56      /**
57       * The format of the report. Supports 'html' or 'xml'. Defaults to 'html'.
58       *
59       * @parameter expression="${cobertura.report.format}"
60       * @deprecated
61       */
62      private String format;
63  
64      /**
65       * The formats of the report. Can be 'html' and/or 'xml'. Defaults to 'html'.
66       *
67       * @parameter
68       */
69      private String[] formats = new String[]{ "html" };
70  
71      /**
72       * The encoding for the java source code files.
73       *
74       * @parameter expression="${project.build.sourceEncoding}" default-value="UTF-8".
75       * @since 2.4
76       */
77      private String encoding;
78  
79      /**
80       * Maximum memory to pass to the JVM for Cobertura processes.
81       *
82       * @parameter expression="${cobertura.maxmem}"
83       */
84      private String maxmem = "64m";
85  
86      /**
87       * <p>
88       * The Datafile Location.
89       * </p>
90       *
91       * @parameter expression="${cobertura.datafile}" default-value="${project.build.directory}/cobertura/cobertura.ser"
92       * @required
93       * @readonly
94       */
95      private File dataFile;
96  
97      /**
98       * <i>Maven Internal</i>: List of artifacts for the plugin.
99       *
100      * @parameter default-value="${plugin.artifacts}"
101      * @required
102      * @readonly
103      */
104     private List<Artifact> pluginClasspathList;
105 
106     /**
107      * The output directory for the report.
108      *
109      * @parameter default-value="${project.reporting.outputDirectory}/cobertura"
110      * @required
111      */
112     private File outputDirectory;
113 
114     /**
115      * Only output Cobertura errors, avoid info messages.
116      *
117      * @parameter expression="${quiet}" default-value="false"
118      * @since 2.1
119      */
120     private boolean quiet;
121 
122     /**
123      * Generate aggregate reports in multi-module projects.
124      *
125      * @parameter expression="${cobertura.aggregate}" default-value="false"
126      * @since 2.5
127      */
128     private boolean aggregate;
129 
130     /**
131      * Whether to remove GPL licensed files from the generated report.
132      * This is required to distribute the report as part of a distribution,
133      * which is licensed under the ASL, or a similar license, which is
134      * incompatible with the GPL.
135      *
136      * @parameter default-value="false" expression="${cobertura.omitGplFiles}"
137      * @since 2.5
138      */
139     private boolean omitGplFiles;
140 
141     /**
142      * <i>Maven Internal</i>: The Doxia Site Renderer.
143      *
144      * @component
145      */
146     private Renderer siteRenderer;
147 
148     /**
149      * List of maven project of the current build
150      *
151      * @parameter expression="${reactorProjects}"
152      * @required
153      * @readonly
154      */
155     private List<MavenProject> reactorProjects;
156 
157     /**
158      * <i>Maven Internal</i>: Project to interact with.
159      *
160      * @parameter default-value="${project}"
161      * @required
162      * @readonly
163      */
164     private MavenProject project;
165 
166     private Map<MavenProject, List<MavenProject>> projectChildren;
167 
168     private String relDataFileName;
169 
170     private String relAggregateOutputDir;
171 
172     /**
173      * Constructs a <code>CoberturaReportMojo</code>.
174      * Sets the max memory to the maven max memory if set, otherwise
175      * the default <code>CoberturaReportMojo</code> value is used.
176      */
177     public CoberturaReportMojo()
178     {
179         if ( MaxHeapSizeUtil.getInstance().envHasMavenMaxMemSetting() )
180         {
181             maxmem = MaxHeapSizeUtil.getInstance().getMavenMaxMemSetting();
182         }
183     }
184 
185     /**
186      * @param locale for the message bundle
187      * @return localized cobertura name
188      * @see org.apache.maven.reporting.MavenReport#getName(java.util.Locale)
189      */
190     public String getName( Locale locale )
191     {
192         return getBundle( locale ).getString( "report.cobertura.name" );
193     }
194 
195     /**
196      * @param locale for the message bundle
197      * @return localized description
198      * @see org.apache.maven.reporting.MavenReport#getDescription(java.util.Locale)
199      */
200     public String getDescription( Locale locale )
201     {
202         return getBundle( locale ).getString( "report.cobertura.description" );
203     }
204 
205     @Override
206     protected String getOutputDirectory()
207     {
208         return outputDirectory.getAbsolutePath();
209     }
210 
211     @Override
212     protected MavenProject getProject()
213     {
214         return project;
215     }
216 
217     @Override
218     protected Renderer getSiteRenderer()
219     {
220         return siteRenderer;
221     }
222 
223     /**
224      * perform the actual reporting
225      *
226      * @param task
227      * @param outputFormat
228      * @throws MavenReportException
229      */
230     private void executeReportTask( ReportTask task, String outputFormat )
231         throws MavenReportException
232     {
233         task.setOutputFormat( outputFormat );
234 
235         // execute task
236         try
237         {
238             task.execute();
239         }
240         catch ( MojoExecutionException e )
241         {
242             // throw new MavenReportException( "Error in Cobertura Report generation: " + e.getMessage(), e );
243             // better don't break the build if report is not generated, also due to the sporadic MCOBERTURA-56
244             getLog().error( "Error in Cobertura Report generation: " + e.getMessage(), e );
245         }
246     }
247 
248     /**
249      * {@inheritDoc}
250      */
251     @Override
252     protected void executeReport( Locale locale )
253         throws MavenReportException
254     {
255         if ( canGenerateSimpleReport() )
256         {
257             executeReport( getDataFile(), outputDirectory, getCompileSourceRoots() );
258         }
259 
260         if ( canGenerateAggregateReports() )
261         {
262             executeAggregateReport( locale );
263         }
264     }
265 
266     /**
267      * Generates aggregate cobertura reports for all multi-module projects.
268      */
269     private void executeAggregateReport( Locale locale )
270         throws MavenReportException
271     {
272         for ( MavenProject proj : reactorProjects )
273         {
274             if ( !isMultiModule( proj ) )
275             {
276                 continue;
277             }
278             executeAggregateReport( locale, proj );
279         }
280     }
281 
282     /**
283      * Generates an aggregate cobertura report for the given project.
284      */
285     private void executeAggregateReport( Locale locale, MavenProject curProject )
286         throws MavenReportException
287     {
288         List<MavenProject> children = getAllChildren( curProject );
289 
290         if ( children.isEmpty() )
291         {
292             return;
293         }
294 
295         List<File> serFiles = getOutputFiles( children );
296         if ( serFiles.isEmpty() )
297         {
298             getLog().info( "Not executing aggregate cobertura:report for " + curProject.getName()
299                                + " as no child cobertura data files could not be found" );
300             return;
301         }
302 
303         getLog().info( "Executing aggregate cobertura:report for " + curProject.getName() );
304 
305         ProjectData aggProjectData = new ProjectData();
306         for ( File serFile : serFiles )
307         {
308             ProjectData data = CoverageDataFileHandler.loadCoverageData( serFile );
309             aggProjectData.merge( data );
310         }
311 
312         File aggSerFile = new File( curProject.getBasedir(), relDataFileName );
313         aggSerFile.getAbsoluteFile().getParentFile().mkdirs();
314         getLog().info( "Saving aggregate cobertura information in " + aggSerFile.getAbsolutePath() );
315         CoverageDataFileHandler.saveCoverageData( aggProjectData, aggSerFile );
316 
317         // get all compile source roots
318         List<String> aggCompileSourceRoots = new ArrayList<String>();
319         for ( MavenProject child : children )
320         {
321             aggCompileSourceRoots.addAll( child.getCompileSourceRoots() );
322         }
323 
324         File reportDir = new File( curProject.getBasedir(), relAggregateOutputDir );
325         reportDir.mkdirs();
326         executeReport( aggSerFile, reportDir, aggCompileSourceRoots );
327     }
328 
329     /**
330      * Executes the cobertura report task for the given dataFile, outputDirectory, and compileSourceRoots.
331      */
332     private void executeReport( File curDataFile, File curOutputDirectory, List<String> curCompileSourceRoots )
333         throws MavenReportException
334     {
335         ReportTask task = new ReportTask();
336 
337         // task defaults
338         task.setLog( getLog() );
339         task.setPluginClasspathList( pluginClasspathList );
340         task.setQuiet( quiet );
341 
342         // task specifics
343         task.setMaxmem( maxmem );
344         task.setDataFile( curDataFile );
345         task.setOutputDirectory( curOutputDirectory );
346         task.setCompileSourceRoots( curCompileSourceRoots );
347         task.setSourceEncoding( encoding );
348 
349         CommandLineArguments cmdLineArgs;
350         cmdLineArgs = new CommandLineArguments();
351         cmdLineArgs.setUseCommandsFile( true );
352         task.setCmdLineArgs( cmdLineArgs );
353 
354         if ( format != null )
355         {
356             formats = new String[]{ format };
357         }
358 
359         for ( int i = 0; i < formats.length; i++ )
360         {
361             executeReportTask( task, formats[i] );
362         }
363 
364         removeGplFiles();
365     }
366 
367     /**
368      * Removes files from the generated report, which are distributed under
369      * the GPL.
370      */
371     private void removeGplFiles()
372         throws MavenReportException
373     {
374         if ( omitGplFiles )
375         {
376             final String[] files =
377                 new String[]{ "js/customsorttypes.js", "js/sortabletable.js", "js/stringbuilder.js" };
378             for ( int i = 0; i < files.length; i++ )
379             {
380                 final File f = new File( outputDirectory, files[i] );
381                 if ( f.exists() )
382                 {
383                     if ( f.delete() )
384                     {
385                         getLog().debug( "Removed GPL licensed file " + f.getPath() );
386                     }
387                     else
388                     {
389                         throw new MavenReportException( "Unable to remove GPL licensed file " + f.getPath() );
390                     }
391                 }
392                 else
393                 {
394                     getLog().info( "GPL licensed file " + f.getPath() + " not found." );
395                 }
396             }
397         }
398     }
399 
400     /**
401      * {@inheritDoc}
402      */
403     public String getOutputName()
404     {
405         return "cobertura/index";
406     }
407 
408     @Override
409     public boolean isExternalReport()
410     {
411         return true;
412     }
413 
414     @Override
415     public boolean canGenerateReport()
416     {
417         if ( canGenerateSimpleReport() )
418         {
419             return true;
420         }
421         else
422         {
423             getLog().info( "Not executing cobertura:report as the cobertura data file (" + getDataFile()
424                                + ") could not be found" );
425         }
426 
427         if ( canGenerateAggregateReports() )
428         {
429             return true;
430         }
431 
432         if ( aggregate && isMultiModule( project ) )
433         {
434             // unfortunately, we don't know before hand whether we can generate an aggregate report for a
435             // multi-module. if we return false here, then we won't get a link in the main reports list. so we'll
436             // just be optimistic
437             return true;
438         }
439         return false;
440     }
441 
442     /**
443      * Returns whether or not we can generate a simple (non-aggregate) report for this project.
444      *
445      * @return <code>true</code> if a simple report can be generated, otherwise <code>false</code>
446      */
447     private boolean canGenerateSimpleReport()
448     {
449         /*
450          * Don't have to check for source directories or java code or the like for report generation. Checks for source
451          * directories or java project classpath existence should only occur in the Instrument Mojo.
452          */
453         return getDataFile().exists() && getDataFile().isFile();
454     }
455 
456     /**
457      * Returns whether or not we can generate any aggregate reports at this time.
458      */
459     private boolean canGenerateAggregateReports()
460     {
461         // we only generate aggregate reports after the last project runs
462         if ( aggregate && isLastProject( project, reactorProjects ) )
463         {
464             buildAggregateInfo();
465 
466             if ( !getOutputFiles( reactorProjects ).isEmpty() )
467             {
468                 return true;
469             }
470         }
471         return false;
472     }
473 
474     /**
475      * Returns the compileSourceRoots for the currently executing project.
476      */
477     @SuppressWarnings( "unchecked" )
478     private List<String> getCompileSourceRoots()
479     {
480         return project.getExecutionProject().getCompileSourceRoots();
481     }
482 
483     @Override
484     public void setReportOutputDirectory( File reportOutputDirectory )
485     {
486         if ( ( reportOutputDirectory != null ) && ( !reportOutputDirectory.getAbsolutePath().endsWith( "cobertura" ) ) )
487         {
488             this.outputDirectory = new File( reportOutputDirectory, "cobertura" );
489         }
490         else
491         {
492             this.outputDirectory = reportOutputDirectory;
493         }
494     }
495 
496     /**
497      * Gets the resource bundle for the report text.
498      *
499      * @param locale The locale for the report, must not be <code>null</code>.
500      * @return The resource bundle for the requested locale.
501      */
502     private ResourceBundle getBundle( Locale locale )
503     {
504         return ResourceBundle.getBundle( "cobertura-report", locale );
505     }
506 
507     /**
508      * Check whether the element is the last element of the list
509      *
510      * @param project          element to check
511      * @param mavenProjectList list of maven project
512      * @return true if project is the last element of mavenProjectList  list
513      */
514     private boolean isLastProject( MavenProject project, List<MavenProject> mavenProjectList )
515     {
516         return project.equals( mavenProjectList.get( mavenProjectList.size() - 1 ) );
517     }
518 
519     /**
520      * Test if the project has pom packaging
521      *
522      * @param mavenProject Project to test
523      * @return True if it has a pom packaging
524      */
525     private boolean isMultiModule( MavenProject mavenProject )
526     {
527         return "pom".equals( mavenProject.getPackaging() );
528     }
529 
530     /**
531      * Generates various information needed for building aggregate reports.
532      */
533     private void buildAggregateInfo()
534     {
535         if ( projectChildren != null )
536         {
537             // already did this work
538             return;
539         }
540 
541         // build parent-child map
542         projectChildren = new HashMap<MavenProject, List<MavenProject>>();
543         for ( MavenProject proj : reactorProjects )
544         {
545             List<MavenProject> depList = projectChildren.get( proj.getParent() );
546             if ( depList == null )
547             {
548                 depList = new ArrayList<MavenProject>();
549                 projectChildren.put( proj.getParent(), depList );
550             }
551             depList.add( proj );
552         }
553 
554         // attempt to determine where data files and output dir are
555         relDataFileName = relativize( project.getBasedir(), getDataFile() );
556         if ( relDataFileName == null )
557         {
558             getLog().warn( "Could not determine relative data file name, defaulting to 'cobertura/cobertura.ser'" );
559             relDataFileName = "cobertura/cobertura.ser";
560         }
561         relAggregateOutputDir = relativize( project.getBasedir(), outputDirectory );
562         if ( relAggregateOutputDir == null )
563         {
564             getLog().warn( "Could not determine relative output dir name, defaulting to 'cobertura'" );
565             relAggregateOutputDir = "cobertura";
566         }
567     }
568 
569     /**
570      * Returns a list containing all the recursive, non-pom children of the given project, never <code>null</code>.
571      */
572     private List<MavenProject> getAllChildren( MavenProject parentProject )
573     {
574         List<MavenProject> children = projectChildren.get( parentProject );
575         if ( children == null )
576         {
577             return Collections.emptyList();
578         }
579 
580         List<MavenProject> result = new ArrayList<MavenProject>();
581         for ( MavenProject child : children )
582         {
583             if ( isMultiModule( child ) )
584             {
585                 result.addAll( getAllChildren( child ) );
586             }
587             else
588             {
589                 result.add( child );
590             }
591         }
592         return result;
593     }
594 
595     /**
596      * Returns any existing cobertura data files from the given list of projects.
597      */
598     private List<File> getOutputFiles( List<MavenProject> projects )
599     {
600         List<File> files = new ArrayList<File>();
601         for ( MavenProject proj : projects )
602         {
603             if ( isMultiModule( proj ) )
604             {
605                 continue;
606             }
607             File outputFile = new File( proj.getBasedir(), relDataFileName );
608             if ( outputFile.exists() )
609             {
610                 files.add( outputFile );
611             }
612         }
613         return files;
614     }
615 
616     /**
617      * Attempts to make the given childFile relative to the given parentFile.
618      */
619     private String relativize( File parentFile, File childFile )
620     {
621         try
622         {
623             URI parentURI = parentFile.getCanonicalFile().toURI().normalize();
624             URI childURI = childFile.getCanonicalFile().toURI().normalize();
625 
626             URI relativeURI = parentURI.relativize( childURI );
627             if ( relativeURI.isAbsolute() )
628             {
629                 // child is not relative to parent
630                 return null;
631             }
632             String relativePath = relativeURI.getPath();
633             if ( File.separatorChar != '/' )
634             {
635                 relativePath = relativePath.replace( '/', File.separatorChar );
636             }
637             return relativePath;
638         }
639         catch ( Exception e )
640         {
641             getLog().warn( "Failed relativizing " + childFile + " to " + parentFile, e );
642         }
643         return null;
644     }
645 
646     /**
647      * Get the data file which is or will be generated by Cobertura, never <code>null</code>.
648      *
649      * @return the data file
650      */
651     private File getDataFile()
652     {
653         return dataFile;
654     }
655 }