View Javadoc

1   package org.apache.maven.plugin.changelog;
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.BufferedOutputStream;
23  import java.io.BufferedReader;
24  import java.io.File;
25  import java.io.FileNotFoundException;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.StringReader;
29  import java.io.UnsupportedEncodingException;
30  import java.io.Writer;
31  import java.text.ParseException;
32  import java.text.SimpleDateFormat;
33  import java.util.ArrayList;
34  import java.util.Collection;
35  import java.util.Collections;
36  import java.util.Comparator;
37  import java.util.Date;
38  import java.util.HashMap;
39  import java.util.Iterator;
40  import java.util.LinkedList;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Properties;
44  import java.util.ResourceBundle;
45  import java.util.StringTokenizer;
46  import java.util.regex.Matcher;
47  import java.util.regex.Pattern;
48  
49  import org.apache.maven.doxia.sink.Sink;
50  import org.apache.maven.doxia.siterenderer.Renderer;
51  import org.apache.maven.model.Developer;
52  import org.apache.maven.plugin.MojoExecutionException;
53  import org.apache.maven.project.MavenProject;
54  import org.apache.maven.reporting.AbstractMavenReport;
55  import org.apache.maven.reporting.MavenReportException;
56  import org.apache.maven.scm.ChangeFile;
57  import org.apache.maven.scm.ChangeSet;
58  import org.apache.maven.scm.ScmBranch;
59  import org.apache.maven.scm.ScmException;
60  import org.apache.maven.scm.ScmFileSet;
61  import org.apache.maven.scm.ScmResult;
62  import org.apache.maven.scm.ScmRevision;
63  import org.apache.maven.scm.command.changelog.ChangeLogScmResult;
64  import org.apache.maven.scm.command.changelog.ChangeLogSet;
65  import org.apache.maven.scm.manager.ScmManager;
66  import org.apache.maven.scm.provider.ScmProvider;
67  import org.apache.maven.scm.provider.ScmProviderRepository;
68  import org.apache.maven.scm.provider.ScmProviderRepositoryWithHost;
69  import org.apache.maven.scm.provider.svn.repository.SvnScmProviderRepository;
70  import org.apache.maven.scm.repository.ScmRepository;
71  import org.apache.maven.settings.Server;
72  import org.apache.maven.settings.Settings;
73  import org.codehaus.plexus.util.ReaderFactory;
74  import org.codehaus.plexus.util.StringUtils;
75  import org.codehaus.plexus.util.WriterFactory;
76  
77  /**
78   * Generate a changelog report.
79   *
80   * @version $Id: ChangeLogReport.java 942456 2010-05-08 20:04:17Z dennisl $
81   * @goal changelog
82   */
83  public class ChangeLogReport
84      extends AbstractMavenReport
85  {
86      /**
87       * A special token that represents the SCM relative path for a file.
88       * It can be used in <code>displayFileDetailUrl</code>.
89       */
90      private static final String FILE_TOKEN = "%FILE%";
91  
92      /**
93       * A special token that represents a Mantis/Bugzilla/JIRA/etc issue ID.
94       * It can be used in the <code>issueLinkUrl</code>.
95       */
96      private static final String ISSUE_TOKEN = "%ISSUE%";
97  
98      /**
99      * A special token that represents the SCM revision number.
100     * It can be used in <code>displayChangeSetDetailUrl</code>
101     * and <code>displayFileRevDetailUrl</code>.
102     */
103     private static final String REV_TOKEN = "%REV%";
104 
105     /**
106      * The number of days to use as a range, when this is not specified.
107      */
108     private static final int DEFAULT_RANGE = 30;
109 
110     /**
111      * Used to specify the format to use for the dates in the headings of the
112      * report.
113      *
114      * @parameter expression="${changelog.headingDateFormat}" default-value="yyyy-MM-dd"
115      * @since 2.1
116      */
117     private String headingDateFormat = "yyyy-MM-dd";
118 
119     /**
120      * Used to specify whether to build the log using range, tag or date.
121      *
122      * @parameter expression="${changelog.type}" default-value="range"
123      * @required
124      */
125     private String type;
126 
127     /**
128      * Used to specify the number of days of log entries to retrieve.
129      *
130      * @parameter expression="${changelog.range}" default-value="-1"
131      */
132     private int range;
133 
134     /**
135      * Used to specify the absolute date (or list of dates) to start log entries from.
136      *
137      * @parameter
138      */
139     private List dates;
140 
141     /**
142      * Used to specify the tag (or list of tags) to start log entries from.
143      *
144      * @parameter
145      */
146     private List tags;
147 
148     /**
149      * Used to specify the date format of the log entries that are retrieved from your SCM system.
150      *
151      * @parameter expression="${changelog.dateFormat}" default-value="yyyy-MM-dd HH:mm:ss"
152      * @required
153      */
154     private String dateFormat;
155 
156     /**
157      * Input dir. Directory where the files under SCM control are located.
158      *
159      * @parameter expression="${basedir}"
160      * @required
161      */
162     private File basedir;
163 
164     /**
165      * Output file for xml document
166      *
167      * @parameter expression="${project.build.directory}/changelog.xml"
168      * @required
169      */
170     private File outputXML;
171 
172     /**
173      * Allows the user to make changelog regenerate the changelog.xml file for the specified time in minutes.
174      *
175      * @parameter expression="${outputXMLExpiration}" default-value="60"
176      * @required
177      */
178     private int outputXMLExpiration;
179 
180     /**
181      * Comment format string used for interrogating
182      * the revision control system.
183      * Currently only used by the ClearcaseChangeLogGenerator.
184      *
185      * @parameter expression="${changelog.commentFormat}"
186      */
187     private String commentFormat;
188 
189     /**
190      * Output encoding for the xml document
191      *
192      * @parameter expression="${changelog.outputEncoding}" default-value="ISO-8859-1"
193      * @required
194      */
195     private String outputEncoding;
196 
197     /**
198      * The user name (used by svn and starteam protocol).
199      *
200      * @parameter expression="${username}"
201      */
202     private String username;
203 
204     /**
205      * The user password (used by svn and starteam protocol).
206      *
207      * @parameter expression="${password}"
208      */
209     private String password;
210 
211     /**
212      * The private key (used by java svn).
213      *
214      * @parameter expression="${privateKey}"
215      */
216     private String privateKey;
217 
218     /**
219      * The passphrase (used by java svn).
220      *
221      * @parameter expression="${passphrase}"
222      */
223     private String passphrase;
224 
225     /**
226      * The url of tags base directory (used by svn protocol).
227      *
228      * @parameter expression="${tagBase}"
229      */
230     private String tagBase;
231 
232     /**
233      * The URL to view the scm. Basis for external links from the generated report.
234      *
235      * @parameter expression="${project.scm.url}"
236      */
237     protected String scmUrl;
238 
239     /**
240      * The Maven Project Object
241      *
242      * @parameter expression="${project}"
243      * @required
244      * @readonly
245      */
246     private MavenProject project;
247 
248     /**
249      * The directory where the report will be generated
250      *
251      * @parameter expression="${project.reporting.outputDirectory}"
252      * @required
253      * @readonly
254      */
255     private File outputDirectory;
256 
257     /**
258      * @component
259      */
260     private Renderer siteRenderer;
261 
262     /**
263      * @parameter expression="${settings.offline}"
264      * @required
265      * @readonly
266      */
267     private boolean offline;
268 
269     /**
270      * @component
271      */
272     private ScmManager manager;
273 
274     /**
275      * @parameter expression="${settings}"
276      * @required
277      * @readonly
278      */
279     private Settings settings;
280 
281     /**
282      * Allows the user to choose which scm connection to use when connecting to the scm.
283      * Can either be "connection" or "developerConnection".
284      *
285      * @parameter default-value="connection"
286      * @required
287      */
288     private String connectionType;
289 
290     /**
291      * A template string that is used to create the URL to the file details.
292      * There is a special token that you can use in your template:
293      * <ul>
294      * <li><code>%FILE%</code> - this is the path to a file</li>
295      * </ul>
296      * <p>
297      * Example:
298      * <code>http://checkstyle.cvs.sourceforge.net/checkstyle%FILE%?view=markup</code>
299      * </p>
300      * <p>
301      * <strong>Note:</strong> If you don't supply the token in your template,
302      * the path of the file will simply be appended to your template URL.
303      * </p>
304      *
305      * @parameter expression="${displayFileDetailUrl}" default-value="${project.scm.url}"
306      */
307     protected String displayFileDetailUrl;
308 
309     /**
310      * A pattern used to identify 'issue tracker' IDs such as those used by JIRA,
311      * Bugzilla and alike in the SCM commit messages. Any matched patterns
312      * are replaced with <code>issueLinkUrl<code> URL. The default
313      * value is a JIRA-style issue identification pattern.
314      *
315      * @parameter expression="${issueIDRegexPattern}" default-value="[a-zA-Z]{2,}-\\d+"
316      * @required
317      * @since 2.2
318      */
319     private String issueIDRegexPattern;
320 
321     /**
322      * The issue tracker URL used when replacing any matched <code>issueIDRegexPattern</code>
323      * found in the SCM commit messages. The default is URL is the codehaus JIRA
324      * URL. If %ISSUE% is found in the URL it is replaced with the matched issue ID,
325      * otherwise the matched issue ID is appended to the URL.
326      *
327      * @parameter expression="${issueLinkUrl}" default-value="http://jira.codehaus.org/browse/%ISSUE%"
328      * @required
329      * @since 2.2
330      */
331     private String issueLinkUrl;
332 
333     /**
334      * A template string that is used to create the changeset URL.
335      *
336      * If not defined no change set link will be created.
337      *
338      * There is one special token that you can use in your template:
339      * <ul>
340      * <li><code>%REV%</code> - this is the changeset revision</li>
341      * </ul>
342      * <p>
343      * Example:
344      * <code>http://fisheye.sourceforge.net/changelog/a-project/?cs=%REV%</code>
345      * </p>
346      * <p>
347      * <strong>Note:</strong> If you don't supply the %REV% token in your template,
348      * the revision will simply be appended to your template URL.
349      * </p>
350      *
351      * @parameter expression="${displayChangeSetDetailUrl}"
352      * @since 2.2
353      */
354     protected String displayChangeSetDetailUrl;
355 
356     /**
357      * A template string that is used to create the revision aware URL to
358      * the file details in a similar fashion to the <code>displayFileDetailUrl</code>.
359      * When a report contains both file and file revision information, as in the
360      * Change Log report, this template string can be used to create a revision
361      * aware URL to the file details.
362      *
363      * If not defined this template string defaults to the same value as the
364      * <code>displayFileDetailUrl</code> and thus revision number aware links will
365      * not be used.
366      *
367      * There are two special tokens that you can use in your template:
368      * <ul>
369      * <li><code>%FILE%</code> - this is the path to a file</li>
370      * <li><code>%REV%</code> - this is the revision of the file</li>
371      * </ul>
372      * <p>
373      * Example:
374      * <code>http://fisheye.sourceforge.net/browse/a-project/%FILE%?r=%REV%</code>
375      * </p>
376      * <p>
377      * <strong>Note:</strong> If you don't supply the %FILE% token in your template,
378      * the path of the file will simply be appended to your template URL.
379      * </p>
380      *
381      * @parameter expression="${displayFileRevDetailUrl}"
382      * @since 2.2
383      */
384     protected String displayFileRevDetailUrl;
385 
386     /**
387      * List of developers to be shown on the report.
388      *
389      * @parameter expression="${project.developers}"
390      * @since 2.2
391      */
392     protected List developers;
393 
394     // temporary field holder while generating the report
395     private String rptRepository, rptOneRepoParam, rptMultiRepoParam;
396 
397     // field for SCM Connection URL
398     private String connection;
399 
400     // field used to hold a map of the developers by Id
401     private HashMap developersById;
402 
403     // field used to hold a map of the developers by Name
404     private HashMap developersByName;
405 
406     /**
407      * The system properties to use (needed by the perforce scm provider).
408      *
409      * @parameter
410      */
411     private Properties systemProperties;
412 
413     /** {@inheritDoc} */
414     public void executeReport( Locale locale )
415         throws MavenReportException
416     {
417         //check if sources exists <-- required for parent poms
418         if ( !basedir.exists() )
419         {
420             doGenerateEmptyReport( getBundle( locale ), getSink() );
421 
422             return;
423         }
424 
425         initializeDefaultConfigurationParameters();
426 
427         initializeDeveloperMaps();
428 
429         verifySCMTypeParams();
430 
431         if ( systemProperties != null )
432         {
433             // Add all system properties configured by the user
434             Iterator iter = systemProperties.keySet().iterator();
435 
436             while ( iter.hasNext() )
437             {
438                 String key = (String) iter.next();
439 
440                 String value = systemProperties.getProperty( key );
441 
442                 System.setProperty( key, value );
443 
444                 getLog().debug( "Setting system property: " + key + "=" + value );
445             }
446         }
447 
448         doGenerateReport( getChangedSets(), getBundle( locale ), getSink() );
449     }
450 
451     /**
452      * Initializes any configuration parameters that have not/can not be defined
453      * or defaulted by the Mojo API.
454      */
455     private void initializeDefaultConfigurationParameters()
456     {
457         if ( displayFileRevDetailUrl == null || displayFileRevDetailUrl.length() == 0 )
458         {
459             displayFileRevDetailUrl = displayFileDetailUrl;
460         }
461     }
462 
463     /**
464      * Creates maps of the project developers by developer Id and developer Name
465      * for quick lookups.
466      */
467     private void initializeDeveloperMaps()
468     {
469         developersById = new HashMap();
470         developersByName = new HashMap();
471 
472         if ( developers != null )
473         {
474             for ( Iterator i = developers.iterator(); i.hasNext(); )
475             {
476                 Developer developer = (Developer) i.next();
477 
478                 developersById.put( developer.getId(), developer );
479                 developersByName.put( developer.getName(), developer );
480             }
481         }
482     }
483 
484     /**
485      * populates the changedSets field by either connecting to the SCM or using an existing XML generated in a previous
486      * run of the report
487      *
488      * @throws MavenReportException
489      */
490     protected List getChangedSets()
491         throws MavenReportException
492     {
493         List changelogList = null;
494 
495         if ( !outputXML.isAbsolute() )
496         {
497             outputXML = new File( project.getBasedir(), outputXML.getPath() );
498         }
499 
500         if ( outputXML.exists() )
501         {
502             if ( outputXMLExpiration > 0
503                 && outputXMLExpiration * 60000 > System.currentTimeMillis() - outputXML.lastModified() )
504             {
505                 try
506                 {
507                     //ReaderFactory.newReader( outputXML, outputEncoding );
508                     //FileInputStream fIn = new FileInputStream( outputXML );
509 
510                     getLog().info( "Using existing changelog.xml..." );
511 
512                     changelogList = ChangeLog.loadChangedSets( ReaderFactory.newReader( outputXML, outputEncoding ) );
513                 }
514                 catch ( FileNotFoundException e )
515                 {
516                     //do nothing, just regenerate
517                 }
518                 catch ( Exception e )
519                 {
520                     throw new MavenReportException( "An error occurred while parsing " + outputXML.getAbsolutePath(),
521                                                     e );
522                 }
523             }
524         }
525 
526         if ( changelogList == null )
527         {
528             if ( offline )
529             {
530                 throw new MavenReportException( "This report requires online mode." );
531             }
532 
533             getLog().info( "Generating changed sets xml to: " + outputXML.getAbsolutePath() );
534 
535             changelogList = generateChangeSetsFromSCM();
536 
537             try
538             {
539                 writeChangelogXml( changelogList );
540             }
541             catch ( FileNotFoundException e )
542             {
543                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
544             }
545             catch ( UnsupportedEncodingException e )
546             {
547                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
548             }
549             catch ( IOException e )
550             {
551                 throw new MavenReportException( "Can't create " + outputXML.getAbsolutePath(), e );
552             }
553         }
554 
555         return changelogList;
556     }
557 
558     private void writeChangelogXml( List changelogList )
559         throws FileNotFoundException, UnsupportedEncodingException, IOException
560     {
561         StringBuffer changelogXml = new StringBuffer();
562 
563         changelogXml.append( "<?xml version=\"1.0\" encoding=\"" ).append( outputEncoding ).append( "\"?>\n" );
564         changelogXml.append( "<changelog>" );
565 
566         for ( Iterator sets = changelogList.iterator(); sets.hasNext(); )
567         {
568             changelogXml.append( "\n  " );
569 
570             ChangeLogSet changelogSet = (ChangeLogSet) sets.next();
571             String changeset = changelogSet.toXML( outputEncoding );
572 
573             //remove xml header
574             if ( changeset.startsWith( "<?xml" ) )
575             {
576                 int idx = changeset.indexOf( "?>" ) + 2;
577                 changeset = changeset.substring( idx );
578             }
579 
580             changelogXml.append( changeset );
581         }
582 
583         changelogXml.append( "\n</changelog>" );
584 
585         outputXML.getParentFile().mkdirs();
586 
587         //PrintWriter pw = new PrintWriter( new BufferedOutputStream( new FileOutputStream( outputXML ) ) );
588         //pw.write( changelogXml.toString() );
589         //pw.flush();
590         //pw.close();
591         // MCHANGELOG-86
592         Writer writer = WriterFactory.newWriter( new BufferedOutputStream( new FileOutputStream( outputXML ) ), outputEncoding );
593         writer.write( changelogXml.toString() );
594         writer.flush();
595         writer.close();
596     }
597 
598     /**
599      * creates a ChangeLog object and then connects to the SCM to generate the changed sets
600      *
601      * @return changedlogsets generated from the SCM
602      * @throws MavenReportException
603      */
604     protected List generateChangeSetsFromSCM()
605         throws MavenReportException
606     {
607         try
608         {
609             List changeSets = new ArrayList();
610 
611             ScmRepository repository = getScmRepository();
612 
613             ScmProvider provider = manager.getProviderByRepository( repository );
614 
615             ChangeLogScmResult result;
616 
617             if ( "range".equals( type ) )
618             {
619                 result = provider.changeLog( repository, new ScmFileSet( basedir ), null, null, range, (ScmBranch) null,
620                                              dateFormat );
621 
622                 checkResult( result );
623 
624                 changeSets.add( result.getChangeLog() );
625             }
626             else if ( "tag".equals( type ) )
627             {
628                 if ( repository.getProvider().equals( "svn" ) )
629                 {
630                     throw new MavenReportException( "The type '" + type + "' isn't supported for svn." );
631                 }
632 
633                 Iterator tagsIter = tags.iterator();
634 
635                 String startTag = (String) tagsIter.next();
636                 String endTag = null;
637 
638                 if ( tagsIter.hasNext() )
639                 {
640                     while ( tagsIter.hasNext() )
641                     {
642                         endTag = (String) tagsIter.next();
643 
644                         result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
645                                                      new ScmRevision( endTag ) );
646 
647                         checkResult( result );
648 
649                         changeSets.add( result.getChangeLog() );
650 
651                         startTag = endTag;
652                     }
653                 }
654                 else
655                 {
656                     result = provider.changeLog( repository, new ScmFileSet( basedir ), new ScmRevision( startTag ),
657                                                  new ScmRevision( endTag ) );
658 
659                     checkResult( result );
660 
661                     changeSets.add( result.getChangeLog() );
662                 }
663             }
664             else if ( "date".equals( type ) )
665             {
666                 Iterator dateIter = dates.iterator();
667 
668                 String startDate = (String) dateIter.next();
669                 String endDate = null;
670 
671                 if ( dateIter.hasNext() )
672                 {
673                     while ( dateIter.hasNext() )
674                     {
675                         endDate = (String) dateIter.next();
676 
677                         result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
678                                                      parseDate( endDate ), 0, (ScmBranch) null );
679 
680                         checkResult( result );
681 
682                         changeSets.add( result.getChangeLog() );
683 
684                         startDate = endDate;
685                     }
686                 }
687                 else
688                 {
689                     result = provider.changeLog( repository, new ScmFileSet( basedir ), parseDate( startDate ),
690                                                  parseDate( endDate ), 0, (ScmBranch) null );
691 
692                     checkResult( result );
693 
694                     changeSets.add( result.getChangeLog() );
695                 }
696             }
697             else
698             {
699                 throw new MavenReportException( "The type '" + type + "' isn't supported." );
700             }
701 
702             return changeSets;
703         }
704         catch ( ScmException e )
705         {
706             throw new MavenReportException( "Cannot run changelog command : ", e );
707         }
708         catch ( MojoExecutionException e )
709         {
710             throw new MavenReportException( "An error has occurred during changelog command : ", e );
711         }
712     }
713 
714     /**
715      * Converts the localized date string pattern to date object.
716      *
717      * @return A date
718      */
719     private Date parseDate( String date )
720         throws MojoExecutionException
721     {
722         if ( date == null || date.trim().length() == 0 )
723         {
724             return null;
725         }
726 
727         SimpleDateFormat formatter = new SimpleDateFormat( "yyyy-MM-dd" );
728 
729         try
730         {
731             return formatter.parse( date );
732         }
733         catch ( ParseException e )
734         {
735             throw new MojoExecutionException( "Please use this date pattern: " + formatter.toLocalizedPattern(), e );
736         }
737     }
738 
739     public ScmRepository getScmRepository()
740         throws ScmException
741     {
742         ScmRepository repository;
743 
744         try
745         {
746             repository = manager.makeScmRepository( getConnection() );
747 
748             ScmProviderRepository providerRepo = repository.getProviderRepository();
749 
750             if ( !StringUtils.isEmpty( username ) )
751             {
752                 providerRepo.setUser( username );
753             }
754 
755             if ( !StringUtils.isEmpty( password ) )
756             {
757                 providerRepo.setPassword( password );
758             }
759 
760             if ( repository.getProviderRepository() instanceof ScmProviderRepositoryWithHost )
761             {
762                 ScmProviderRepositoryWithHost repo = (ScmProviderRepositoryWithHost) repository.getProviderRepository();
763 
764                 loadInfosFromSettings( repo );
765 
766                 if ( !StringUtils.isEmpty( username ) )
767                 {
768                     repo.setUser( username );
769                 }
770 
771                 if ( !StringUtils.isEmpty( password ) )
772                 {
773                     repo.setPassword( password );
774                 }
775 
776                 if ( !StringUtils.isEmpty( privateKey ) )
777                 {
778                     repo.setPrivateKey( privateKey );
779                 }
780 
781                 if ( !StringUtils.isEmpty( passphrase ) )
782                 {
783                     repo.setPassphrase( passphrase );
784                 }
785             }
786 
787             if ( !StringUtils.isEmpty( tagBase ) && repository.getProvider().equals( "svn" ) )
788             {
789                 SvnScmProviderRepository svnRepo = (SvnScmProviderRepository) repository.getProviderRepository();
790 
791                 svnRepo.setTagBase( tagBase );
792             }
793         }
794         catch ( Exception e )
795         {
796             throw new ScmException( "Can't load the scm provider.", e );
797         }
798 
799         return repository;
800     }
801 
802     /**
803      * Load username password from settings if user has not set them in JVM properties
804      *
805      * @param repo
806      */
807     private void loadInfosFromSettings( ScmProviderRepositoryWithHost repo )
808     {
809         if ( username == null || password == null )
810         {
811             String host = repo.getHost();
812 
813             int port = repo.getPort();
814 
815             if ( port > 0 )
816             {
817                 host += ":" + port;
818             }
819 
820             Server server = this.settings.getServer( host );
821 
822             if ( server != null )
823             {
824                 if ( username == null )
825                 {
826                     username = this.settings.getServer( host ).getUsername();
827                 }
828 
829                 if ( password == null )
830                 {
831                     password = this.settings.getServer( host ).getPassword();
832                 }
833 
834                 if ( privateKey == null )
835                 {
836                     privateKey = this.settings.getServer( host ).getPrivateKey();
837                 }
838 
839                 if ( passphrase == null )
840                 {
841                     passphrase = this.settings.getServer( host ).getPassphrase();
842                 }
843             }
844         }
845     }
846 
847     public void checkResult( ScmResult result )
848         throws MojoExecutionException
849     {
850         if ( !result.isSuccess() )
851         {
852             getLog().error( "Provider message:" );
853 
854             getLog().error( result.getProviderMessage() == null ? "" : result.getProviderMessage() );
855 
856             getLog().error( "Command output:" );
857 
858             getLog().error( result.getCommandOutput() == null ? "" : result.getCommandOutput() );
859 
860             throw new MojoExecutionException( "Command failed." );
861         }
862     }
863 
864     /**
865      * used to retrieve the SCM connection string
866      *
867      * @return the url string used to connect to the SCM
868      * @throws MavenReportException when there is insufficient information to retrieve the SCM connection string
869      */
870     protected String getConnection()
871         throws MavenReportException
872     {
873         if ( this.connection != null )
874         {
875             return connection;
876         }
877 
878         if ( project.getScm() == null )
879         {
880             throw new MavenReportException( "SCM Connection is not set." );
881         }
882 
883         String scmConnection = project.getScm().getConnection();
884         if ( StringUtils.isNotEmpty( scmConnection ) && "connection".equals( connectionType.toLowerCase() ) )
885         {
886             connection = scmConnection;
887         }
888 
889         String scmDeveloper = project.getScm().getDeveloperConnection();
890         if ( StringUtils.isNotEmpty( scmDeveloper ) && "developerconnection".equals( connectionType.toLowerCase() ) )
891         {
892             connection = scmDeveloper;
893         }
894 
895         if ( StringUtils.isEmpty( connection ) )
896         {
897             throw new MavenReportException( "SCM Connection is not set." );
898         }
899 
900         return connection;
901     }
902 
903     /**
904      * checks whether there are enough configuration parameters to generate the report
905      *
906      * @throws MavenReportException when there is insufficient paramters to generate the report
907      */
908     private void verifySCMTypeParams()
909         throws MavenReportException
910     {
911         if ( "range".equals( type ) )
912         {
913             if ( range == -1 )
914             {
915                 range = DEFAULT_RANGE;
916             }
917         }
918         else if ( "date".equals( type ) )
919         {
920             if ( dates == null )
921             {
922                 throw new MavenReportException(
923                     "The dates parameter is required when type=\"date\"."
924                     + " The value should be the absolute date for the start of the log." );
925             }
926         }
927         else if ( "tag".equals( type ) )
928         {
929             if ( tags == null )
930             {
931                 throw new MavenReportException( "The tags parameter is required when type=\"tag\"." );
932             }
933         }
934         else
935         {
936             throw new MavenReportException( "The type parameter has an invalid value: " + type
937                 + ".  The value should be \"range\", \"date\", or \"tag\"." );
938         }
939     }
940 
941     /**
942      * generates an empty report in case there are no sources to generate a report with
943      *
944      * @param bundle the resource bundle to retrieve report phrases from
945      * @param sink   the report formatting tool
946      */
947     protected void doGenerateEmptyReport( ResourceBundle bundle, Sink sink )
948     {
949         sink.head();
950         sink.title();
951         sink.text( bundle.getString( "report.changelog.header" ) );
952         sink.title_();
953         sink.head_();
954 
955         sink.body();
956         sink.section1();
957 
958         sink.sectionTitle1();
959         sink.text( bundle.getString( "report.changelog.mainTitle" ) );
960         sink.sectionTitle1_();
961 
962         sink.paragraph();
963         sink.text( bundle.getString( "report.changelog.nosources" ) );
964         sink.paragraph_();
965 
966         sink.section1_();
967 
968         sink.body_();
969         sink.flush();
970         sink.close();
971     }
972 
973     /**
974      * method that generates the report for this mojo. This method is overridden by dev-activity and file-activity mojo
975      *
976      * @param changeLogSets changed sets to generate the report from
977      * @param bundle        the resource bundle to retrieve report phrases from
978      * @param sink          the report formatting tool
979      */
980     protected void doGenerateReport( List changeLogSets, ResourceBundle bundle, Sink sink )
981     {
982         sink.head();
983         sink.title();
984         sink.text( bundle.getString( "report.changelog.header" ) );
985         sink.title_();
986         sink.head_();
987 
988         sink.body();
989         sink.section1();
990 
991         sink.sectionTitle1();
992         sink.text( bundle.getString( "report.changelog.mainTitle" ) );
993         sink.sectionTitle1_();
994 
995         // Summary section
996         doSummarySection( changeLogSets, bundle, sink );
997 
998         for ( Iterator sets = changeLogSets.iterator(); sets.hasNext(); )
999         {
1000             ChangeLogSet changeLogSet = (ChangeLogSet) sets.next();
1001 
1002             doChangedSet( changeLogSet, bundle, sink );
1003         }
1004 
1005         sink.section1_();
1006         sink.body_();
1007 
1008         sink.flush();
1009         sink.close();
1010     }
1011 
1012     /**
1013      * generates the report summary section of the report
1014      *
1015      * @param changeLogSets changed sets to generate the report from
1016      * @param bundle        the resource bundle to retrieve report phrases from
1017      * @param sink          the report formatting tool
1018      */
1019     private void doSummarySection( List changeLogSets, ResourceBundle bundle, Sink sink )
1020     {
1021         sink.paragraph();
1022 
1023         sink.text( bundle.getString( "report.changelog.ChangedSetsTotal" ) );
1024         sink.text( ": " + changeLogSets.size() );
1025 
1026         sink.paragraph_();
1027     }
1028 
1029     /**
1030      * generates a section of the report referring to a changeset
1031      *
1032      * @param set    the current ChangeSet to generate this section of the report
1033      * @param bundle the resource bundle to retrieve report phrases from
1034      * @param sink   the report formatting tool
1035      */
1036     private void doChangedSet( ChangeLogSet set, ResourceBundle bundle, Sink sink )
1037     {
1038         sink.section2();
1039 
1040         doChangeSetTitle( set, bundle, sink );
1041 
1042         doSummary( set, bundle, sink );
1043 
1044         doChangedSetTable( set.getChangeSets(), bundle, sink );
1045 
1046         sink.section2_();
1047     }
1048 
1049     /**
1050      * Generate the title for the report.
1051      *
1052      * @param set    change set to generate the report from
1053      * @param bundle the resource bundle to retrieve report phrases from
1054      * @param sink   the report formatting tool
1055      */
1056     protected void doChangeSetTitle( ChangeLogSet set, ResourceBundle bundle, Sink sink )
1057     {
1058         sink.sectionTitle2();
1059 
1060         SimpleDateFormat headingDateFormater = new SimpleDateFormat( headingDateFormat );
1061 
1062         if ( "tag".equals( type ) )
1063         {
1064             if ( set.getStartVersion() == null || set.getStartVersion().getName() == null )
1065             {
1066                 sink.text( bundle.getString( "report.SetTagCreation" ) );
1067                 if ( set.getEndVersion() != null && set.getEndVersion().getName() != null )
1068                 {
1069                     sink.text( " " + bundle.getString( "report.SetTagUntil" ) + " '" + set.getEndVersion() + "'" );
1070                 }
1071             }
1072             else if ( set.getEndVersion() == null || set.getEndVersion().getName() == null )
1073             {
1074                 sink.text( bundle.getString( "report.SetTagSince" ) );
1075                 sink.text( " '" + set.getStartVersion() + "'" );
1076             }
1077             else
1078             {
1079                 sink.text( bundle.getString( "report.SetTagBetween" ) );
1080                 sink.text( " '" + set.getStartVersion() + "' " + bundle.getString( "report.And" ) + " '"
1081                     + set.getEndVersion() + "'" );
1082             }
1083         }
1084         else  if ( set.getStartDate() == null )
1085         {
1086             sink.text( bundle.getString( "report.SetRangeUnknown" ) );
1087         }
1088         else if ( set.getEndDate() == null )
1089         {
1090             sink.text( bundle.getString( "report.SetRangeSince" ) );
1091             sink.text( " " + headingDateFormater.format( set.getStartDate() ) );
1092         }
1093         else
1094         {
1095             sink.text( bundle.getString( "report.SetRangeBetween" ) );
1096             sink.text( " " + headingDateFormater.format( set.getStartDate() )
1097                 + " " + bundle.getString( "report.And" ) + " "
1098                 + headingDateFormater.format( set.getEndDate() ) );
1099         }
1100         sink.sectionTitle2_();
1101     }
1102 
1103     /**
1104      * Generate the summary section of the report.
1105      *
1106      * @param set    change set to generate the report from
1107      * @param bundle the resource bundle to retrieve report phrases from
1108      * @param sink   the report formatting tool
1109      */
1110     protected void doSummary( ChangeLogSet set, ResourceBundle bundle, Sink sink )
1111     {
1112         sink.paragraph();
1113         sink.text( bundle.getString( "report.TotalCommits" ) );
1114         sink.text( ": " + set.getChangeSets().size() );
1115         sink.lineBreak();
1116         sink.text( bundle.getString( "report.changelog.FilesChanged" ) );
1117         sink.text( ": " + countFilesChanged( set.getChangeSets() ) );
1118         sink.paragraph_();
1119     }
1120 
1121     /**
1122      * counts the number of files that were changed in the specified SCM
1123      *
1124      * @param entries a collection of SCM changes
1125      * @return number of files changed for the changedsets
1126      */
1127     protected long countFilesChanged( Collection entries )
1128     {
1129         if ( entries == null )
1130         {
1131             return 0;
1132         }
1133 
1134         if ( entries.isEmpty() )
1135         {
1136             return 0;
1137         }
1138 
1139         HashMap fileList = new HashMap();
1140 
1141         for ( Iterator i = entries.iterator(); i.hasNext(); )
1142         {
1143             ChangeSet entry = (ChangeSet) i.next();
1144 
1145             List files = entry.getFiles();
1146 
1147             for ( Iterator fileIterator = files.iterator(); fileIterator.hasNext(); )
1148             {
1149                 ChangeFile file = (ChangeFile) fileIterator.next();
1150 
1151                 String name = file.getName();
1152 
1153                 if ( fileList.containsKey( name ) )
1154                 {
1155                     LinkedList list = (LinkedList) fileList.get( name );
1156 
1157                     list.add( file );
1158                 }
1159                 else
1160                 {
1161                     LinkedList list = new LinkedList();
1162 
1163                     list.add( file );
1164 
1165                     fileList.put( name, list );
1166                 }
1167             }
1168         }
1169 
1170         return fileList.size();
1171     }
1172 
1173     /**
1174      * generates the report table showing the SCM log entries
1175      *
1176      * @param entries a list of change log entries to generate the report from
1177      * @param bundle  the resource bundle to retrieve report phrases from
1178      * @param sink    the report formatting tool
1179      */
1180     private void doChangedSetTable( Collection entries, ResourceBundle bundle, Sink sink )
1181     {
1182         sink.table();
1183 
1184         sink.tableRow();
1185         sink.tableHeaderCell();
1186         sink.text( bundle.getString( "report.changelog.timestamp" ) );
1187         sink.tableHeaderCell_();
1188         sink.tableHeaderCell();
1189         sink.text( bundle.getString( "report.changelog.author" ) );
1190         sink.tableHeaderCell_();
1191         sink.tableHeaderCell();
1192         sink.text( bundle.getString( "report.changelog.details" ) );
1193         sink.tableHeaderCell_();
1194         sink.tableRow_();
1195 
1196         initReportUrls();
1197 
1198         List sortedEntries = new ArrayList( entries );
1199         Collections.sort( sortedEntries, new Comparator()
1200         {
1201             public int compare( Object arg0, Object arg1 )
1202             {
1203                 ChangeSet changeSet0 = (ChangeSet) arg0;
1204                 ChangeSet changeSet1 = (ChangeSet) arg1;
1205                 return changeSet1.getDate().compareTo( changeSet0.getDate() );
1206             }
1207         } );
1208 
1209         for ( Iterator i = sortedEntries.iterator(); i.hasNext(); )
1210         {
1211             ChangeSet entry = (ChangeSet) i.next();
1212 
1213             doChangedSetDetail( entry, bundle, sink );
1214         }
1215 
1216         sink.table_();
1217     }
1218 
1219     /**
1220      * reports on the details of an SCM entry log
1221      *
1222      * @param entry  an SCM entry to generate the report from
1223      * @param bundle the resource bundle to retrieve report phrases from
1224      * @param sink   the report formatting tool
1225      */
1226     private void doChangedSetDetail( ChangeSet entry, ResourceBundle bundle, Sink sink )
1227     {
1228         sink.tableRow();
1229 
1230         sink.tableCell();
1231         sink.text( entry.getDateFormatted() + " " + entry.getTimeFormatted() );
1232         sink.tableCell_();
1233 
1234         sink.tableCell();
1235 
1236         sinkAuthorDetails( sink, entry.getAuthor() );
1237 
1238         sink.tableCell_();
1239 
1240         sink.tableCell();
1241         //doRevision( entry.getFiles(), bundle, sink );
1242         doChangedFiles( entry.getFiles(), sink );
1243         sink.lineBreak();
1244         StringReader sr = new StringReader( entry.getComment() );
1245         BufferedReader br = new BufferedReader( sr );
1246         String line;
1247         try
1248         {
1249             if ( ( issueIDRegexPattern != null && issueIDRegexPattern.length() > 0 )
1250                 && ( issueLinkUrl != null && issueLinkUrl.length() > 0 ) )
1251             {
1252                 Pattern pattern = Pattern.compile( issueIDRegexPattern );
1253 
1254                 line = br.readLine();
1255 
1256                 while ( line != null )
1257                 {
1258                     sinkIssueLink( sink, line, pattern );
1259 
1260                     line = br.readLine();
1261                     if ( line != null )
1262                     {
1263                         sink.lineBreak();
1264                     }
1265                 }
1266             }
1267             else
1268             {
1269                 line = br.readLine();
1270 
1271                 while ( line != null )
1272                 {
1273                     sink.text( line );
1274                     line = br.readLine();
1275                     if ( line != null )
1276                     {
1277                         sink.lineBreak();
1278                     }
1279                 }
1280             }
1281         }
1282         catch ( IOException e )
1283         {
1284             getLog().warn( "Unable to read the comment of a ChangeSet as a stream." );
1285         }
1286         finally
1287         {
1288             if ( br != null )
1289             {
1290                 try
1291                 {
1292                     br.close();
1293                 }
1294                 catch ( IOException e )
1295                 {
1296                     getLog().warn( "Unable to close a reader." );
1297                 }
1298             }
1299             if ( sr != null )
1300             {
1301                 sr.close();
1302             }
1303         }
1304         sink.tableCell_();
1305 
1306         sink.tableRow_();
1307     }
1308 
1309     private void sinkIssueLink( Sink sink, String line, Pattern pattern )
1310     {
1311         // replace any ticket patterns found.
1312 
1313         Matcher matcher = pattern.matcher( line );
1314 
1315         int currLoc = 0;
1316 
1317         while ( matcher.find() )
1318         {
1319             String match = matcher.group();
1320 
1321             String link;
1322 
1323             if ( issueLinkUrl.indexOf( ISSUE_TOKEN ) > 0 )
1324             {
1325                 link = issueLinkUrl.replaceAll( ISSUE_TOKEN, match );
1326             }
1327             else
1328             {
1329                 if ( issueLinkUrl.endsWith( "/" ) )
1330                 {
1331                     link = issueLinkUrl;
1332                 }
1333                 else
1334                 {
1335                     link = issueLinkUrl + "/";
1336                 }
1337 
1338                 link += match;
1339             }
1340 
1341             int startOfMatch = matcher.start();
1342 
1343             String unmatchedText = line.substring( currLoc, startOfMatch );
1344 
1345             currLoc = matcher.end();
1346 
1347             sink.text( unmatchedText );
1348 
1349             sink.link( link );
1350             sink.text( match );
1351             sink.link_();
1352         }
1353 
1354         sink.text( line.substring( currLoc ) );
1355     }
1356 
1357     /**
1358      * If the supplied author is a known developer this method outputs a
1359      * link to the team members report, or alternatively, if the supplied
1360      * author is unknown, outputs the author's name as plain text.
1361      *
1362      * @param sink Sink to use for outputting
1363      * @param author The author's name.
1364      */
1365     protected void sinkAuthorDetails( Sink sink, String author )
1366     {
1367         Developer developer = (Developer) developersById.get( author );
1368 
1369         if ( developer == null )
1370         {
1371             developer = (Developer) developersByName.get( author );
1372         }
1373 
1374         if ( developer != null )
1375         {
1376             sink.link( "team-list.html#" + developer.getId() );
1377             sink.text( developer.getName() );
1378             sink.link_();
1379         }
1380         else
1381         {
1382             sink.text( author );
1383         }
1384     }
1385 
1386     /**
1387      * populates the report url used to create links from certain elements of the report
1388      */
1389     protected void initReportUrls()
1390     {
1391         if ( scmUrl != null )
1392         {
1393             int idx = scmUrl.indexOf( '?' );
1394 
1395             if ( idx > 0 )
1396             {
1397                 rptRepository = scmUrl.substring( 0, idx );
1398 
1399                 if ( scmUrl.equals( displayFileDetailUrl ) )
1400                 {
1401                     String rptTmpMultiRepoParam = scmUrl.substring( rptRepository.length() );
1402 
1403                     rptOneRepoParam = "?" + rptTmpMultiRepoParam.substring( 1 );
1404 
1405                     rptMultiRepoParam = "&" + rptTmpMultiRepoParam.substring( 1 );
1406                 }
1407             }
1408             else
1409             {
1410                 rptRepository = scmUrl;
1411 
1412                 rptOneRepoParam = "";
1413 
1414                 rptMultiRepoParam = "";
1415             }
1416         }
1417     }
1418 
1419     /**
1420      * generates the section of the report listing all the files revisions
1421      *
1422      * @param files list of files to generate the reports from
1423      * @param sink  the report formatting tool
1424      */
1425     private void doChangedFiles( List files, Sink sink )
1426     {
1427         for ( Iterator i = files.iterator(); i.hasNext(); )
1428         {
1429             ChangeFile file = (ChangeFile) i.next();
1430             sinkLogFile( sink, file.getName(), file.getRevision() );
1431         }
1432     }
1433 
1434     /**
1435      * generates the section of the report detailing the revisions made and the files changed
1436      *
1437      * @param sink     the report formatting tool
1438      * @param name     filename of the changed file
1439      * @param revision the revision code for this file
1440      */
1441     private void sinkLogFile( Sink sink, String name, String revision )
1442     {
1443         try
1444         {
1445             String connection = getConnection();
1446 
1447             generateLinks( connection, name, revision, sink );
1448         }
1449         catch ( Exception e )
1450         {
1451             getLog().debug( e );
1452 
1453             sink.text( name + " v " + revision );
1454         }
1455 
1456         sink.lineBreak();
1457     }
1458 
1459     /**
1460      * attaches the http links from the changed files
1461      *
1462      * @param connection the string used to connect to the SCM
1463      * @param name       filename of the file that was changed
1464      * @param sink       the report formatting tool
1465      */
1466     protected void generateLinks( String connection, String name, Sink sink )
1467     {
1468         generateLinks( connection, name, null, sink );
1469     }
1470 
1471     /**
1472      * attaches the http links from the changed files
1473      *
1474      * @param connection the string used to connect to the SCM
1475      * @param name       filename of the file that was changed
1476      * @param revision   the revision code
1477      * @param sink       the report formatting tool
1478      */
1479     protected void generateLinks( String connection, String name, String revision, Sink sink )
1480     {
1481         String linkFile = null;
1482         String linkRev = null;
1483 
1484         if ( revision != null )
1485         {
1486             linkFile = displayFileRevDetailUrl;
1487         }
1488         else
1489         {
1490             linkFile = displayFileDetailUrl;
1491         }
1492 
1493         if ( linkFile != null )
1494         {
1495             if ( !scmUrl.equals( linkFile ) )
1496             {
1497                 // Use the given URL to create links to the files
1498 
1499                 if ( linkFile.indexOf( FILE_TOKEN ) > 0 )
1500                 {
1501                     linkFile = linkFile.replaceAll( FILE_TOKEN, name );
1502                 }
1503                 else
1504                 {
1505                     // This is here so that we are backwards compatible with the
1506                     // format used before the special token was introduced
1507 
1508                     linkFile = linkFile + name;
1509                 }
1510 
1511                 // Use the given URL to create links to the files
1512 
1513                 if (  revision != null && linkFile.indexOf( REV_TOKEN ) > 0 )
1514                 {
1515                     linkFile = linkFile.replaceAll( REV_TOKEN, revision );
1516                 }
1517             }
1518             else if ( connection.startsWith( "scm:perforce" ) )
1519             {
1520                 String path = getAbsolutePath( displayFileDetailUrl, name );
1521                 linkFile = path + "?ac=22";
1522                 if ( revision != null )
1523                 {
1524                     linkRev = path + "?ac=64&rev=" + revision;
1525                 }
1526             }
1527             else if ( connection.startsWith( "scm:clearcase" ) )
1528             {
1529                 String path = getAbsolutePath( displayFileDetailUrl, name );
1530                 linkFile = path + rptOneRepoParam;
1531             }
1532             else if ( connection.indexOf( "cvsmonitor.pl" ) > 0 )
1533             {
1534                 String module = rptOneRepoParam.replaceAll( "^.*(&amp;module=.*?(?:&amp;|$)).*$", "$1" );
1535                 linkFile = displayFileDetailUrl + "?cmd=viewBrowseFile" + module + "&file=" + name;
1536                 if ( revision != null )
1537                 {
1538                     linkRev =
1539                         rptRepository + "?cmd=viewBrowseVersion" + module + "&file=" + name + "&version=" + revision;
1540                 }
1541             }
1542             else
1543             {
1544                 String path = getAbsolutePath( displayFileDetailUrl, name );
1545                 linkFile = path + rptOneRepoParam;
1546                 if ( revision != null )
1547                 {
1548                     linkRev = path + "?rev=" + revision + "&content-type=text/vnd.viewcvs-markup" + rptMultiRepoParam;
1549                 }
1550             }
1551         }
1552 
1553         if ( linkFile != null )
1554         {
1555             sink.link( linkFile );
1556             sinkFileName( name, sink );
1557             sink.link_();
1558         }
1559         else
1560         {
1561             sinkFileName( name, sink );
1562         }
1563 
1564         sink.text( " " );
1565 
1566         if ( linkRev == null && revision != null && displayChangeSetDetailUrl != null )
1567         {
1568             if ( displayChangeSetDetailUrl.indexOf( REV_TOKEN ) > 0 )
1569             {
1570                 linkRev = displayChangeSetDetailUrl.replaceAll( REV_TOKEN, revision );
1571             }
1572             else
1573             {
1574                 linkRev = displayChangeSetDetailUrl + revision;
1575             }
1576         }
1577 
1578         if ( linkRev != null )
1579         {
1580             sink.link( linkRev );
1581             sink.text( "v " + revision );
1582             sink.link_();
1583         }
1584         else if ( revision != null )
1585         {
1586             sink.text( "v " + revision );
1587         }
1588     }
1589 
1590     /**
1591      * Encapsulates the logic for rendering the name with a bolded markup.
1592      *
1593      * @param name filename of the file that was changed
1594      * @param sink the report formatting tool
1595      */
1596     private void sinkFileName( String name, Sink sink )
1597     {
1598         name = name.replaceAll( "\\\\", "/" );
1599         int pos = name.lastIndexOf( '/' );
1600 
1601         String head;
1602         String tail;
1603         if ( pos < 0 )
1604         {
1605             head = "";
1606             tail = name;
1607         }
1608         else
1609         {
1610             head = name.substring( 0, pos ) + "/";
1611             tail = name.substring( pos + 1 );
1612         }
1613 
1614         sink.text( head );
1615         sink.bold();
1616         sink.text( tail );
1617         sink.bold_();
1618     }
1619 
1620     /**
1621      * calculates the path from a base directory to a target file
1622      *
1623      * @param base   base directory to create the absolute path from
1624      * @param target target file to create the absolute path to
1625      */
1626     private String getAbsolutePath( final String base, final String target )
1627     {
1628         String absPath = "";
1629 
1630         StringTokenizer baseTokens = new StringTokenizer( base.replaceAll( "\\\\", "/" ), "/", true );
1631 
1632         StringTokenizer targetTokens = new StringTokenizer( target.replaceAll( "\\\\", "/" ), "/" );
1633 
1634         String targetRoot = targetTokens.nextToken();
1635 
1636         while ( baseTokens.hasMoreTokens() )
1637         {
1638             String baseToken = baseTokens.nextToken();
1639 
1640             if ( baseToken.equals( targetRoot ) )
1641             {
1642                 break;
1643             }
1644 
1645             absPath += baseToken;
1646         }
1647 
1648         if ( !absPath.endsWith( "/" ) )
1649         {
1650             absPath += "/";
1651         }
1652 
1653         String newTarget = target;
1654         if ( newTarget.startsWith( "/" ) )
1655         {
1656             newTarget = newTarget.substring( 1 );
1657         }
1658 
1659         return absPath + newTarget;
1660     }
1661 
1662     /** {@inheritDoc} */
1663     protected MavenProject getProject()
1664     {
1665         return project;
1666     }
1667 
1668     /** {@inheritDoc} */
1669     protected String getOutputDirectory()
1670     {
1671         if ( !outputDirectory.isAbsolute() )
1672         {
1673             outputDirectory = new File( project.getBasedir(), outputDirectory.getPath() );
1674         }
1675 
1676         return outputDirectory.getAbsolutePath();
1677     }
1678 
1679     /** {@inheritDoc} */
1680     protected Renderer getSiteRenderer()
1681     {
1682         return siteRenderer;
1683     }
1684 
1685     /** {@inheritDoc} */
1686     public String getDescription( Locale locale )
1687     {
1688         return getBundle( locale ).getString( "report.changelog.description" );
1689     }
1690 
1691     /** {@inheritDoc} */
1692     public String getName( Locale locale )
1693     {
1694         return getBundle( locale ).getString( "report.changelog.name" );
1695     }
1696 
1697     /** {@inheritDoc} */
1698     public String getOutputName()
1699     {
1700         return "changelog";
1701     }
1702 
1703     /**
1704      * @param locale
1705      * @return the current bundle
1706      */
1707     protected ResourceBundle getBundle( Locale locale )
1708     {
1709         return ResourceBundle.getBundle( "scm-activity", locale, this.getClass().getClassLoader() );
1710     }
1711 
1712     /** {@inheritDoc} */
1713     public boolean canGenerateReport()
1714     {
1715         if ( offline && !outputXML.exists() )
1716         {
1717             return false;
1718         }
1719 
1720         return true;
1721     }
1722 }