View Javadoc

1   package net.sf.statscm;
2   
3   /*
4    * Copyright 2006 Doug Culnane
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  import net.sf.statsvn.util.SvnStartupUtils;
20  import net.sf.statsvn.util.SvnVersionMismatchException;
21  import net.sf.statcvs.Messages;
22  import net.sf.statcvs.input.LogSyntaxException;
23  import net.sf.statcvs.output.CommandLineParser;
24  import net.sf.statsvn.output.SvnCommandLineParser;
25  import net.sf.statsvn.output.SvnConfigurationOptions;
26  import net.sf.statcvs.output.ConfigurationException;
27  import org.apache.maven.doxia.sink.Sink;
28  import org.apache.maven.doxia.siterenderer.Renderer;
29  import org.apache.maven.reporting.AbstractMavenReport;
30  import org.apache.maven.reporting.MavenReportException;
31  
32  import java.io.File;
33  import java.io.FileInputStream;
34  import java.io.FileNotFoundException;
35  import java.io.FileOutputStream;
36  import java.io.IOException;
37  import java.util.Locale;
38  import java.util.ResourceBundle;
39  
40  import org.apache.maven.model.Reporting;
41  import org.apache.maven.project.MavenProject;
42  
43  /**
44   * Generate a Source Code Management Metrics Report.
45   * 
46   * @goal stats
47   * @phase site
48   */
49  public class StatScmMojo extends AbstractMavenReport
50  {
51  
52      /**
53       * Maven Project. This represents the POM where StatSCM gets most of its configuration from. This object is provided
54       * by Maven.
55       * 
56       * @parameter expression="${project}"
57       * @required
58       * @readonly
59       */
60      private MavenProject project;
61  
62      /**
63       * Each <include> tag inside here gives one Ant-style wildcard pattern indicating what to include. Example:
64       * "src / ** / *.java" matches all java files in the src directory or its subdirectories. Note that "*.java "only
65       * matches java files in the root directory, because '*' does not match subdirectories. (Quotes not needed.)<br/>
66       * <br/>
67       * If not specified, all files will be included.<br/>
68       * <br/>
69       * First we add in all files or all these files and then we subtract all the excluded files.
70       * 
71       * @parameter
72       */
73      private String[] includes;
74  
75      /**
76       * Each &lt;exclude&gt; tag inside here gives one Ant-style wildcard pattern indicating what to exclude. Example:
77       * "** /test/*.java" matches all java files in with a 'test' folder somewhere in the path. Note that "*.java" only
78       * matches java files in the root directory, because '*' does not match subdirectories. (Quotes not needed.)<br/>
79       * <br/>
80       * If not specified, no files will be excluded.<br/>
81       * <br/>
82       * First we add in all files or all these files and then we subtract all the excluded files.
83       * 
84       * @parameter
85       */
86      private String[] excludes;
87  
88      /**
89       * Each &lt;nonDeveloperLogin&gt; tag in inside here gives one scm login account name. Each account name given here
90       * will be excluded from all developer reports. This is useful to reduce noise from administrative and other
91       * non-developer accounts. <br/>
92       * <br/>
93       * Multiple accounts can be excluded by adding a tag for each such account.
94       * 
95       * @parameter
96       */
97      private String[] nonDeveloperLogins;
98  
99      /**
100      * Specify a cache directory for information retrieved from the Subversion server (e.g. line counts). The data will
101      * be saved in this directory. The directory will be created if it doesn't exist. By default, the current user
102      * directory is used. This is only relevant when the SCM is subversion and is ignored if it is CVS.
103      * 
104      * @parameter
105      */
106     private String cacheDir;
107 
108     /**
109      * The contents of the specified file will be included at the top of the report's index page. The file should
110      * contain a valid block-level HTML fragment, for example: <br/>
111      * 
112      * <pre>
113      * &lt;p&gt;
114      *     These are development statistics for the
115      *     &lt;a href=&quot;http://www.statsvn.org&quot;&gt;StatSVN&lt;/a&gt;
116      *     project, created by StatSVN itself.
117      * &lt;/p&gt;
118      * &lt;p&gt;
119      *     &lt;strong&gt;Note:&lt;/strong&gt;
120      *     This report was generated by an unreleased
121      *     development version of StatSVN. It might
122      *     contain features not yet found in the
123      *     official release.
124      * &lt;/p&gt;
125      * </pre>
126      * 
127      * 
128      */
129     private String notesFile;
130     
131     /**
132      * The config file allows you to set some user or chart configurations.
133      * 
134      * @see http://statcvs.sourceforge.net/manual.html#section_config
135      * @parameter
136      */
137    	private String configFile = "src/site/stat-scm.properties";
138     
139 	/**
140      * Set this true to avoid running the stat-scm report in this project. Useful in child projects where the parent pom 
141      * project specifies the stat-scm report.
142      * 
143      * @parameter expression="false"
144      */
145     private boolean skip;
146 
147     /**
148      * Specifies a display title to be used in the reports. The setting in &lt;project&gt;&lt;name&gt; is used if no
149      * title is specified here and the name of the Subversion module will be used as default if no title is otherwise
150      * specified.
151      * 
152      * @parameter
153      */
154     private String title;
155 
156     /**
157      * Configuration for this instance.
158      */
159     private StatConf statConf = new StatConf();
160     
161     /**
162      * For running directly.
163      * @todo This direct call should generate HTML reports by changing the output from Xdoc to html and the outputDir.
164      */
165     public void execute()
166     {
167         getLog().error( "StatSCM can not be run directly!" );
168         getLog().info( "Configure it to run with the \"mvn site\" command." );
169         getLog().info( "See: http://sourceforge.net/tracker/index.php?func=detail&aid=1610964&group_id=182522&atid=901553." );
170     }
171 
172     /**
173      * Main mojo execution method called by plugin environment.
174      * 
175      * @param locale Locale for 118n.
176      * @throws MavenReportException
177      *             Throws an error if any failure.
178      */
179     public void executeReport( Locale locale ) throws MavenReportException
180     {
181         if (skip) 
182         {
183             getLog().info( getMessage("info.greeting.skip") );
184         }
185         else
186         {
187             // Start
188             getLog().info( getMessage("info.greeting.ok") );
189             
190             // Configure
191             try
192             {
193                 getLog().info( "StatSCM Version:" + getStatSCMVersion() );
194                 getLog().info( "Configuring StatXXX" );
195                 StatConf.setConfigFile(configFile);
196                 statConf.configure( project, locale );
197                 configFromMojoProperties();
198                 getLog().info( "SCM Connection Type :" + statConf.getConnectionType() );
199                 getLog().info( "Output Directory    :" + this.getOutputDirectory() );
200             }
201             catch ( ConfigurationException e )
202             {
203                 getLog().error( getMessage( "error.config" ), e );
204                 return;
205             }
206     
207             // Get the Log file
208             SrcManager scm = new SrcManager();
209             if ( !scm.log( statConf.getBaseDirectory(), getLog(), statConf ) )
210             {
211                 getLog().error( getMessage( "error.scmlog.file_missing" ) );
212                 return;
213             }
214     
215             // Create Output dirs
216             if ( !createOutputDirectory() )
217             {
218                 getLog().error( getMessage( "error.ouput.can_not_create_output_folder" )  );
219                 return;
220             }
221     
222             // Perform report
223             try
224             {
225                 if ( statConf.isStatSVN() || statConf.isStatCVS() )
226                 {
227                     if ( statConf.isStatSVN() )
228                     {
229                         doSvnStats();
230                     }
231                     else if ( statConf.isStatCVS() )
232                     {
233                         doCvsStats();
234                     }
235                     doGenerateReport( getSink() );
236                     copyResourceFiles( getOutputDirectory(), getReportingTargetDirectory() );
237                 }
238                 else
239                 {
240                     getLog().error( getMessage( "warn.scm.type_xxx" ) );
241                 }
242             }
243             catch ( Exception e )
244             {
245                 getLog().error( getMessage( "error.config" ), e );
246             }
247         }        
248     }
249 
250     /**
251      * Use StatSVN to generate xDoc output.
252      * 
253      * @throws MavenReportException if there is an error.
254      */
255     private void doSvnStats() throws MavenReportException
256     {
257         Messages.setPrimaryResource( "net.sf.statsvn.statcvs" );
258         try
259         {
260             Messages.setPrimaryResource( "net.sf.statsvn.statcvs" );
261             new SvnCommandLineParser( new String[] { statConf.getSCMLogFileName(), "." } ).parse();
262             SvnStartupUtils.checkSvnVersionSufficient();
263             SvnStartupUtils.checkRepoRootAvailable();
264             net.sf.statsvn.Main.generateDefaultHTMLSuite();
265         }
266         catch ( LogSyntaxException e )
267         {
268             getLog().error( getMessage( "error.scmlog.parsing" ), e );
269             throw new MavenReportException( e.getMessage() );
270         }
271         catch ( IOException e )
272         {
273             getLog().error( "Error generating Subversion Stats.", e );
274             throw new MavenReportException( e.getMessage() );
275         }
276         catch ( ConfigurationException e )
277         {
278             getLog().error( getMessage( "error.config" ), e );
279             throw new MavenReportException( e.getMessage() );
280 
281         }
282         catch ( SvnVersionMismatchException e )
283         {
284             getLog().error( e.getMessage(), e );
285             throw new MavenReportException( e.getMessage() );
286         }
287         catch ( NullPointerException e )
288         {
289         	getLog().warn("Null Pointer: Sometimes happens when local files are not synced with the repository!");
290             getLog().error( e.getMessage(), e );
291             //throw new MavenReportException( e.getMessage() );
292         }
293     }
294 
295     /**
296      * Use StatCVS to generate xDoc output.
297      * 
298      * @throws MavenReportException if there is an error.
299      */
300     private void doCvsStats() throws MavenReportException
301     {
302 
303         try
304         {
305             new CommandLineParser( new String[] { statConf.getSCMLogFileName(), "." } ).parse();
306             net.sf.statcvs.Main.generateDefaultHTMLSuite();
307         }
308         catch ( LogSyntaxException e )
309         {
310             getLog().error( getMessage( "error.scmlog.parsing" ), e );
311             throw new MavenReportException( e.getMessage() );
312         }
313         catch ( IOException e )
314         {
315             getLog().error( "Error generating Subversion Stats.", e );
316             throw new MavenReportException( e.getMessage() );
317         }
318         catch ( ConfigurationException e )
319         {
320             getLog().error( getMessage( "error.config" ), e );
321             throw new MavenReportException( e.getMessage() );
322         }
323     }
324 
325     /**
326      * Create the output directory for generated xdoc output.
327      * 
328      * @return True if directory there or was sucessfully created.
329      */
330     private boolean createOutputDirectory()
331     {
332         File outputDir = new File( getOutputDirectory() );
333         if ( outputDir.exists() )
334         {
335             if ( outputDir.isDirectory() )
336             {
337                 return true;
338             }
339             else
340             {
341                 getLog().error( getMessage( "error.outputDir.file_in_the_way" )
342                                                 + outputDir.getAbsolutePath() );
343                 return false;
344             }
345         }
346         else
347         {
348             if ( outputDir.mkdirs() )
349             {
350                 return true;
351             }
352             else
353             {
354                 getLog().error( "Can  not make output directory at: " + outputDir.getAbsolutePath() );
355                 return false;
356             }
357         }
358     }
359 
360     /**
361      * Use settings in the POM to configure some more things for STATSCM.
362      * 
363      * @throws ConfigurationException if any of the POM settings are invalid 
364      */
365     private void configFromMojoProperties() throws ConfigurationException
366     {
367         configIncludeExclude();
368         configNonDeveloperLogins();
369         configCacheSettings();
370         configNotesFile();
371         configTitle();        
372     }
373 
374     /**
375      * Use POM content to set the title appearing on the window. This takes precedence over the name of the POM project
376      * name, which is the default title.
377      */
378     private void configTitle()
379     {
380         if ( title != null )
381         {
382             String projectName = title.trim();
383             if ( projectName.length() > 0 )
384             {
385                 statConf.setProjectName( projectName );
386             }
387             getLog().info( "Page title: " + statConf.getProjectName() );
388         }
389     }
390 
391     /**
392      * Use POM settings to configure a 'notes' file holding HTML to appear at the top of pages.
393      * 
394      * @throws ConfigurationException if the notes file can't be used
395      */
396     private void configNotesFile() throws ConfigurationException
397     {
398         if ( notesFile != null )
399         {
400             String filename = notesFile.trim();
401             if ( filename.length() > 0 )
402             {
403                 try
404                 {
405                     statConf.setNotesFile( filename );
406                     getLog().info( "Notes file: " + filename );
407                 }
408                 catch ( ConfigurationException e )
409                 {
410                     getLog().info( "Notes file: " + filename );
411                     throw e;
412                     // @todo: convert this to use message file
413 //                    getLog().error( e.getMessage() );
414                 }
415             }
416         }
417     }
418 
419     /**
420      * Use POM setting to configure where the cache file is stored.
421      * 
422      * @throws ConfigurationException if the cache folder cannot be used
423      */
424     private void configCacheSettings() throws ConfigurationException
425     {
426         if ( cacheDir != null )
427         {
428             String dir = cacheDir.trim();
429             if ( dir.length() > 0 )
430             {
431                 try
432                 {
433                     SvnConfigurationOptions.setCacheDir( dir );
434                     getLog().info( "Cache directory: " + dir );
435                 }
436                 catch ( ConfigurationException e )
437                 {
438                     getLog().info( "Cache directory: " + dir );
439                     throw e;
440 //                    getLog().error( getMessage( "error.config.bad_cache_dir" ) + dir );
441                 }
442             }
443         }
444     }
445 
446     /**
447      * Use the parameters configured in the POM to set things up for the list of files that will be included or
448      * excluded.
449      */
450     private void configIncludeExclude()
451     {
452         String patternList = buildIncludeExcludeString( includes );
453         if ( patternList.length() > 0 )
454         {
455             statConf.setIncludePattern( patternList );
456             getLog().info( "Includes: " + patternList );
457         }
458         else
459         {
460             getLog().info( "Include all" );
461         }
462 
463         patternList = buildIncludeExcludeString( excludes );
464         if ( patternList.length() > 0 )
465         {
466             statConf.setExcludePattern( patternList );
467             getLog().info( "Excludes: " + patternList );
468         }
469         else
470         {
471             getLog().info( "Exclude none" );
472         }
473     }
474 
475     /**
476      * Convert an array of strings to a single semi-colon delimited string of the values. Blank values are discarded.
477      * 
478      * @return the delimited string or an empty string if there are no non-blank values.
479      */
480     protected String buildIncludeExcludeString( String[] list )
481     {
482         StringBuffer patternList;
483         patternList = new StringBuffer();
484         if ( list != null )
485         {
486             for ( int i = 0; i < list.length; i++ )
487             {
488                 String include = list[i].trim();
489                 if ( include.indexOf( ';' ) > 0 )
490                 {
491                     getLog().warn(
492                             getMessage( "warn.config.include.exclude.delimiter.1" ) + " ':' "
493                                     + getMessage( "warn.config.include.exclude.delimiter.2" ) + include );
494                 }
495                 if ( include.indexOf( ':' ) > 0 )
496                 {
497                     getLog().warn(
498                             getMessage( "warn.config.include.exclude.delimiter.1" ) + " ';' "
499                                     + getMessage( "warn.config.include.exclude.delimiter.2" ) + include );
500                 }
501                 if ( include.length() > 0 )
502                 {
503                     if ( patternList.length() > 0 )
504                     {
505                         patternList.append( ';' );
506                     }
507                     patternList.append( list[i] );
508                 }
509             }
510         }
511         return patternList.toString();
512     }
513 
514     /**
515      * Set the list of non-developers so these guys will be ignored in SCM info.
516      */
517     private void configNonDeveloperLogins()
518     {
519         if ( nonDeveloperLogins != null )
520         {
521             for ( int i = 0; i < nonDeveloperLogins.length; i++ )
522             {
523                 String login = nonDeveloperLogins[i].trim();
524                 if ( login.length() > 0 )
525                 {
526                     statConf.addNonDeveloperLogin( login );
527                     getLog().info( "Non-developer login: " + login );
528                 }
529             }
530         }
531     }
532 
533     /**
534      * Called my Maven Repoerint API.
535      * 
536      * @return Null becuase we handel the rendering ourselves.
537      */
538     protected Renderer getSiteRenderer()
539     {
540         return null;
541     }
542 
543     /**
544      * String repesentation of the output generated XDOC Directory.
545      * 
546      * @return Absolute path to the generated XDOC Directory.
547      */
548     protected String getOutputDirectory()
549     {
550         return StatConf.getOutputDir();
551     }
552 
553     /**
554      * Returns the StatSCM report target directory.
555      * 
556      * @return Absolute path to the directory with final HTML result.
557      */
558     protected String getReportingTargetDirectory()
559     {
560         String targetBaseDirectory = null;
561         Reporting reporting = getProject().getReporting();
562         if ( reporting != null)
563         {
564             targetBaseDirectory = reporting.getOutputDirectory();
565         }
566         if ( targetBaseDirectory == null)
567         {
568             targetBaseDirectory = statConf.getBaseDirectory().getAbsolutePath()
569                             + statConf.FILE_SEPARATOR + "target"
570                             + statConf.FILE_SEPARATOR + "site";
571         }
572         return targetBaseDirectory + statConf.FILE_SEPARATOR + StatConf.STATSCM_DIR_NAME;
573     }
574 
575     /**
576      * String name of the html file that the Reporting Menu uses for a link.
577      * 
578      * @return Name of Html file to link to.
579      */
580     public String getOutputName()
581     {
582         return StatConf.STATSCM_DIR_NAME + "/statscm";
583     }
584 
585     /**
586      * Menu Item name for Reporting API.
587      * 
588      * @param locale
589      *            Locale for i18n.
590      * @return link menu name.
591      */
592     public String getName( Locale locale )
593     {
594         return "StatSCM";
595     }
596 
597     /**
598      * Description of Report for Reporting API.
599      * 
600      * @param loc Locale fro 118n.
601      * @return i18n Description of Report.
602      */
603     public String getDescription( Locale loc )
604     {
605         return getMessage( "statscm.description" );
606     }
607 
608     /**
609      * Utility method for copying resource files.
610      * 
611      * @param src
612      *            Source Directory.
613      * @param dest
614      *            Target Directory.
615      */
616     private void copyResourceFiles( String src, String dest )
617     {
618     	getLog().info("Copying resources from " + src + " to " + dest);
619     	
620         File srcDir = new File( src );
621         if ( !srcDir.isDirectory() )
622         {
623             getLog().error( "Can not copy reources src is not a directory: " + srcDir.getAbsolutePath() );
624             return;
625         }
626         File destDir = new File( dest );
627         if ( !destDir.isDirectory() )
628         {
629             destDir.mkdirs();
630         }
631 
632         String[] resoures = srcDir.list( new java.io.FilenameFilter()
633         {
634             public boolean accept( File dir, String name )
635             {
636                 String[] extensions = { ".png", ".jar", ".txt" };
637                 String fileName = name.toLowerCase( Locale.getDefault() );
638                 for ( int i = 0; i < extensions.length; i++ )
639                 {
640                     if ( fileName.endsWith( extensions[i] ) )
641                     {
642                         return true;
643                     }
644                 }
645                 return false;
646 
647             }
648         } );
649 
650         for ( int i = 0; i < resoures.length; i++ )
651         {
652             FileInputStream fis = null;
653             FileOutputStream fos = null;
654             try
655             {
656                 fis = new FileInputStream( new File( src + statConf.FILE_SEPARATOR + resoures[i] ) );
657                 fos = new FileOutputStream( new File( dest + statConf.FILE_SEPARATOR + resoures[i] ) );
658                 int b = -1;
659                 while ( ( b = fis.read() ) != -1 )
660                 {
661                     fos.write( b );
662                 }
663 
664                 fis.close();
665                 fos.close();
666             }
667             catch ( FileNotFoundException e )
668             {
669                 getLog().error( "Can not find file " + resoures[i], e );
670             }
671             catch ( IOException e )
672             {
673                 getLog().error( "Error copying file: " + src + "/" + resoures[i], e );
674             }
675             finally
676             {
677                 try
678                 {
679                     if ( fis != null )
680                     {
681                         fis.close();
682                     }
683                     if ( fos != null )
684                     {
685                         fos.close();
686                     }
687                 }
688                 catch ( IOException e )
689                 {
690                     getLog().error( "I/O Exception copying resources.", e );
691                 }
692                 finally
693                 {
694                     fis = null;
695                     fos = null;
696                 }
697             }
698             if ( getLog().isDebugEnabled() )
699             {
700                 getLog().debug(
701                                 "Copied file " + src + statConf.FILE_SEPARATOR + resoures[i] + " to " + dest
702                                                 + statConf.FILE_SEPARATOR + resoures[i] );
703             }
704         }
705     }
706 
707     /**
708      * Create a structured StatSCM / StatSVN report front page.
709      * 
710      * @param sink
711      *            used to write structured report to.
712      */
713     private void doGenerateReport( Sink sink )
714     {
715 
716         // protect against direct runs.
717         if ( sink == null )
718         {
719             return;
720         }
721         
722         sink.head();
723         sink.title();
724         sink.text( "StatSCM" );
725         sink.title_();
726         sink.head_();
727 
728         sink.body();
729         sink.section1();
730 
731         sink.sectionTitle1();
732         sink.text( getDescription( Locale.getDefault() ) );
733         sink.sectionTitle1_();
734 
735         sink.bold();
736         sink.link( "index.html" );
737         sink.text( "Main page" );
738         sink.link_();
739         sink.bold_();
740         // sink.link_();
741         sink.lineBreak();
742 
743         // TODO Think about adding first page links this back in once the StatSVN and StatCVS output stableises.
744 
745         sink.link( "index.html" );
746         sink.figure();
747         sink.figureGraphics( "loc.png" );
748         sink.figure_();
749         sink.link_();
750         sink.lineBreak();
751 
752         sink.text( "Generated using " );
753         sink.link( "http://stat-scm.sourceforge.net/" );
754         sink.text( "StatSCM" + getStatSCMVersion() );
755         sink.link_();
756         sink.text( "." );
757 
758         sink.flush();
759         sink.close();
760     }
761 
762     /**
763      * Get the version of StatSCM.  This uses the filtered resource so that 
764      * Maven automatically sets this at build time.
765      *
766      * @return The pom.version or an empty string if this fails.
767      */
768     private String getStatSCMVersion() {
769         String versionText = getMessage("statscm.version");
770         if (versionText == null) {
771             return "";
772         } else if (versionText.equals("${pom.version}")) {
773             return "";
774         }
775         
776         return " (" + versionText +")";
777     }
778 
779     /**
780      * Access to the Maven Project representation for Reporting API.
781      * 
782      * @return MavenProject.
783      */
784     protected MavenProject getProject()
785     {
786         return project;
787     }
788 
789     /**
790      * Setter to allow unit tests to set up Mojo.
791      * 
792      * @param project Maven Project
793      */
794     public void setProject( MavenProject project )
795     {
796         this.project = project;
797     }
798     
799     public String getMessage(String messageKey) {
800         return ResourceBundle.getBundle( "net.sf.statscm.message", statConf.getLocale() ).getString( messageKey );
801     }
802 
803     void setSkipParameter( boolean b )
804     {
805         skip = b;
806     }
807 
808 	public StatConf getStatConf() {
809 		return statConf;
810 	}
811 
812 	public void setConfigFileParameter(String configFile) throws ConfigurationException {
813 		if (configFile != null) {
814 			this.configFile = configFile;
815 		}
816 	}
817 }